github.com/ubuntu/ubuntu-report@v1.7.4-0.20240410144652-96f37d845fac/pkg/sysmetrics/run_test.go (about) 1 package sysmetrics 2 3 import ( 4 "bufio" 5 "bytes" 6 "context" 7 "encoding/json" 8 "flag" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "net/http" 13 "net/http/httptest" 14 "os" 15 "os/exec" 16 "path/filepath" 17 "strings" 18 "testing" 19 20 "github.com/ubuntu/ubuntu-report/internal/helper" 21 "github.com/ubuntu/ubuntu-report/internal/metrics" 22 ) 23 24 var Update = flag.Bool("update", false, "update golden files") 25 26 const ( 27 // ExpectedReportItem is the field we expect to always get in JSON 28 ExpectedReportItem = `"Version":` 29 30 // OptOutJSON is the data sent in case of Opt-Out choice 31 // export the private field for tests 32 OptOutJSON = optOutJSON 33 ) 34 35 func TestMetricsCollect(t *testing.T) { 36 t.Parallel() 37 38 testCases := []struct { 39 name string 40 root string 41 caseGPU string 42 caseCPU string 43 caseScreen string 44 casePartition string 45 caseArchitecture string 46 caseLibc6 string 47 caseHwCap string 48 env map[string]string 49 50 // note that only an internal json package error can make it returning an error 51 wantErr bool 52 }{ 53 {"regular", 54 "testdata/good", "one gpu", "regular", "one screen", 55 "one partition", "regular", "regular", "regular", 56 map[string]string{"XDG_CURRENT_DESKTOP": "some:thing", "XDG_SESSION_DESKTOP": "ubuntusession", "XDG_SESSION_TYPE": "x12"}, 57 false}, 58 } 59 for _, tc := range testCases { 60 tc := tc // capture range variable for parallel execution 61 t.Run(tc.name, func(t *testing.T) { 62 t.Parallel() 63 a := helper.Asserter{T: t} 64 65 m, cancelGPU, cancelCPU, cancelScreen, cancelPartition, 66 cancelArchitecture, cancelLibc6, cancelHwCap := newTestMetricsWithCommands(t, tc.root, 67 tc.caseGPU, tc.caseCPU, tc.caseScreen, tc.casePartition, 68 tc.caseArchitecture, tc.caseLibc6, tc.caseHwCap, tc.env) 69 defer cancelGPU() 70 defer cancelCPU() 71 defer cancelScreen() 72 defer cancelPartition() 73 defer cancelArchitecture() 74 defer cancelLibc6() 75 defer cancelHwCap() 76 b1, err1 := metricsCollect(m) 77 78 want := helper.LoadOrUpdateGolden(t, filepath.Join(tc.root, "gold", "metricscollect"), b1, *Update) 79 a.CheckWantedErr(err1, tc.wantErr) 80 a.Equal(b1, want) 81 82 // second run should return the same thing (idemnpotence) 83 m, cancelGPU, cancelCPU, cancelScreen, cancelPartition, 84 cancelArchitecture, cancelLibc6, cancelHwCap = newTestMetricsWithCommands(t, 85 tc.root, tc.caseGPU, tc.caseCPU, tc.caseScreen, tc.casePartition, 86 tc.caseArchitecture, tc.caseLibc6, tc.caseHwCap, tc.env) 87 defer cancelGPU() 88 defer cancelCPU() 89 defer cancelScreen() 90 defer cancelPartition() 91 defer cancelArchitecture() 92 defer cancelLibc6() 93 defer cancelHwCap() 94 b2, err2 := metricsCollect(m) 95 96 a.CheckWantedErr(err2, tc.wantErr) 97 var got1, got2 json.RawMessage 98 json.Unmarshal(b1, &got1) 99 json.Unmarshal(b2, &got2) 100 a.Equal(got1, got2) 101 }) 102 } 103 } 104 105 func TestMetricsSend(t *testing.T) { 106 t.Parallel() 107 108 testCases := []struct { 109 name string 110 root string 111 data []byte 112 ack bool 113 manualServerURL string 114 115 cacheReportP string 116 pendingReportP string 117 shouldHitServer bool 118 sHitHat string 119 wantErr bool 120 }{ 121 {"send data", 122 "testdata/good", []byte(`{ "some-data": true }`), true, "", 123 "ubuntu-report/ubuntu.18.04", "", true, "/ubuntu/desktop/18.04", false}, 124 {"nack send data", 125 "testdata/good", []byte(`{ "some-data": true }`), false, "", 126 "ubuntu-report/ubuntu.18.04", "", true, "/ubuntu/desktop/18.04", false}, 127 {"no IDs (mandatory)", 128 "testdata/no-ids", []byte(`{ "some-data": true }`), true, "", 129 "ubuntu-report", "", false, "", true}, 130 {"no network", 131 "testdata/good", []byte(`{ "some-data": true }`), true, "http://localhost:4299", 132 "ubuntu-report", "ubuntu-report/pending", false, "", true}, 133 {"invalid URL", 134 "testdata/good", []byte(`{ "some-data": true }`), true, "http://a b.com/", 135 "ubuntu-report", "", false, "", true}, 136 {"unwritable path", 137 "testdata/good", []byte(`{ "some-data": true }`), true, "", 138 "/unwritable/cache/path", "", true, "/ubuntu/desktop/18.04", true}, 139 } 140 for _, tc := range testCases { 141 tc := tc // capture range variable for parallel execution 142 t.Run(tc.name, func(t *testing.T) { 143 t.Parallel() 144 a := helper.Asserter{T: t} 145 146 m := metrics.NewTestMetrics(tc.root, nil, nil, nil, nil, nil, nil, nil, os.Getenv) 147 out, tearDown := helper.TempDir(t) 148 defer tearDown() 149 if strings.HasPrefix(tc.cacheReportP, "/") { 150 // absolute path, override temporary one 151 out = tc.cacheReportP 152 } 153 serverHitAt := "" 154 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 155 serverHitAt = r.URL.String() 156 })) 157 defer ts.Close() 158 url := tc.manualServerURL 159 if url == "" { 160 url = ts.URL 161 } 162 163 err := metricsSend(m, tc.data, tc.ack, false, url, out, os.Stdout, os.Stdin) 164 165 a.CheckWantedErr(err, tc.wantErr) 166 // check we didn't do too much work on error 167 if err != nil { 168 if !tc.shouldHitServer { 169 a.Equal(serverHitAt, "") 170 } 171 if tc.shouldHitServer && serverHitAt == "" { 172 t.Error("we should have hit the local server and it didn't") 173 } 174 if tc.pendingReportP == "" { 175 if _, err := os.Stat(filepath.Join(out, tc.cacheReportP)); !os.IsNotExist(err) { 176 t.Errorf("we didn't expect finding a cache report path as we erroring out") 177 } 178 } else { 179 gotF, err := os.Open(filepath.Join(out, tc.pendingReportP)) 180 if err != nil { 181 t.Fatal("didn't generate a pending report file on disk", err) 182 } 183 got, err := ioutil.ReadAll(gotF) 184 if err != nil { 185 t.Fatal("couldn't read generated pending report file", err) 186 } 187 want := helper.LoadOrUpdateGolden(t, filepath.Join(tc.root, "gold", fmt.Sprintf("metricssendpending.%s.%t", strings.Replace(tc.name, " ", "_", -1), tc.ack)), got, *Update) 188 a.Equal(got, want) 189 } 190 return 191 } 192 a.Equal(serverHitAt, tc.sHitHat) 193 gotF, err := os.Open(filepath.Join(out, tc.cacheReportP)) 194 if err != nil { 195 t.Fatal("didn't generate a report file on disk", err) 196 } 197 got, err := ioutil.ReadAll(gotF) 198 if err != nil { 199 t.Fatal("couldn't read generated report file", err) 200 } 201 202 want := helper.LoadOrUpdateGolden(t, filepath.Join(tc.root, "gold", fmt.Sprintf("metricssend.%s.%t", strings.Replace(tc.name, " ", "_", -1), tc.ack)), got, *Update) 203 a.Equal(got, want) 204 }) 205 } 206 } 207 208 func TestMultipleMetricsSend(t *testing.T) { 209 t.Parallel() 210 211 testCases := []struct { 212 name string 213 alwaysReport bool 214 215 cacheReportP string 216 shouldHitServer bool 217 sHitHat string 218 wantErr bool 219 }{ 220 {"fail report twice", false, "ubuntu-report/ubuntu.18.04", false, "/ubuntu/desktop/18.04", true}, 221 {"forcing report twice", true, "ubuntu-report/ubuntu.18.04", true, "/ubuntu/desktop/18.04", false}, 222 } 223 for _, tc := range testCases { 224 tc := tc // capture range variable for parallel execution 225 t.Run(tc.name, func(t *testing.T) { 226 t.Parallel() 227 a := helper.Asserter{T: t} 228 229 m := metrics.NewTestMetrics("testdata/good", nil, nil, nil, nil, nil, nil, nil, os.Getenv) 230 out, tearDown := helper.TempDir(t) 231 defer tearDown() 232 serverHitAt := "" 233 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 234 serverHitAt = r.URL.String() 235 })) 236 defer ts.Close() 237 238 err := metricsSend(m, []byte(`{ "some-data": true }`), true, tc.alwaysReport, ts.URL, out, os.Stdout, os.Stdin) 239 if err != nil { 240 t.Fatal("Didn't expect first call to fail") 241 } 242 243 // second call, reset server 244 serverHitAt = "" 245 m = metrics.NewTestMetrics("testdata/good", nil, nil, nil, nil, nil, nil, nil, os.Getenv) 246 err = metricsSend(m, []byte(`{ "some-data": true }`), true, tc.alwaysReport, ts.URL, out, os.Stdout, os.Stdin) 247 248 a.CheckWantedErr(err, tc.wantErr) 249 // check we didn't do too much work on error 250 if err != nil { 251 if !tc.shouldHitServer { 252 a.Equal(serverHitAt, "") 253 } 254 if tc.shouldHitServer && serverHitAt == "" { 255 t.Error("we should have hit the local server and we didn't") 256 } 257 return 258 } 259 a.Equal(serverHitAt, tc.sHitHat) 260 gotF, err := os.Open(filepath.Join(out, tc.cacheReportP)) 261 if err != nil { 262 t.Fatal("didn't generate a report file on disk", err) 263 } 264 got, err := ioutil.ReadAll(gotF) 265 if err != nil { 266 t.Fatal("couldn't read generated report file", err) 267 } 268 want := helper.LoadOrUpdateGolden(t, filepath.Join("testdata/good", "gold", fmt.Sprintf("metricssend_twice.%s", strings.Replace(tc.name, " ", "_", -1))), got, *Update) 269 a.Equal(got, want) 270 }) 271 } 272 } 273 274 func TestMetricsCollectAndSend(t *testing.T) { 275 t.Parallel() 276 277 testCases := []struct { 278 name string 279 root string 280 caseGPU string 281 caseCPU string 282 caseScreen string 283 casePartition string 284 caseArchitecture string 285 caseLibc6 string 286 caseHwCap string 287 env map[string]string 288 r ReportType 289 manualServerURL string 290 291 cacheReportP string 292 pendingReportP string 293 shouldHitServer bool 294 sHitHat string 295 wantErr bool 296 }{ 297 {"regular report auto", 298 "testdata/good", "one gpu", "regular", "one screen", 299 "one partition", "regular", "regular", "regular", 300 map[string]string{"XDG_CURRENT_DESKTOP": "some:thing", "XDG_SESSION_DESKTOP": "ubuntusession", "XDG_SESSION_TYPE": "x12", "LANG": "fr_FR.UTF-8", "LANGUAGE": "fr_FR.UTF-8"}, 301 ReportAuto, "", 302 "ubuntu-report/ubuntu.18.04", "", true, "/ubuntu/desktop/18.04", false}, 303 {"regular report OptOut", 304 "testdata/good", "one gpu", "regular", "one screen", 305 "one partition", "regular", "regular", "regular", 306 map[string]string{"XDG_CURRENT_DESKTOP": "some:thing", "XDG_SESSION_DESKTOP": "ubuntusession", "XDG_SESSION_TYPE": "x12", "LANG": "fr_FR.UTF-8", "LANGUAGE": "fr_FR.UTF-8"}, 307 ReportOptOut, "", 308 "ubuntu-report/ubuntu.18.04", "", true, "/ubuntu/desktop/18.04", false}, 309 {"no network", 310 "testdata/good", "", "", "", "", "", "", "", nil, ReportAuto, 311 "http://localhost:4299", "ubuntu-report", "ubuntu-report/pending", false, "", true}, 312 {"No IDs (mandatory)", 313 "testdata/no-ids", "", "", "", "", "", "", "", nil, ReportAuto, 314 "", "ubuntu-report", "", false, "", true}, 315 {"Invalid URL", 316 "testdata/good", "", "", "", "", "", "", "", nil, ReportAuto, 317 "http://a b.com/", "ubuntu-report", "", false, "", true}, 318 {"Unwritable path", 319 "testdata/good", "", "", "", "", "", "", "", nil, ReportAuto, 320 "", "/unwritable/cache/path", "", true, "/ubuntu/desktop/18.04", true}, 321 } 322 for _, tc := range testCases { 323 tc := tc // capture range variable for parallel execution 324 t.Run(tc.name, func(t *testing.T) { 325 t.Parallel() 326 a := helper.Asserter{T: t} 327 328 m, cancelGPU, cancelCPU, cancelScreen, cancelPartition, 329 cancelArchitecture, cancelLibc6, cancelHwCap := newTestMetricsWithCommands(t, tc.root, 330 tc.caseGPU, tc.caseCPU, tc.caseScreen, tc.casePartition, 331 tc.caseArchitecture, tc.caseLibc6, tc.caseHwCap, tc.env) 332 defer cancelGPU() 333 defer cancelCPU() 334 defer cancelScreen() 335 defer cancelPartition() 336 defer cancelArchitecture() 337 defer cancelLibc6() 338 defer cancelHwCap() 339 out, tearDown := helper.TempDir(t) 340 defer tearDown() 341 if strings.HasPrefix(tc.cacheReportP, "/") { 342 // absolute path, override temporary one 343 out = tc.cacheReportP 344 } 345 serverHitAt := "" 346 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 347 serverHitAt = r.URL.String() 348 })) 349 defer ts.Close() 350 url := tc.manualServerURL 351 if url == "" { 352 url = ts.URL 353 } 354 355 err := metricsCollectAndSend(m, tc.r, false, url, out, os.Stdout, os.Stdin) 356 357 a.CheckWantedErr(err, tc.wantErr) 358 // check we didn't do too much work on error 359 if err != nil { 360 if !tc.shouldHitServer { 361 a.Equal(serverHitAt, "") 362 } 363 if tc.shouldHitServer && serverHitAt == "" { 364 t.Error("we should have hit the local server and it didn't") 365 } 366 if tc.pendingReportP == "" { 367 if _, err := os.Stat(filepath.Join(out, tc.cacheReportP)); !os.IsNotExist(err) { 368 t.Errorf("we didn't expect finding a cache report path as we erroring out") 369 } 370 } else { 371 gotF, err := os.Open(filepath.Join(out, tc.pendingReportP)) 372 if err != nil { 373 t.Fatal("didn't generate a pending report file on disk", err) 374 } 375 got, err := ioutil.ReadAll(gotF) 376 if err != nil { 377 t.Fatal("couldn't read generated pending report file", err) 378 } 379 want := helper.LoadOrUpdateGolden(t, filepath.Join(tc.root, "gold", fmt.Sprintf("pendingreport.ReportType%d", int(tc.r))), got, *Update) 380 a.Equal(got, want) 381 } 382 return 383 } 384 a.Equal(serverHitAt, tc.sHitHat) 385 gotF, err := os.Open(filepath.Join(out, tc.cacheReportP)) 386 if err != nil { 387 t.Fatal("didn't generate a report file on disk", err) 388 } 389 got, err := ioutil.ReadAll(gotF) 390 if err != nil { 391 t.Fatal("couldn't read generated report file", err) 392 } 393 want := helper.LoadOrUpdateGolden(t, filepath.Join(tc.root, "gold", fmt.Sprintf("cachereport.ReportType%d", int(tc.r))), got, *Update) 394 a.Equal(got, want) 395 }) 396 } 397 } 398 399 func TestMultipleMetricsCollectAndSend(t *testing.T) { 400 t.Parallel() 401 402 testCases := []struct { 403 name string 404 alwaysReport bool 405 406 cacheReportP string 407 shouldHitServer bool 408 sHitHat string 409 wantErr bool 410 }{ 411 {"fail report twice", false, "ubuntu-report/ubuntu.18.04", false, "/ubuntu/desktop/18.04", true}, 412 {"forcing report twice", true, "ubuntu-report/ubuntu.18.04", true, "/ubuntu/desktop/18.04", false}, 413 } 414 for _, tc := range testCases { 415 tc := tc // capture range variable for parallel execution 416 t.Run(tc.name, func(t *testing.T) { 417 t.Parallel() 418 a := helper.Asserter{T: t} 419 420 m, cancelGPU, cancelCPU, cancelScreen, cancelPartition, 421 cancelArchitecture, cancelLibc6, cancelHwCap := newTestMetricsWithCommands(t, 422 "testdata/good", "one gpu", "regular", "one screen", 423 "one partition", "regular", "regular", "regular", 424 map[string]string{"XDG_CURRENT_DESKTOP": "some:thing", "XDG_SESSION_DESKTOP": "ubuntusession", "XDG_SESSION_TYPE": "x12", "LANG": "fr_FR.UTF-8", "LANGUAGE": "fr_FR.UTF-8"}) 425 defer cancelGPU() 426 defer cancelCPU() 427 defer cancelScreen() 428 defer cancelPartition() 429 defer cancelArchitecture() 430 defer cancelLibc6() 431 defer cancelHwCap() 432 out, tearDown := helper.TempDir(t) 433 defer tearDown() 434 serverHitAt := "" 435 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 436 serverHitAt = r.URL.String() 437 })) 438 defer ts.Close() 439 440 err := metricsCollectAndSend(m, ReportAuto, tc.alwaysReport, ts.URL, out, os.Stdout, os.Stdin) 441 if err != nil { 442 t.Fatal("Didn't expect first call to fail") 443 } 444 445 // second call, reset server 446 serverHitAt = "" 447 m, cancelGPU, cancelCPU, cancelScreen, cancelPartition, 448 cancelArchitecture, cancelLibc6, cancelHwCap = newTestMetricsWithCommands(t, 449 "testdata/good", "one gpu", "regular", "one screen", 450 "one partition", "regular", "regular", "regular", 451 map[string]string{"XDG_CURRENT_DESKTOP": "some:thing", "XDG_SESSION_DESKTOP": "ubuntusession", "XDG_SESSION_TYPE": "x12", "LANG": "fr_FR.UTF-8", "LANGUAGE": "fr_FR.UTF-8"}) 452 defer cancelGPU() 453 defer cancelCPU() 454 defer cancelScreen() 455 defer cancelPartition() 456 defer cancelArchitecture() 457 defer cancelLibc6() 458 defer cancelHwCap() 459 err = metricsCollectAndSend(m, ReportAuto, tc.alwaysReport, ts.URL, out, os.Stdout, os.Stdin) 460 461 a.CheckWantedErr(err, tc.wantErr) 462 // check we didn't do too much work on error 463 if err != nil { 464 if !tc.shouldHitServer { 465 a.Equal(serverHitAt, "") 466 } 467 if tc.shouldHitServer && serverHitAt == "" { 468 t.Error("we should have hit the local server and we didn't") 469 } 470 return 471 } 472 a.Equal(serverHitAt, tc.sHitHat) 473 gotF, err := os.Open(filepath.Join(out, tc.cacheReportP)) 474 if err != nil { 475 t.Fatal("didn't generate a report file on disk", err) 476 } 477 got, err := ioutil.ReadAll(gotF) 478 if err != nil { 479 t.Fatal("couldn't read generated report file", err) 480 } 481 want := helper.LoadOrUpdateGolden(t, filepath.Join("testdata/good", "gold", fmt.Sprintf("cachereport-twice.ReportType%d", int(ReportAuto))), got, *Update) 482 a.Equal(got, want) 483 }) 484 } 485 } 486 487 func TestMetricsCollectAndSendOnUpgrade(t *testing.T) { 488 t.Parallel() 489 490 testCases := []struct { 491 name string 492 previousReportP string 493 494 cacheReportP string 495 shouldHitServer bool 496 wantOptOut bool 497 wantErr bool 498 }{ 499 {"without previous report", 500 "", 501 "", false, false, false}, 502 {"with previous report, current release", 503 "testdata/previous_reports/current_release", 504 "", false, false, true}, 505 {"with previous report, previous release opt in", 506 "testdata/previous_reports/previous_release_optin", 507 "ubuntu-report/ubuntu.18.04", true, false, false}, 508 {"with previous report, previous release opt out", 509 "testdata/previous_reports/previous_release_optout", 510 "ubuntu-report/ubuntu.18.04", true, true, false}, 511 {"with two previous reports, latest previous release opt in", 512 "testdata/previous_reports/latest_previous_release_optin", 513 "ubuntu-report/ubuntu.18.04", true, false, false}, 514 {"with two previous reports, latest previous release opt out", 515 "testdata/previous_reports/latest_previous_release_optout", 516 "ubuntu-report/ubuntu.18.04", true, true, false}, 517 {"with different distro reports, current optin, other distro more recent opt out", 518 "testdata/previous_reports/previous_with_different_distros", 519 "ubuntu-report/ubuntu.18.04", true, false, false}, 520 } 521 for _, tc := range testCases { 522 tc := tc // capture range variable for parallel execution 523 t.Run(tc.name, func(t *testing.T) { 524 t.Parallel() 525 a := helper.Asserter{T: t} 526 527 m, cancelGPU, cancelCPU, cancelScreen, cancelPartition, 528 cancelArchitecture, cancelLibc6, cancelHwCap := newTestMetricsWithCommands(t, 529 "testdata/good", "one gpu", "regular", "one screen", 530 "one partition", "regular", "regular", "regular", 531 map[string]string{"XDG_CURRENT_DESKTOP": "some:thing", "XDG_SESSION_DESKTOP": "ubuntusession", 532 "XDG_SESSION_TYPE": "x12", "LANG": "fr_FR.UTF-8", "LANGUAGE": "fr_FR.UTF-8"}) 533 defer cancelGPU() 534 defer cancelCPU() 535 defer cancelScreen() 536 defer cancelPartition() 537 defer cancelArchitecture() 538 defer cancelLibc6() 539 defer cancelHwCap() 540 out, tearDown := helper.TempDir(t) 541 defer tearDown() 542 543 if tc.previousReportP != "" { 544 reportDir := filepath.Join(out, "ubuntu-report") 545 if err := os.MkdirAll(reportDir, 0700); err != nil { 546 t.Fatalf("couldn't create report directory: %v", err) 547 } 548 files, err := ioutil.ReadDir(tc.previousReportP) 549 if err != nil { 550 t.Fatalf("couldn't list files under %s: %v", tc.previousReportP, err) 551 } 552 for _, file := range files { 553 data, err := ioutil.ReadFile(filepath.Join(tc.previousReportP, file.Name())) 554 if err != nil { 555 t.Fatalf("couldn't read report file: %v", err) 556 } 557 if err = ioutil.WriteFile(filepath.Join(reportDir, file.Name()), data, 0644); err != nil { 558 t.Fatalf("couldn't write to destination report file in setup: %v", err) 559 } 560 } 561 } 562 563 serverHit := false 564 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 565 serverHit = true 566 })) 567 defer ts.Close() 568 url := ts.URL 569 570 err := metricsCollectAndSendOnUpgrade(m, false, url, out, os.Stdout, os.Stdin) 571 572 a.CheckWantedErr(err, tc.wantErr) 573 // check we didn't do too much work on error 574 if err != nil { 575 if tc.shouldHitServer && serverHit == false { 576 t.Error("we should have hit the local server and we didn't") 577 } 578 if !tc.shouldHitServer && serverHit == true { 579 t.Error("we have hit the local server when we shouldn't have") 580 } 581 return 582 } 583 a.Equal(serverHit, tc.shouldHitServer) 584 // case with no report to generate (no previous answer) 585 if tc.cacheReportP == "" { 586 files, err := ioutil.ReadDir(filepath.Join(out, "ubuntu-report")) 587 if err != nil { 588 return 589 } 590 if len(files) != 0 { 591 t.Fatalf("we expected no report to be generated but we found some") 592 } 593 return 594 } 595 596 gotF, err := os.Open(filepath.Join(out, tc.cacheReportP)) 597 if err != nil { 598 t.Fatal("didn't generate a report file on disk", err) 599 } 600 got, err := ioutil.ReadAll(gotF) 601 if err != nil { 602 t.Fatal("couldn't read generated report file", err) 603 } 604 isOptOut := strings.Contains(string(got), optOutJSON) 605 606 if tc.wantOptOut && !isOptOut { 607 t.Errorf("we wanted an opt out as we opted out in previous release but got some data in: %s", got) 608 } else if !tc.wantOptOut && isOptOut { 609 t.Errorf("we wanted some data which are not opt out information, but got opt out content instead") 610 } 611 }) 612 } 613 } 614 615 func TestInteractiveMetricsCollectAndSend(t *testing.T) { 616 t.Parallel() 617 618 testCases := []struct { 619 name string 620 answers []string 621 622 cacheReportP string 623 wantWriteAndUpload bool 624 }{ 625 {"yes", []string{"yes"}, "ubuntu-report/ubuntu.18.04", true}, 626 {"y", []string{"y"}, "ubuntu-report/ubuntu.18.04", true}, 627 {"YES", []string{"YES"}, "ubuntu-report/ubuntu.18.04", true}, 628 {"Y", []string{"Y"}, "ubuntu-report/ubuntu.18.04", true}, 629 {"no", []string{"no"}, "ubuntu-report/ubuntu.18.04", true}, 630 {"n", []string{"n"}, "ubuntu-report/ubuntu.18.04", true}, 631 {"NO", []string{"NO"}, "ubuntu-report/ubuntu.18.04", true}, 632 {"n", []string{"N"}, "ubuntu-report/ubuntu.18.04", true}, 633 {"quit", []string{"quit"}, "ubuntu-report/ubuntu.18.04", false}, 634 {"q", []string{"q"}, "ubuntu-report/ubuntu.18.04", false}, 635 {"QUIT", []string{"QUIT"}, "ubuntu-report/ubuntu.18.04", false}, 636 {"Q", []string{"Q"}, "ubuntu-report/ubuntu.18.04", false}, 637 {"default-quit", []string{""}, "ubuntu-report/ubuntu.18.04", false}, 638 {"garbage-then-quit", []string{"garbage", "yesgarbage", "nogarbage", "quitgarbage", "Q"}, "ubuntu-report/ubuntu.18.04", false}, 639 {"ctrl-c-input", []string{"CTRL-C"}, "ubuntu-report/ubuntu.18.04", false}, 640 } 641 for _, tc := range testCases { 642 tc := tc // capture range variable for parallel execution 643 t.Run(tc.name, func(t *testing.T) { 644 t.Parallel() 645 a := helper.Asserter{T: t} 646 647 m, cancelGPU, cancelCPU, cancelScreen, cancelPartition, 648 cancelArchitecture, cancelLibc6, cancelHwCap := newTestMetricsWithCommands(t, 649 "testdata/good", "one gpu", "regular", "one screen", 650 "one partition", "regular", "regular", "regular", 651 map[string]string{"XDG_CURRENT_DESKTOP": "some:thing", "XDG_SESSION_DESKTOP": "ubuntusession", "XDG_SESSION_TYPE": "x12", "LANG": "fr_FR.UTF-8", "LANGUAGE": "fr_FR.UTF-8"}) 652 defer cancelGPU() 653 defer cancelCPU() 654 defer cancelScreen() 655 defer cancelPartition() 656 defer cancelArchitecture() 657 defer cancelLibc6() 658 defer cancelHwCap() 659 out, tearDown := helper.TempDir(t) 660 defer tearDown() 661 serverHitAt := "" 662 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 663 serverHitAt = r.URL.String() 664 })) 665 defer ts.Close() 666 667 stdin, stdinW := io.Pipe() 668 stdout, stdoutW := io.Pipe() 669 670 cmdErrs := helper.RunFunctionWithTimeout(t, func() error { return metricsCollectAndSend(m, ReportInteractive, false, ts.URL, out, stdin, stdoutW) }) 671 672 gotJSONReport := false 673 answerIndex := 0 674 scanner := bufio.NewScanner(stdout) 675 scanner.Split(ScanLinesOrQuestion) 676 for scanner.Scan() { 677 txt := scanner.Text() 678 // first, we should have a known element 679 if strings.Contains(txt, ExpectedReportItem) { 680 gotJSONReport = true 681 } 682 if !strings.Contains(txt, "Do you agree to report this?") { 683 continue 684 } 685 a := tc.answers[answerIndex] 686 if a == "CTRL-C" { 687 stdinW.Close() 688 break 689 } else { 690 stdinW.Write([]byte(tc.answers[answerIndex] + "\n")) 691 } 692 answerIndex = answerIndex + 1 693 // all answers have be provided 694 if answerIndex >= len(tc.answers) { 695 stdinW.Close() 696 break 697 } 698 } 699 700 if err := <-cmdErrs; err != nil { 701 t.Fatal("didn't expect to get an error, got:", err) 702 } 703 a.Equal(gotJSONReport, true) 704 705 // check we didn't do too much work on error 706 if !tc.wantWriteAndUpload { 707 a.Equal(serverHitAt, "") 708 if _, err := os.Stat(filepath.Join(out, tc.cacheReportP)); !os.IsNotExist(err) { 709 t.Errorf("we didn't expect finding a cache report path as we said to quit") 710 } 711 return 712 } 713 if serverHitAt == "" { 714 t.Error("we should have hit the local server and we didn't") 715 } 716 gotF, err := os.Open(filepath.Join(out, tc.cacheReportP)) 717 if err != nil { 718 t.Fatal("didn't generate a report file on disk", err) 719 } 720 got, err := ioutil.ReadAll(gotF) 721 if err != nil { 722 t.Fatal("couldn't read generated report file", err) 723 } 724 725 // To avoid case-insensitive file name collisions, append command case to golden file name. 726 cmdCase := "lc" 727 if 'A' <= tc.name[0] && tc.name[0] <= 'Z' { 728 cmdCase = "uc" 729 } 730 731 want := helper.LoadOrUpdateGolden(t, filepath.Join("testdata/good", "gold", fmt.Sprintf("cachereport-twice.ReportType%d-%s-%s", int(ReportInteractive), strings.Replace(tc.name, " ", "-", -1), cmdCase)), got, *Update) 732 a.Equal(got, want) 733 }) 734 } 735 } 736 737 func TestMetricsSendPendingReport(t *testing.T) { 738 t.Parallel() 739 initialReportTimeoutDuration = 0 740 741 testCases := []struct { 742 name string 743 root string 744 manualServerURL string 745 746 cacheReportP string 747 pendingReportP string 748 pendingReportKept bool 749 numHitServer int 750 sHitHat string 751 wantErr bool 752 }{ 753 {"send previous report", 754 "testdata/good", "", 755 "ubuntu-report/ubuntu.18.04", "ubuntu-report/pending", false, 1, "/ubuntu/desktop/18.04", false}, 756 {"no previous report", 757 "testdata/good", "", 758 "", "", false, 0, "", true}, 759 {"send previous report after backoff", 760 "testdata/good", "", 761 "ubuntu-report/ubuntu.18.04", "ubuntu-report/pending", false, 2, "/ubuntu/desktop/18.04", false}, 762 {"no IDs (mandatory)", 763 "testdata/no-ids", "", 764 "", "", false, 0, "", true}, 765 {"invalid URL", 766 "testdata/good", "http://a b.com/", 767 "", "", false, 0, "", true}, 768 {"unwritable path", 769 "testdata/good", "", 770 "", "ubuntu-report/pending", true, 1, "/ubuntu/desktop/18.04", true}, 771 } 772 for _, tc := range testCases { 773 tc := tc // capture range variable for parallel execution 774 t.Run(tc.name, func(t *testing.T) { 775 t.Parallel() 776 a := helper.Asserter{T: t} 777 778 m := metrics.NewTestMetrics(tc.root, nil, nil, nil, nil, nil, nil, nil, os.Getenv) 779 out, tearDown := helper.TempDir(t) 780 defer tearDown() 781 if strings.HasPrefix(tc.cacheReportP, "/") { 782 // absolute path, override temporary one 783 out = tc.cacheReportP 784 } 785 var pendingReportData []byte 786 var err error 787 resetwritable := func() {} 788 if tc.pendingReportP != "" { 789 if pendingReportData, err = ioutil.ReadFile(filepath.Join(tc.root, tc.pendingReportP)); err != nil { 790 t.Fatalf("couldn't open pending report file: %v", err) 791 } 792 tc.pendingReportP = filepath.Join(out, tc.pendingReportP) 793 d := filepath.Dir(tc.pendingReportP) 794 if err := os.MkdirAll(d, 0700); err != nil { 795 t.Fatal("couldn't create parent directory of pending report", err) 796 } 797 if err := ioutil.WriteFile(tc.pendingReportP, pendingReportData, 0644); err != nil { 798 t.Fatalf("couldn't copy pending report file to cache directory: %v", err) 799 } 800 // switch back mode to unwritable 801 if strings.HasPrefix(tc.name, "unwritable") { 802 if err := os.Chmod(d, 0500); err != nil { 803 t.Fatalf("couldn't switch %s to not being writable: %v", d, err) 804 } 805 resetwritable = func() { 806 if err := os.Chmod(d, 0700); err != nil { 807 t.Fatalf("couldn't switch %s back to being writable: %v", d, err) 808 } 809 } 810 defer resetwritable() 811 } 812 } 813 814 serverHitAt := "" 815 numHitServer := 0 816 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 817 numHitServer++ 818 if numHitServer < tc.numHitServer { 819 http.NotFound(w, r) 820 return 821 } 822 serverHitAt = r.URL.String() 823 })) 824 defer ts.Close() 825 url := tc.manualServerURL 826 if url == "" { 827 url = ts.URL 828 } 829 830 err = metricsSendPendingReport(m, url, out, os.Stdout, os.Stdin) 831 832 // restore directory state for checking 833 resetwritable() 834 835 a.CheckWantedErr(err, tc.wantErr) 836 a.Equal(numHitServer, tc.numHitServer) 837 a.Equal(serverHitAt, tc.sHitHat) 838 839 _, pendingReportErr := os.Stat(tc.pendingReportP) 840 if !tc.pendingReportKept && os.IsExist(pendingReportErr) { 841 t.Errorf("we expected the pending report to be removed and it wasn't") 842 } else if tc.pendingReportKept && os.IsNotExist(pendingReportErr) { 843 t.Errorf("we expected the pending report to be kept and it was removed") 844 } 845 846 // check we didn't do too much work on error 847 if err != nil { 848 if _, err := os.Stat(filepath.Join(out, tc.cacheReportP)); os.IsExist(err) { 849 t.Errorf("we didn't expect finding a cache report path as we erroring out") 850 } 851 return 852 } 853 854 gotF, err := os.Open(filepath.Join(out, tc.cacheReportP)) 855 if err != nil { 856 t.Fatal("didn't generate a report file on disk", err) 857 } 858 got, err := ioutil.ReadAll(gotF) 859 if err != nil { 860 t.Fatal("couldn't read generated report file", err) 861 } 862 a.Equal(got, pendingReportData) 863 }) 864 } 865 } 866 867 func newMockShortCmd(t *testing.T, s ...string) (*exec.Cmd, context.CancelFunc) { 868 t.Helper() 869 return helper.ShortProcess(t, "TestMetricsHelperProcess", s...) 870 } 871 872 func newTestMetricsWithCommands(t *testing.T, root, caseGPU, caseCPU, caseScreen, casePartition, caseArch string, caseHwCap string, caseLibc6 string, env map[string]string) (m metrics.Metrics, 873 cancelGPU, cancelCPU, cancelSreen, cancelPartition, cancelArchitecture, cancelLibc6, cancelHwCap context.CancelFunc) { 874 t.Helper() 875 cmdGPU, cancelGPU := newMockShortCmd(t, "lspci", "-n", caseGPU) 876 cmdCPU, cancelCPU := newMockShortCmd(t, "lscpu", "-J", caseCPU) 877 cmdScreen, cancelScreen := newMockShortCmd(t, "xrandr", caseScreen) 878 cmdPartition, cancelPartition := newMockShortCmd(t, "df", casePartition) 879 cmdArchitecture, cancelArchitecture := newMockShortCmd(t, "dpkg", "--print-architecture", caseArch) 880 cmdLibc6, cancelLibc6 := newMockShortCmd(t, "dpkg", "--status", "libc6", caseHwCap) 881 cmdHwCap, cancelHwCap := newMockShortCmd(t, "/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", "--help", caseHwCap) 882 return metrics.NewTestMetrics(root, cmdGPU, cmdCPU, cmdScreen, cmdPartition, 883 cmdArchitecture, cmdLibc6, cmdHwCap, helper.GetenvFromMap(env)), 884 cancelGPU, cancelCPU, cancelScreen, cancelPartition, 885 cancelArchitecture, cancelLibc6, cancelHwCap 886 } 887 888 // ScanLinesOrQuestion is copy of ScanLines, adding the expected question string as we don't return here 889 func ScanLinesOrQuestion(data []byte, atEOF bool) (advance int, token []byte, err error) { 890 if atEOF && len(data) == 0 { 891 return 0, nil, nil 892 } 893 if i := bytes.IndexByte(data, '\n'); i >= 0 { 894 // We have a full newline-terminated line. 895 return i + 1, dropCR(data[0:i]), nil 896 } 897 if i := bytes.IndexByte(data, ']'); i >= 0 { 898 // We have a full newline-terminated line. 899 return i + 1, dropCR(data[0:i]), nil 900 } 901 // If we're at EOF, we have a final, non-terminated line. Return it. 902 if atEOF { 903 return len(data), dropCR(data), nil 904 } 905 // Request more data. 906 return 0, nil, nil 907 } 908 909 // dropCR drops a terminal \r from the data. 910 func dropCR(data []byte) []byte { 911 if len(data) > 0 && data[len(data)-1] == '\r' { 912 return data[0 : len(data)-1] 913 } 914 return data 915 }