github.com/GoogleCloudPlatform/compute-image-tools/cli_tools@v0.0.0-20240516224744-de2dabc4ed1b/common/utils/logging/service/logger_test.go (about) 1 // Copyright 2019 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 15 package service 16 17 import ( 18 "encoding/json" 19 "fmt" 20 "io/ioutil" 21 "net/http" 22 "os" 23 "strings" 24 "testing" 25 "time" 26 27 "github.com/stretchr/testify/assert" 28 29 "github.com/GoogleCloudPlatform/compute-image-tools/proto/go/pb" 30 ) 31 32 var ( 33 logger *Logger 34 serverLogEnabledPrevValue bool 35 ) 36 37 func TestMain(m *testing.M) { 38 setup() 39 code := m.Run() 40 shutdown() 41 os.Exit(code) 42 } 43 44 func setup() { 45 serverLogEnabledPrevValue = serverLogEnabled 46 serverLogEnabled = true 47 } 48 49 func shutdown() { 50 serverLogEnabled = serverLogEnabledPrevValue 51 } 52 53 func TestLogStart(t *testing.T) { 54 prepareTestLogger(t, nil, buildLogResponses(deleteRequest)) 55 56 e, r := logger.logStart() 57 58 if r != logResult(deleteRequest) { 59 t.Errorf("Unexpected logResult: %v, expect: %v", r, deleteRequest) 60 } 61 if e.Status != statusStart { 62 t.Errorf("Unexpected Status %v, expect: %v", e.Status, statusStart) 63 } 64 } 65 66 func TestLogSuccess(t *testing.T) { 67 prepareTestLogger(t, nil, buildLogResponses(deleteRequest)) 68 time.Sleep(20 * time.Millisecond) 69 70 w := literalLoggable{ 71 strings: map[string]string{ 72 importFileFormat: "vmdk", 73 }, 74 int64s: map[string][]int64{ 75 targetSizeGb: {5}, 76 sourceSizeGb: {3, 2, 1}, 77 }, 78 traceLogs: []string{ 79 "serial-log1", "serial-log2", 80 }, 81 inspectionResults: &pb.InspectionResults{ 82 OsCount: 1, 83 UefiBootable: true, 84 OsRelease: &pb.OsRelease{ 85 Architecture: pb.Architecture_X64, 86 CliFormatted: "ubuntu-2004", 87 Distro: "ubuntu", 88 MajorVersion: "20", 89 MinorVersion: "04", 90 DistroId: pb.Distro_UBUNTU, 91 }, 92 }, 93 } 94 95 e, r := logger.logSuccess(w) 96 97 if r != logResult(deleteRequest) { 98 t.Errorf("Unexpected logResult: %v, expect: %v", r, deleteRequest) 99 } 100 if e.Status != statusSuccess { 101 t.Errorf("Unexpected Status %v, expect: %v", e.Status, statusSuccess) 102 } 103 104 expectedInspectionResults := pb.InspectionResults{ 105 OsCount: 1, 106 UefiBootable: true, 107 OsRelease: &pb.OsRelease{ 108 Architecture: pb.Architecture_X64, 109 CliFormatted: "ubuntu-2004", 110 Distro: "ubuntu", 111 MajorVersion: "20", 112 MinorVersion: "04", 113 DistroId: pb.Distro_UBUNTU, 114 }, 115 } 116 117 expectedOutputInfo := OutputInfo{ 118 SourcesSizeGb: []int64{3, 2, 1}, 119 TargetsSizeGb: []int64{5}, 120 ImportFileFormat: "vmdk", 121 SerialOutputs: nil, // don't send serial output on success 122 InspectionResults: &expectedInspectionResults, 123 } 124 125 assert.Equal(t, &expectedInspectionResults, 126 e.InputParams.ImageImportParams.InspectionResults) 127 assert.Equal(t, expectedOutputInfo, *e.OutputInfo) 128 129 if e.ElapsedTimeMs < 20 { 130 t.Errorf("Unexpected ElapsedTimeMs %v < %v", e.ElapsedTimeMs, 20) 131 } 132 } 133 134 func TestLogFailure(t *testing.T) { 135 prepareTestLogger(t, nil, buildLogResponses(deleteRequest)) 136 time.Sleep(20 * time.Millisecond) 137 138 w := literalLoggable{ 139 strings: map[string]string{ 140 importFileFormat: "vmdk", 141 }, 142 int64s: map[string][]int64{ 143 targetSizeGb: {5}, 144 sourceSizeGb: {3, 2, 1}, 145 }, 146 traceLogs: []string{ 147 "serial-log1", "serial-log2", 148 }, 149 } 150 e, r := logger.logFailure(fmt.Errorf("error - [Privacy-> sensitive <-Privacy]"), w) 151 152 expected := OutputInfo{ 153 SourcesSizeGb: []int64{3, 2, 1}, 154 TargetsSizeGb: []int64{5}, 155 FailureMessage: "error - sensitive ", 156 FailureMessageWithoutPrivacyInfo: "error - ", 157 ImportFileFormat: "vmdk", 158 SerialOutputs: []string{"serial-log1", "serial-log2"}, 159 } 160 assert.Equal(t, expected, *e.OutputInfo) 161 162 if r != logResult(deleteRequest) { 163 t.Errorf("Unexpected logResult: %v, expect: %v", r, deleteRequest) 164 } 165 if e.Status != statusFailure { 166 t.Errorf("Unexpected Status %v, expect: %v", e.Status, statusFailure) 167 } 168 if e.ElapsedTimeMs < 20 { 169 t.Errorf("Unexpected ElapsedTimeMs %v < %v", e.ElapsedTimeMs, 20) 170 } 171 } 172 173 func TestRunWithServerLoggingSuccess(t *testing.T) { 174 prepareTestLogger(t, nil, buildLogResponses(deleteRequest, deleteRequest)) 175 176 logExtension, _ := logger.runWithServerLogging( 177 func() (Loggable, error) { 178 return literalLoggable{}, nil 179 }, nil) 180 if logExtension.Status != statusSuccess { 181 t.Errorf("Unexpected Status: %v, expect: %v", logExtension.Status, statusSuccess) 182 } 183 } 184 185 func TestRunWithServerLoggingFailed(t *testing.T) { 186 prepareTestLogger(t, nil, buildLogResponses(deleteRequest, deleteRequest)) 187 188 logExtension, _ := logger.runWithServerLogging( 189 func() (Loggable, error) { 190 return literalLoggable{}, fmt.Errorf("test msg - failure by purpose") 191 }, nil) 192 if logExtension.Status != statusFailure { 193 t.Errorf("Unexpected Status: %v, expect: %v", logExtension.Status, statusFailure) 194 } 195 } 196 197 func TestRunWithServerLogging_LogsFailure_WhenApplicationPanics(t *testing.T) { 198 prepareTestLogger(t, nil, buildLogResponses(deleteRequest, deleteRequest)) 199 200 panicMessage := "client code panic" 201 logExtension, err := logger.runWithServerLogging( 202 func() (Loggable, error) { 203 panic(panicMessage) 204 }, nil) 205 assert.EqualError(t, err, "A fatal error has occurred. Please submit an issue at https://github.com/GoogleCloudPlatform/compute-image-tools/issues") 206 assert.Equal(t, statusFailure, logExtension.Status) 207 208 // Include stacktrace and panic message in serial outputs. 209 assertContainsSubstring(t, "stacktrace", logExtension.OutputInfo.SerialOutputs) 210 assertContainsSubstring(t, panicMessage, logExtension.OutputInfo.SerialOutputs) 211 } 212 213 func assertContainsSubstring(t *testing.T, sub string, arr []string) { 214 for _, s := range arr { 215 if strings.Contains(s, sub) { 216 return 217 } 218 } 219 t.Errorf("Substring %q not found in %v", sub, arr) 220 } 221 222 func TestRunWithServerLoggingSuccessWithUpdatedProject(t *testing.T) { 223 prepareTestLogger(t, nil, buildLogResponses(deleteRequest, deleteRequest)) 224 225 project := "dummy-project" 226 logExtension, _ := logger.runWithServerLogging( 227 func() (Loggable, error) { 228 return literalLoggable{}, nil 229 }, &project) 230 if logExtension.Status != statusSuccess { 231 t.Errorf("Unexpected Status: %v, expect: %v", logExtension.Status, statusSuccess) 232 } 233 if logExtension.InputParams.ImageImportParams.Project != project { 234 t.Errorf("Unexpected Updated Project: %v, expect: %v", 235 logExtension.InputParams.ImageImportParams.Project, project) 236 } 237 } 238 239 func TestSendLogToServerSuccess(t *testing.T) { 240 testSendLogToServerWithResponses(t, logResult(deleteRequest), buildLogResponses(deleteRequest)) 241 } 242 243 func TestSendLogToServerResponseActionUnknown(t *testing.T) { 244 testSendLogToServerWithResponses(t, logResult(responseActionUnknown), buildLogResponses(responseActionUnknown)) 245 } 246 247 func TestSendLogToServerSuccessAfterRetry(t *testing.T) { 248 testSendLogToServerWithResponses(t, logResult(deleteRequest), buildLogResponses(retryRequestLater, retryRequestLater, deleteRequest)) 249 } 250 251 func TestSendLogToServerFailedOnCreateRequest(t *testing.T) { 252 backupServerURL := serverURL 253 serverURL = "%%bad-url" 254 defer func() { serverURL = backupServerURL }() 255 testSendLogToServerWithResponses(t, failedOnCreateRequest, nil) 256 } 257 258 func TestSendLogToServerFailedOnCreateRequestJSON(t *testing.T) { 259 prepareTestLogger(t, nil, nil) 260 r := logger.sendLogToServer(nil) 261 if r != logResult(failedOnCreateRequestJSON) { 262 t.Errorf("Unexpected Status: %v, expect: %v", r, failedOnCreateRequestJSON) 263 } 264 } 265 266 func TestSendLogToServerLogDisabled(t *testing.T) { 267 serverLogEnabled = false 268 defer func() { serverLogEnabled = true }() 269 270 testSendLogToServerWithResponses(t, serverLogDisabled, buildLogResponses(deleteRequest)) 271 } 272 273 func TestSendLogToServerFailedToParseResponse(t *testing.T) { 274 prepareTestLoggerWithJSONLogResponse(t, nil, []string{"bad-json"}) 275 r := logger.sendLogToServer(buildComputeImageToolsLogExtension()) 276 if r != logResult(failedToParseResponse) { 277 t.Errorf("Unexpected Status: %v, expect: %v", r, failedToParseResponse) 278 } 279 } 280 281 func TestSendLogToServerFailedOnUndefinedResponse(t *testing.T) { 282 testSendLogToServerWithResponses(t, failedOnUndefinedResponse, buildLogResponses("UndefinedResponseForTest")) 283 } 284 285 func TestSendLogToServerFailedOnMissingResponseDetails(t *testing.T) { 286 testSendLogToServerWithResponses(t, failedOnMissingResponseDetails, []logResponse{{}}) 287 } 288 289 func TestSendLogToServerFailedAfterRetry(t *testing.T) { 290 testSendLogToServerWithResponses(t, failedAfterRetry, buildLogResponses(retryRequestLater, retryRequestLater, retryRequestLater, deleteRequest)) 291 } 292 293 func testSendLogToServerWithResponses(t *testing.T, expectedLogResult logResult, resps []logResponse) { 294 prepareTestLogger(t, nil, resps) 295 r := logger.sendLogToServer(buildComputeImageToolsLogExtension()) 296 if r != logResult(expectedLogResult) { 297 t.Errorf("Unexpected Status: %v, expect: %v", r, expectedLogResult) 298 } 299 } 300 301 func TestRemoveNewLinesFromMultilineErrorSingleLine(t *testing.T) { 302 testRemoveNewLinesFromMultilineError(t, "Single line", "Single line") 303 } 304 305 func TestRemoveNewLinesFromMultilineErrorMultiLine(t *testing.T) { 306 testRemoveNewLinesFromMultilineError(t, "Header:\nFirst line\nSecond line", "Header: First line; Second line") 307 } 308 309 func TestRemoveNewLinesFromMultilineErrorEmptyString(t *testing.T) { 310 testRemoveNewLinesFromMultilineError(t, "", "") 311 } 312 313 func testRemoveNewLinesFromMultilineError(t *testing.T, input string, expected string) { 314 result := removeNewLinesFromMultilineError(input) 315 if result != expected { 316 t.Errorf("Expected %v, got %v", expected, result) 317 } 318 } 319 320 func prepareTestLogger(t *testing.T, err error, resps []logResponse) { 321 var lrs []string 322 for _, resp := range resps { 323 bytes, _ := json.Marshal(resp) 324 lrs = append(lrs, string(bytes)) 325 } 326 327 prepareTestLoggerWithJSONLogResponse(t, err, lrs) 328 } 329 330 func prepareTestLoggerWithJSONLogResponse(t *testing.T, err error, lrs []string) { 331 httpClient = &MockHTTPClient{ 332 t: t, 333 lrs: lrs, 334 err: err, 335 } 336 337 logger = NewLoggingServiceLogger(ImageImportAction, InputParams{ 338 ImageImportParams: &ImageImportParams{ 339 CommonParams: &CommonParams{ 340 ClientID: "test-client", 341 }, 342 }, 343 }) 344 } 345 346 func buildComputeImageToolsLogExtension() *ComputeImageToolsLogExtension { 347 logExtension := &ComputeImageToolsLogExtension{ 348 ID: "dummy-id", 349 CloudBuildID: "dummy-cloud-build-id", 350 ToolAction: ImageImportAction, 351 Status: statusStart, 352 InputParams: &InputParams{ 353 ImageImportParams: &ImageImportParams{ 354 CommonParams: &CommonParams{ 355 Project: "dummy-project", 356 ObfuscatedProject: Hash("dummy-project"), 357 }, 358 SourceImage: "dummy-image", 359 }, 360 }, 361 } 362 return logExtension 363 } 364 365 func buildLogResponses(actions ...responseAction) []logResponse { 366 var lrs []logResponse 367 for _, a := range actions { 368 lrs = append(lrs, logResponse{ 369 NextRequestWaitMillis: 100, 370 LogResponseDetails: []logResponseDetails{ 371 { 372 ResponseAction: a, 373 }, 374 }, 375 }) 376 } 377 return lrs 378 } 379 380 type MockHTTPClient struct { 381 t *testing.T 382 lrs []string 383 index int 384 err error 385 } 386 387 func (c *MockHTTPClient) Do(req *http.Request) (*http.Response, error) { 388 if c.index >= len(c.lrs) { 389 c.t.Fatal("Exceeds time of prepared mock calls") 390 } 391 392 bodyReader := ioutil.NopCloser(strings.NewReader(c.lrs[c.index])) 393 resp := http.Response{ 394 Body: bodyReader, 395 } 396 397 c.index++ 398 399 return &resp, c.err 400 }