github.com/ubuntu/ubuntu-report@v1.7.4-0.20240410144652-96f37d845fac/internal/metrics/internals_test.go (about)

     1  package metrics
     2  
     3  import (
     4  	"context"
     5  	"flag"
     6  	"os/exec"
     7  	"path"
     8  	"path/filepath"
     9  	"testing"
    10  
    11  	"github.com/ubuntu/ubuntu-report/internal/helper"
    12  )
    13  
    14  /*
    15   * Tests here some private functions to gather metrics
    16   * Collect() public API is calling out a lot of functions,
    17   * that's why we add some unit tests on direct Collect() callees here
    18   * for finer-graind results in case of failure.
    19   */
    20  
    21  var Update = flag.Bool("update", false, "update golden files")
    22  
    23  func TestInstallerInfo(t *testing.T) {
    24  	t.Parallel()
    25  
    26  	testCases := []struct {
    27  		name string
    28  		root string
    29  	}{
    30  		{"regular", "testdata/good"},
    31  		{"empty file", "testdata/empty"},
    32  		{"doesn't exist", "testdata/none"},
    33  		{"garbage content", "testdata/garbage"},
    34  	}
    35  	for _, tc := range testCases {
    36  		tc := tc // capture range variable for parallel execution
    37  		t.Run(tc.name, func(t *testing.T) {
    38  			t.Parallel()
    39  			a := helper.Asserter{T: t}
    40  
    41  			m := newTestMetrics(t, WithRootAt(tc.root))
    42  			got := []byte(m.installerInfo())
    43  			want := helper.LoadOrUpdateGolden(t, path.Join(m.root, "gold", "intallerInfo"), got, *Update)
    44  
    45  			a.Equal(got, want)
    46  		})
    47  	}
    48  }
    49  
    50  func TestUpgradeInfo(t *testing.T) {
    51  	t.Parallel()
    52  
    53  	testCases := []struct {
    54  		name string
    55  		root string
    56  	}{
    57  		{"regular", "testdata/good"},
    58  		{"empty file", "testdata/empty"},
    59  		{"doesn't exist", "testdata/none"},
    60  		{"garbage content", "testdata/garbage"},
    61  	}
    62  	for _, tc := range testCases {
    63  		tc := tc // capture range variable for parallel execution
    64  		t.Run(tc.name, func(t *testing.T) {
    65  			t.Parallel()
    66  			a := helper.Asserter{T: t}
    67  
    68  			m := newTestMetrics(t, WithRootAt(tc.root))
    69  			got := []byte(m.upgradeInfo())
    70  			want := helper.LoadOrUpdateGolden(t, filepath.Join(m.root, "gold", "upgradeInfo"), got, *Update)
    71  
    72  			a.Equal(got, want)
    73  		})
    74  	}
    75  }
    76  
    77  func TestGetVersion(t *testing.T) {
    78  	t.Parallel()
    79  
    80  	testCases := []struct {
    81  		name string
    82  		root string
    83  
    84  		want string
    85  	}{
    86  		{"regular", "testdata/good", "18.04"},
    87  		{"empty file", "testdata/empty", ""},
    88  		{"missing", "testdata/missing-fields/ids/version", ""},
    89  		{"empty", "testdata/empty-fields/ids/version", ""},
    90  		{"doesn't exist", "testdata/none", ""},
    91  		{"garbage content", "testdata/garbage", ""},
    92  	}
    93  	for _, tc := range testCases {
    94  		tc := tc // capture range variable for parallel execution
    95  		t.Run(tc.name, func(t *testing.T) {
    96  			t.Parallel()
    97  			a := helper.Asserter{T: t}
    98  
    99  			m := newTestMetrics(t, WithRootAt(tc.root))
   100  			got := m.getVersion()
   101  
   102  			a.Equal(got, tc.want)
   103  		})
   104  	}
   105  }
   106  
   107  func TestGetRAM(t *testing.T) {
   108  	t.Parallel()
   109  
   110  	normalRAM := 8.0
   111  	testCases := []struct {
   112  		name string
   113  		root string
   114  
   115  		want *float64
   116  	}{
   117  		{"regular", "testdata/good", &normalRAM},
   118  		{"empty file", "testdata/empty", nil},
   119  		{"missing", "testdata/missing-fields/ram", nil},
   120  		{"empty", "testdata/empty-fields/ram", nil},
   121  		{"malformed", "testdata/specials/ram/malformed", nil},
   122  		{"doesn't exist", "testdata/none", nil},
   123  		{"garbage content", "testdata/garbage", nil},
   124  	}
   125  	for _, tc := range testCases {
   126  		tc := tc // capture range variable for parallel execution
   127  		t.Run(tc.name, func(t *testing.T) {
   128  			t.Parallel()
   129  			a := helper.Asserter{T: t}
   130  
   131  			m := newTestMetrics(t, WithRootAt(tc.root))
   132  			got := m.getRAM()
   133  
   134  			a.Equal(got, tc.want)
   135  		})
   136  	}
   137  }
   138  
   139  func TestGetTimeZone(t *testing.T) {
   140  	t.Parallel()
   141  
   142  	testCases := []struct {
   143  		name string
   144  		root string
   145  
   146  		want string
   147  	}{
   148  		{"regular", "testdata/good", "Europe/Paris"},
   149  		{"empty file", "testdata/empty", ""},
   150  		{"doesn't exist", "testdata/none", ""},
   151  		{"garbage content", "testdata/garbage", ""},
   152  	}
   153  	for _, tc := range testCases {
   154  		tc := tc // capture range variable for parallel execution
   155  		t.Run(tc.name, func(t *testing.T) {
   156  			t.Parallel()
   157  			a := helper.Asserter{T: t}
   158  
   159  			m := newTestMetrics(t, WithRootAt(tc.root))
   160  			got := m.getTimeZone()
   161  
   162  			a.Equal(got, tc.want)
   163  		})
   164  	}
   165  }
   166  
   167  func TestGetAutologin(t *testing.T) {
   168  	t.Parallel()
   169  
   170  	testCases := []struct {
   171  		name string
   172  		root string
   173  
   174  		want bool
   175  	}{
   176  		{"regular", "testdata/good", false},
   177  		{"empty file", "testdata/empty", false},
   178  		{"missing", "testdata/missing-fields/autologin", false},
   179  		{"empty", "testdata/empty-fields/autologin", false},
   180  		{"enabled", "testdata/specials/autologin/true", true},
   181  		{"disabled", "testdata/specials/autologin/false", false},
   182  		{"enabled no space", "testdata/specials/autologin/true-no-space", true},
   183  		{"uppercase", "testdata/specials/autologin/true-uppercase", true},
   184  		{"doesn't exist", "testdata/none", false},
   185  		{"garbage content", "testdata/garbage", 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  			t.Parallel()
   191  			a := helper.Asserter{T: t}
   192  
   193  			m := newTestMetrics(t, WithRootAt(tc.root))
   194  			got := m.getAutologin()
   195  
   196  			a.Equal(got, tc.want)
   197  		})
   198  	}
   199  }
   200  
   201  func TestGetOEM(t *testing.T) {
   202  	t.Parallel()
   203  
   204  	testCases := []struct {
   205  		name string
   206  		root string
   207  
   208  		wantVendor  string
   209  		wantProduct string
   210  		wantFamily  string
   211  		wantDCD     string
   212  	}{
   213  		{"regular", "testdata/good", "DID", "4287CTO", "Thinkpad", ""},
   214  		{"with dcd", "testdata/specials/oem/with-dcd", "", "", "", "canonical-oem-somerville-xenial-amd64-20160624-2"},
   215  		{"empty vendor", "testdata/empty-fields/oem/vendor", "", "4287CTO", "Thinkpad", ""},
   216  		{"empty product", "testdata/empty-fields/oem/product", "DID", "", "Thinkpad", ""},
   217  		{"empty family", "testdata/empty-fields/oem/family", "DID", "4287CTO", "", ""},
   218  		{"empty dcd", "testdata/empty-fields/oem/dcd", "DID", "4287CTO", "Thinkpad", ""},
   219  		{"empty both", "testdata/empty", "", "", "", ""},
   220  		{"doesn't exist", "testdata/none", "", "", "", ""},
   221  		{"garbage content", "testdata/garbage", "", "", "", ""},
   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 := newTestMetrics(t, WithRootAt(tc.root))
   230  			vendor, product, family, dcd := m.getOEM()
   231  
   232  			a.Equal(vendor, tc.wantVendor)
   233  			a.Equal(product, tc.wantProduct)
   234  			a.Equal(family, tc.wantFamily)
   235  			a.Equal(dcd, tc.wantDCD)
   236  		})
   237  	}
   238  }
   239  
   240  func TestGetBIOS(t *testing.T) {
   241  	t.Parallel()
   242  
   243  	testCases := []struct {
   244  		name string
   245  		root string
   246  
   247  		wantVendor  string
   248  		wantVersion string
   249  	}{
   250  		{"regular", "testdata/good", "DID", "42 (maybe 43)"},
   251  		{"empty vendor", "testdata/empty-fields/bios/vendor", "", "42 (maybe 43)"},
   252  		{"empty product", "testdata/empty-fields/bios/version", "DID", ""},
   253  		{"empty both", "testdata/empty", "", ""},
   254  		{"doesn't exist", "testdata/none", "", ""},
   255  		{"garbage content", "testdata/garbage", "", ""},
   256  	}
   257  	for _, tc := range testCases {
   258  		tc := tc // capture range variable for parallel execution
   259  		t.Run(tc.name, func(t *testing.T) {
   260  			t.Parallel()
   261  			a := helper.Asserter{T: t}
   262  
   263  			m := newTestMetrics(t, WithRootAt(tc.root))
   264  			vendor, version := m.getBIOS()
   265  
   266  			a.Equal(vendor, tc.wantVendor)
   267  			a.Equal(version, tc.wantVersion)
   268  		})
   269  	}
   270  }
   271  
   272  func TestGetLivePatch(t *testing.T) {
   273  	t.Parallel()
   274  
   275  	testCases := []struct {
   276  		name string
   277  		root string
   278  
   279  		want bool
   280  	}{
   281  		{"regular", "testdata/good", true},
   282  		{"disabled", "testdata/none", false},
   283  	}
   284  	for _, tc := range testCases {
   285  		tc := tc // capture range variable for parallel execution
   286  		t.Run(tc.name, func(t *testing.T) {
   287  			t.Parallel()
   288  			a := helper.Asserter{T: t}
   289  
   290  			m := newTestMetrics(t, WithRootAt(tc.root))
   291  			enabled := m.getLivePatch()
   292  
   293  			a.Equal(enabled, tc.want)
   294  		})
   295  	}
   296  }
   297  
   298  func TestGetDisks(t *testing.T) {
   299  	t.Parallel()
   300  
   301  	testCases := []struct {
   302  		name string
   303  		root string
   304  
   305  		wantSize []float64
   306  	}{
   307  		{"one disk", "testdata/good", []float64{240.1}},
   308  		{"multiple disks", "testdata/specials/disks/multiple", []float64{240.1, 500.1}},
   309  		{"all supported formats", "testdata/specials/disks/all-formats", []float64{500.1, 240.1, 192.9}},
   310  		{"no disks", "testdata/specials/disks/no-disks", nil},
   311  		{"filters undesired devices", "testdata/specials/disks/filter-devices", []float64{240.1}},
   312  		{"no block numbers", "testdata/empty-fields/disks/block-numbers", nil},
   313  		{"no block logical size", "testdata/empty-fields/disks/block-logical-size", nil},
   314  		{"none", "testdata/none", nil},
   315  		{"garbage block numbers", "testdata/specials/disks/garbage-block-numbers", nil},
   316  		{"garbage block size", "testdata/specials/disks/garbage-block-size", nil},
   317  	}
   318  	for _, tc := range testCases {
   319  		tc := tc // capture range variable for parallel execution
   320  		t.Run(tc.name, func(t *testing.T) {
   321  			t.Parallel()
   322  			a := helper.Asserter{T: t}
   323  
   324  			m := newTestMetrics(t, WithRootAt(tc.root))
   325  			disks := m.getDisks()
   326  
   327  			a.Equal(disks, tc.wantSize)
   328  		})
   329  	}
   330  }
   331  func TestGetCPU(t *testing.T) {
   332  	t.Parallel()
   333  
   334  	testCases := []struct {
   335  		name string
   336  
   337  		want cpuInfo
   338  	}{
   339  		{"regular", cpuInfo{"32-bit, 64-bit", "8", "2", "4", "1", "Genuine", "6", "158", "10",
   340  			"Intuis Corus i5-8300H CPU @ 2.30GHz", "VT-x", "", ""}},
   341  		{"missing one expected field", cpuInfo{"32-bit, 64-bit", "8", "2", "4", "1", "", "6", "158", "10",
   342  			"Intuis Corus i5-8300H CPU @ 2.30GHz", "VT-x", "", ""}},
   343  		{"missing one optional field", cpuInfo{"32-bit, 64-bit", "8", "2", "4", "1", "Genuine", "6", "158", "10",
   344  			"Intuis Corus i5-8300H CPU @ 2.30GHz", "VT-x", "", ""}},
   345  		{"virtualized", cpuInfo{"32-bit, 64-bit", "8", "2", "4", "1", "Genuine", "6", "158", "10",
   346  			"Intuis Corus i5-8300H CPU @ 2.30GHz", "VT-x", "KVM", "full"}},
   347  		{"without space", cpuInfo{"32-bit, 64-bit", "8", "2", "4", "1", "Genuine", "6", "158", "10",
   348  			"Intuis Corus i5-8300H CPU @ 2.30GHz", "VT-x", "", ""}},
   349  		{"empty", cpuInfo{}},
   350  		{"garbage", cpuInfo{}},
   351  		{"fail", cpuInfo{}},
   352  	}
   353  	for _, tc := range testCases {
   354  		tc := tc // capture range variable for parallel execution
   355  		t.Run(tc.name, func(t *testing.T) {
   356  			t.Parallel()
   357  			a := helper.Asserter{T: t}
   358  
   359  			cmd, cancel := newMockShortCmd(t, "lscpu", "-J", tc.name)
   360  			defer cancel()
   361  
   362  			m := newTestMetrics(t, WithCPUInfoCommand(cmd))
   363  			info := m.getCPU()
   364  
   365  			a.Equal(info, tc.want)
   366  		})
   367  	}
   368  }
   369  
   370  func TestGetGPU(t *testing.T) {
   371  	t.Parallel()
   372  
   373  	testCases := []struct {
   374  		name string
   375  
   376  		want []gpuInfo
   377  	}{
   378  		{"one gpu", []gpuInfo{{"8086", "0126"}}},
   379  		{"multiple gpus", []gpuInfo{{"8086", "0126"}, {"8086", "0127"}}},
   380  		{"no revision number", []gpuInfo{{"8086", "0126"}}},
   381  		{"no gpu", nil},
   382  		{"hexa numbers", []gpuInfo{{"8b86", "a126"}}},
   383  		{"empty", nil},
   384  		{"malformed gpu line", nil},
   385  		{"garbage", nil},
   386  		{"fail", nil},
   387  	}
   388  	for _, tc := range testCases {
   389  		tc := tc // capture range variable for parallel execution
   390  		t.Run(tc.name, func(t *testing.T) {
   391  			t.Parallel()
   392  			a := helper.Asserter{T: t}
   393  
   394  			cmd, cancel := newMockShortCmd(t, "lspci", "-n", tc.name)
   395  			defer cancel()
   396  
   397  			m := newTestMetrics(t, WithGPUInfoCommand(cmd))
   398  			info := m.getGPU()
   399  
   400  			a.Equal(info, tc.want)
   401  		})
   402  	}
   403  }
   404  
   405  func TestGetScreens(t *testing.T) {
   406  	t.Parallel()
   407  
   408  	testCases := []struct {
   409  		name string
   410  
   411  		want []screenInfo
   412  	}{
   413  		{"one screen", []screenInfo{{"277mmx156mm", "1366x768", "60.02"}}},
   414  		{"multiple screens", []screenInfo{{"277mmx156mm", "1366x768", "60.02"}, {"510mmx287mm", "1920x1080", "60.00"}}},
   415  		{"no screen", nil},
   416  		{"chosen resolution not first", []screenInfo{{"510mmx287mm", "1600x1200", "60.00"}}},
   417  		{"no specified screen size", nil},
   418  		{"no chosen resolution", nil},
   419  		{"chosen resolution not preferred", []screenInfo{{"510mmx287mm", "1920x1080", "60.00"}}},
   420  		{"multiple frequencies for resolution", []screenInfo{{"510mmx287mm", "1920x1080", "60.00"}}},
   421  		{"multiple frequencies select other resolution", []screenInfo{{"510mmx287mm", "1920x1080", "50.00"}}},
   422  		{"multiple frequencies select other resolution on non preferred", []screenInfo{{"510mmx287mm", "1920x1080", "50.00"}}},
   423  		{"empty", nil},
   424  		{"malformed screen line", nil},
   425  		{"garbage", nil},
   426  		{"fail", nil},
   427  	}
   428  	for _, tc := range testCases {
   429  		tc := tc // capture range variable for parallel execution
   430  		t.Run(tc.name, func(t *testing.T) {
   431  			t.Parallel()
   432  			a := helper.Asserter{T: t}
   433  
   434  			cmd, cancel := newMockShortCmd(t, "xrandr", tc.name)
   435  			defer cancel()
   436  
   437  			m := newTestMetrics(t, WithScreenInfoCommand(cmd))
   438  			info := m.getScreens()
   439  
   440  			a.Equal(info, tc.want)
   441  		})
   442  	}
   443  }
   444  
   445  func TestGetPartitions(t *testing.T) {
   446  	t.Parallel()
   447  
   448  	testCases := []struct {
   449  		name string
   450  
   451  		want []float64
   452  	}{
   453  		{"one partition", []float64{159.4}},
   454  		{"multiple partitions", []float64{159.4, 309.7}},
   455  		{"no partitions", nil},
   456  		{"filters loop devices", []float64{159.4}},
   457  		{"empty", nil},
   458  		{"malformed partition line string", nil},
   459  		{"malformed partition line one field", nil},
   460  		{"garbage", nil},
   461  		{"fail", nil},
   462  	}
   463  	for _, tc := range testCases {
   464  		tc := tc // capture range variable for parallel execution
   465  		t.Run(tc.name, func(t *testing.T) {
   466  			t.Parallel()
   467  			a := helper.Asserter{T: t}
   468  
   469  			cmd, cancel := newMockShortCmd(t, "df", tc.name)
   470  			defer cancel()
   471  
   472  			m := newTestMetrics(t, WithSpaceInfoCommand(cmd))
   473  			info := m.getPartitions()
   474  
   475  			a.Equal(info, tc.want)
   476  		})
   477  	}
   478  }
   479  
   480  func TestGetArch(t *testing.T) {
   481  	t.Parallel()
   482  
   483  	testCases := []struct {
   484  		name string
   485  
   486  		want string
   487  	}{
   488  		{"regular", "amd64"},
   489  		{"empty", ""},
   490  		{"fail", ""},
   491  	}
   492  	for _, tc := range testCases {
   493  		tc := tc // capture range variable for parallel execution
   494  		t.Run(tc.name, func(t *testing.T) {
   495  			t.Parallel()
   496  			a := helper.Asserter{T: t}
   497  
   498  			cmd, cancel := newMockShortCmd(t, "dpkg", "--print-architecture", tc.name)
   499  			defer cancel()
   500  
   501  			m := newTestMetrics(t, WithArchitectureCommand(cmd))
   502  			arch := m.getArch()
   503  
   504  			a.Equal(arch, tc.want)
   505  		})
   506  	}
   507  }
   508  
   509  func TestGetHwCap(t *testing.T) {
   510  	t.Parallel()
   511  
   512  	testCases := []struct {
   513  		name string
   514  
   515  		want string
   516  	}{
   517  		{"regular", "x86-64-v3"},
   518  		{"no hwcap", "-"},
   519  		{"empty", ""},
   520  		{"fail", ""},
   521  	}
   522  	for _, tc := range testCases {
   523  		tc := tc // capture range variable for parallel execution
   524  		t.Run(tc.name, func(t *testing.T) {
   525  			t.Parallel()
   526  			a := helper.Asserter{T: t}
   527  
   528  			hwCapCmd, cancel := newMockShortCmd(t, "/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", "--help", tc.name)
   529  			defer cancel()
   530  
   531  			m := newTestMetrics(t, WithHwCapCommand(hwCapCmd))
   532  			hwCap := m.getHwCap()
   533  
   534  			a.Equal(hwCap, tc.want)
   535  		})
   536  	}
   537  }
   538  
   539  func TestGetLibc6Ver(t *testing.T) {
   540  	t.Parallel()
   541  
   542  	testCases := []struct {
   543  		name string
   544  
   545  		want string
   546  	}{
   547  		{"old version", ""},
   548  		{"not installed", ""},
   549  	}
   550  	for _, tc := range testCases {
   551  		tc := tc // capture range variable for parallel execution
   552  		t.Run(tc.name, func(t *testing.T) {
   553  			t.Parallel()
   554  			a := helper.Asserter{T: t}
   555  
   556  			libc6Cmd, cancel := newMockShortCmd(t, "dpkg", "--status", "libc6", tc.name)
   557  			defer cancel()
   558  
   559  			m := newTestMetrics(t, WithLibc6Command(libc6Cmd))
   560  			hwCap := m.getHwCap()
   561  
   562  			a.Equal(hwCap, tc.want)
   563  		})
   564  	}
   565  }
   566  
   567  func TestGetLanguage(t *testing.T) {
   568  	t.Parallel()
   569  
   570  	testCases := []struct {
   571  		name string
   572  		env  map[string]string
   573  
   574  		want string
   575  	}{
   576  		{"regular", map[string]string{"LANG": "fr_FR.UTF-8", "LANGUAGE": "fr_FR.UTF-8"}, "fr_FR"},
   577  		{"LC_ALL override all", map[string]string{
   578  			"LC_ALL": "en_US.UTF-8", "LANG": "fr_FR.UTF-8", "LANGUAGE": "fr_FR.UTF-8"}, "en_US"},
   579  		{"LANG override LANGUAGE",
   580  			map[string]string{"LANG": "en_US.UTF-8", "LANGUAGE": "fr_FR.UTF-8"}, "en_US"},
   581  		{"LANGUAGE only",
   582  			map[string]string{"LANGUAGE": "fr_FR.UTF-8"}, "fr_FR"},
   583  		{"only first in LANGUAGE list",
   584  			map[string]string{"LANGUAGE": "fr_FR.UTF-8:en_US.UTF8"}, "fr_FR"},
   585  		{"without encoding", map[string]string{"LANG": "fr_FR", "LANGUAGE": "fr_FR"}, "fr_FR"},
   586  		{"none", nil, ""},
   587  	}
   588  	for _, tc := range testCases {
   589  		tc := tc // capture range variable for parallel execution
   590  		t.Run(tc.name, func(t *testing.T) {
   591  			t.Parallel()
   592  			a := helper.Asserter{T: t}
   593  
   594  			m := newTestMetrics(t, WithMapForEnv(tc.env))
   595  			got := m.getLanguage()
   596  
   597  			a.Equal(got, tc.want)
   598  		})
   599  	}
   600  
   601  }
   602  
   603  func newTestMetrics(t *testing.T, fixtures ...func(m *Metrics) error) Metrics {
   604  	t.Helper()
   605  	m, err := New(fixtures...)
   606  	if err != nil {
   607  		t.Fatal("can't create metrics object", err)
   608  	}
   609  	return m
   610  }
   611  
   612  func newMockShortCmd(t *testing.T, s ...string) (*exec.Cmd, context.CancelFunc) {
   613  	t.Helper()
   614  	return helper.ShortProcess(t, "TestMetricsHelperProcess", s...)
   615  }