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  }