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  }