github.com/ubuntu/ubuntu-report@v1.7.4-0.20240410144652-96f37d845fac/pkg/sysmetrics/C/libsystemetricstest.go (about) 1 package main 2 3 // #include <stdbool.h> 4 // #include <stdio.h> 5 // #include <stdlib.h> 6 // extern char* sysmetrics_collect(char** p0); 7 // typedef enum { 8 // sysmetrics_report_interactive = 0, 9 // sysmetrics_report_auto = 1, 10 // sysmetrics_report_optout = 2, 11 // } sysmetrics_report_type; 12 // typedef unsigned char GoUint8; 13 // extern char* sysmetrics_send_report(char* p0, GoUint8 p1, char* p2); 14 // extern char* sysmetrics_send_decline(GoUint8 p0, char* p1); 15 // extern char* sysmetrics_collect_and_send(sysmetrics_report_type p0, GoUint8 p1, char* p2); 16 import "C" 17 18 import ( 19 "bufio" 20 "bytes" 21 "errors" 22 "fmt" 23 "io/ioutil" 24 "net/http" 25 "net/http/httptest" 26 "os" 27 "path/filepath" 28 "strings" 29 "testing" 30 "unsafe" 31 32 "github.com/ubuntu/ubuntu-report/internal/helper" 33 "github.com/ubuntu/ubuntu-report/pkg/sysmetrics" 34 ) 35 36 /* 37 The C API is calling the Go API, which is heavily tested. Consequently, we only test 38 main cases. 39 */ 40 41 const ( 42 expectedReportItem = `"Version":` 43 optOutJSON = `{"OptOut": true}` 44 ) 45 46 func testCollect(t *testing.T) { 47 t.Parallel() 48 49 var res *C.char 50 defer C.free(unsafe.Pointer(res)) 51 52 err := C.sysmetrics_collect(&res) 53 defer C.free(unsafe.Pointer(err)) 54 55 if err != nil { 56 t.Fatal("we didn't expect an error and got one", C.GoString(err)) 57 } 58 data := C.GoString(res) 59 if !strings.Contains(data, expectedReportItem) { 60 t.Errorf("we expected at least %s in output, got: '%s", expectedReportItem, data) 61 } 62 } 63 64 func testSendReport(t *testing.T) { 65 // we change current path and env variable: not parallelizable tests 66 helper.SkipIfShort(t) 67 68 testCases := []struct { 69 name string 70 71 shouldHitServer bool 72 wantErr bool 73 }{ 74 {"regular send", true, false}, 75 } 76 for _, tc := range testCases { 77 tc := tc // capture range variable for parallel execution 78 t.Run(tc.name, func(t *testing.T) { 79 a := helper.Asserter{T: t} 80 81 out, tearDown := helper.TempDir(t) 82 defer tearDown() 83 defer helper.ChangeEnv("XDG_CACHE_HOME", out)() 84 out = filepath.Join(out, "ubuntu-report") 85 // we don't really care where we hit for this API integration test, internal ones test it 86 // and we don't really control /etc/os-release version and id. 87 // Same for report file 88 serverHit := false 89 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 90 serverHit = true 91 })) 92 defer ts.Close() 93 94 cData := C.CString(fmt.Sprintf(`{ %s: "18.04" }`, expectedReportItem)) 95 url := C.CString(ts.URL) 96 defer C.free(unsafe.Pointer(url)) 97 98 err := C.sysmetrics_send_report(cData, C.uchar(0), url) 99 defer C.free(unsafe.Pointer(err)) 100 101 if err != nil { 102 t.Fatal("we didn't expect getting an error, got:", err) 103 } 104 105 a.Equal(serverHit, tc.shouldHitServer) 106 p := filepath.Join(out, helper.FindInDirectory(t, "", out)) 107 data, errread := ioutil.ReadFile(p) 108 if errread != nil { 109 t.Fatalf("couldn't open report file %s", out) 110 } 111 d := string(data) 112 if !strings.Contains(d, expectedReportItem) { 113 t.Errorf("we expected to find %s in report file, got: %s", expectedReportItem, d) 114 } 115 }) 116 } 117 } 118 119 func testSendDecline(t *testing.T) { 120 // we change current path and env variable: not parallelizable tests 121 helper.SkipIfShort(t) 122 123 testCases := []struct { 124 name string 125 126 shouldHitServer bool 127 wantErr bool 128 }{ 129 {"regular send opt-out", true, false}, 130 } 131 for _, tc := range testCases { 132 tc := tc // capture range variable for parallel execution 133 t.Run(tc.name, func(t *testing.T) { 134 a := helper.Asserter{T: t} 135 136 out, tearDown := helper.TempDir(t) 137 defer tearDown() 138 defer helper.ChangeEnv("XDG_CACHE_HOME", out)() 139 out = filepath.Join(out, "ubuntu-report") 140 // we don't really care where we hit for this API integration test, internal ones test it 141 // and we don't really control /etc/os-release version and id. 142 // Same for report file 143 serverHit := false 144 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 145 serverHit = true 146 })) 147 defer ts.Close() 148 149 url := C.CString(ts.URL) 150 defer C.free(unsafe.Pointer(url)) 151 152 err := C.sysmetrics_send_decline(C.uchar(0), url) 153 defer C.free(unsafe.Pointer(err)) 154 155 if err != nil { 156 t.Fatal("we didn't expect getting an error, got:", err) 157 } 158 159 a.Equal(serverHit, tc.shouldHitServer) 160 p := filepath.Join(out, helper.FindInDirectory(t, "", out)) 161 data, errread := ioutil.ReadFile(p) 162 if errread != nil { 163 t.Fatalf("couldn't open report file %s", out) 164 } 165 d := string(data) 166 if !strings.Contains(d, optOutJSON) { 167 t.Errorf("we expected to find %s in report file, got: %s", optOutJSON, d) 168 } 169 }) 170 } 171 } 172 173 func testNonInteractiveCollectAndSend(t *testing.T) { 174 // we change current path and env variable: not parallelizable tests 175 helper.SkipIfShort(t) 176 177 testCases := []struct { 178 name string 179 r sysmetrics.ReportType 180 181 shouldHitServer bool 182 wantErr bool 183 }{ 184 {"regular report auto", sysmetrics.ReportAuto, true, false}, 185 {"regular report opt-out", sysmetrics.ReportOptOut, true, false}, 186 } 187 for _, tc := range testCases { 188 tc := tc // capture range variable for parallel execution 189 t.Run(tc.name, func(t *testing.T) { 190 a := helper.Asserter{T: t} 191 192 out, tearDown := helper.TempDir(t) 193 defer tearDown() 194 defer helper.ChangeEnv("XDG_CACHE_HOME", out)() 195 out = filepath.Join(out, "ubuntu-report") 196 // we don't really care where we hit for this API integration test, internal ones test it 197 // and we don't really control /etc/os-release version and id. 198 // Same for report file 199 serverHit := false 200 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 201 serverHit = true 202 })) 203 defer ts.Close() 204 205 url := C.CString(ts.URL) 206 defer C.free(unsafe.Pointer(url)) 207 208 err := C.sysmetrics_collect_and_send(C.sysmetrics_report_type(tc.r), C.uchar(0), url) 209 defer C.free(unsafe.Pointer(err)) 210 211 if err != nil { 212 t.Fatal("we didn't expect getting an error, got:", err) 213 } 214 215 a.Equal(serverHit, tc.shouldHitServer) 216 p := filepath.Join(out, helper.FindInDirectory(t, "", out)) 217 data, errread := ioutil.ReadFile(p) 218 if errread != nil { 219 t.Fatalf("couldn't open report file %s", out) 220 } 221 d := string(data) 222 switch tc.r { 223 case sysmetrics.ReportAuto: 224 if !strings.Contains(d, expectedReportItem) { 225 t.Errorf("we expected to find %s in report file, got: %s", expectedReportItem, d) 226 } 227 case sysmetrics.ReportOptOut: 228 if !strings.Contains(d, optOutJSON) { 229 t.Errorf("we expected to find %s in report file, got: %s", optOutJSON, d) 230 } 231 } 232 }) 233 } 234 } 235 236 func testInteractiveCollectAndSend(t *testing.T) { 237 // we change current path and env variable: not parallelizable tests 238 helper.SkipIfShort(t) 239 240 testCases := []struct { 241 name string 242 answers []string 243 244 sendOnlyOptOutData bool 245 wantWriteAndUpload bool 246 }{ 247 {"yes", []string{"yes"}, false, true}, 248 {"y", []string{"y"}, false, true}, 249 {"YES", []string{"YES"}, false, true}, 250 {"Y", []string{"Y"}, false, true}, 251 {"no", []string{"no"}, true, true}, 252 {"n", []string{"n"}, true, true}, 253 {"NO", []string{"NO"}, true, true}, 254 {"n", []string{"N"}, true, true}, 255 {"quit", []string{"quit"}, false, false}, 256 {"q", []string{"q"}, false, false}, 257 {"QUIT", []string{"QUIT"}, false, false}, 258 {"Q", []string{"Q"}, false, false}, 259 {"default-quit", []string{""}, false, false}, 260 {"garbage-then-quit", []string{"garbage", "yesgarbage", "nogarbage", "quitgarbage", "Q"}, false, false}, 261 {"ctrl-c-input", []string{"CTRL-C"}, false, false}, 262 } 263 for _, tc := range testCases { 264 tc := tc // capture range variable for parallel execution 265 t.Run(tc.name, func(t *testing.T) { 266 a := helper.Asserter{T: t} 267 268 out, tearDown := helper.TempDir(t) 269 defer tearDown() 270 defer helper.ChangeEnv("XDG_CACHE_HOME", out)() 271 out = filepath.Join(out, "ubuntu-report") 272 // we don't really care where we hit for this API integration test, internal ones test it 273 // and we don't really control /etc/os-release version and id. 274 // Same for report file 275 serverHit := false 276 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 277 fmt.Println("HIT") 278 serverHit = true 279 })) 280 defer ts.Close() 281 282 stdout, tearDown := helper.CaptureStdout(t) 283 defer tearDown() 284 stdin, tearDown := helper.CaptureStdin(t) 285 defer tearDown() 286 287 cmdErrs := helper.RunFunctionWithTimeout(t, func() error { 288 url := C.CString(ts.URL) 289 defer C.free(unsafe.Pointer(url)) 290 291 errstr := C.sysmetrics_collect_and_send(C.sysmetrics_report_type(sysmetrics.ReportInteractive), C.uchar(0), url) 292 defer C.free(unsafe.Pointer(errstr)) 293 var err error 294 if errstr != nil { 295 err = errors.New(C.GoString(errstr)) 296 } 297 return err 298 }) 299 300 gotJSONReport := false 301 answerIndex := 0 302 scanner := bufio.NewScanner(stdout) 303 scanner.Split(scanLinesOrQuestion) 304 for scanner.Scan() { 305 txt := scanner.Text() 306 // first, we should have a known element 307 if strings.Contains(txt, expectedReportItem) { 308 gotJSONReport = true 309 } 310 if !strings.Contains(txt, "Do you agree to report this?") { 311 continue 312 } 313 a := tc.answers[answerIndex] 314 if a == "CTRL-C" { 315 stdin.Close() 316 break 317 } else { 318 stdin.Write([]byte(tc.answers[answerIndex] + "\n")) 319 } 320 answerIndex = answerIndex + 1 321 // all answers have be provided 322 if answerIndex >= len(tc.answers) { 323 stdin.Close() 324 break 325 } 326 } 327 328 if err := <-cmdErrs; err != nil { 329 t.Fatal("didn't expect to get an error, got:", err) 330 } 331 a.Equal(gotJSONReport, true) 332 a.Equal(serverHit, tc.wantWriteAndUpload) 333 334 if !tc.wantWriteAndUpload { 335 if _, err := os.Stat(filepath.Join(out, "ubuntu-report")); err == nil || (err != nil && !os.IsNotExist(err)) { 336 t.Fatal("we didn't want to get a report but we got one") 337 } 338 return 339 } 340 p := filepath.Join(out, helper.FindInDirectory(t, "", out)) 341 data, err := ioutil.ReadFile(p) 342 if err != nil { 343 t.Fatalf("couldn't open report file %s", out) 344 } 345 d := string(data) 346 expected := expectedReportItem 347 if tc.sendOnlyOptOutData { 348 expected = optOutJSON 349 } 350 if !strings.Contains(d, expected) { 351 t.Errorf("we expected to find %s in report file, got: %s", expected, d) 352 } 353 }) 354 } 355 } 356 357 // scanLinesOrQuestion is copy of ScanLines, adding the expected question string as we don't return here 358 func scanLinesOrQuestion(data []byte, atEOF bool) (advance int, token []byte, err error) { 359 if atEOF && len(data) == 0 { 360 return 0, nil, nil 361 } 362 if i := bytes.IndexByte(data, '\n'); i >= 0 { 363 // We have a full newline-terminated line. 364 return i + 1, dropCR(data[0:i]), nil 365 } 366 if i := bytes.IndexByte(data, ']'); i >= 0 { 367 // We have a full newline-terminated line. 368 return i + 1, dropCR(data[0:i]), nil 369 } 370 // If we're at EOF, we have a final, non-terminated line. Return it. 371 if atEOF { 372 return len(data), dropCR(data), nil 373 } 374 // Request more data. 375 return 0, nil, nil 376 } 377 378 // dropCR drops a terminal \r from the data. 379 func dropCR(data []byte) []byte { 380 if len(data) > 0 && data[len(data)-1] == '\r' { 381 return data[0 : len(data)-1] 382 } 383 return data 384 }