github.com/intel/goresctrl@v0.5.0/pkg/blockio/blockio_test.go (about)

     1  // Copyright 2019-2021 Intel Corporation. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package blockio
    16  
    17  import (
    18  	"fmt"
    19  	"os"
    20  	"path/filepath"
    21  	"strings"
    22  	"testing"
    23  
    24  	"github.com/intel/goresctrl/pkg/cgroups"
    25  	"github.com/intel/goresctrl/pkg/testutils"
    26  )
    27  
    28  var knownIOSchedulers map[string]bool = map[string]bool{
    29  	"bfq":         true,
    30  	"cfq":         true,
    31  	"deadline":    true,
    32  	"kyber":       true,
    33  	"mq-deadline": true,
    34  	"none":        true,
    35  	"noop":        true,
    36  }
    37  
    38  // TestSetConfig: unit tests for SetConfigFromFile(), SetConfigFromData(), and SetConfig().
    39  func TestSetConfig(t *testing.T) {
    40  	initialConf := map[string]cgroups.BlockIOParameters{
    41  		"classname": cgroups.BlockIOParameters{},
    42  	}
    43  	emptyConf := map[string]cgroups.BlockIOParameters{}
    44  	goodConf := map[string]cgroups.BlockIOParameters{
    45  		"goodclass": cgroups.NewBlockIOParameters(),
    46  	}
    47  	classBlockIO = copyConf(initialConf)
    48  
    49  	err := SetConfigFromFile("/blockio-test/non-existent-file", true)
    50  	testutils.VerifyError(t, err, 1, []string{"/blockio-test/non-existent-file", "failed to read"})
    51  	testutils.VerifyDeepEqual(t, "effective configuration 1", initialConf, classBlockIO)
    52  
    53  	badConfFile := testutils.CreateTempFile(t, "bad config contents.\n")
    54  	emptyConfFile := testutils.CreateTempFile(t, "")
    55  	goodConfFile := testutils.CreateTempFile(t, "Classes:\n  goodclass:\n")
    56  	defer os.Remove(badConfFile)
    57  	defer os.Remove(emptyConfFile)
    58  	defer os.Remove(goodConfFile)
    59  
    60  	for syntaxerror := 0; syntaxerror < 4; syntaxerror++ {
    61  		classBlockIO, err = copyConf(initialConf), nil
    62  		switch syntaxerror {
    63  		case 0:
    64  			err = SetConfigFromFile(badConfFile, false)
    65  		case 1:
    66  			err = SetConfigFromFile(badConfFile, true)
    67  		case 2:
    68  			err = SetConfigFromData([]byte("bad config."), false)
    69  		case 3:
    70  			err = SetConfigFromData([]byte("bad config."), true)
    71  		}
    72  		if syntaxerror < 2 {
    73  			testutils.VerifyError(t, err, 1, []string{badConfFile})
    74  		}
    75  		testutils.VerifyError(t, err, 1, []string{"error unmarshaling"})
    76  		testutils.VerifyDeepEqual(t,
    77  			fmt.Sprintf("syntax error configuration %d", syntaxerror),
    78  			initialConf, classBlockIO)
    79  	}
    80  
    81  	// Test valid ways to clear (reset) all classes
    82  	for clear := 0; clear < 8; clear++ {
    83  		classBlockIO, err = copyConf(initialConf), nil
    84  		switch clear {
    85  		case 0:
    86  			err = SetConfigFromFile(emptyConfFile, false)
    87  		case 1:
    88  			err = SetConfigFromFile(emptyConfFile, true)
    89  		case 2:
    90  			err = SetConfigFromData([]byte(""), false)
    91  		case 3:
    92  			err = SetConfigFromData([]byte(""), true)
    93  		case 4:
    94  			err = SetConfig(nil, false)
    95  		case 5:
    96  			err = SetConfig(nil, true)
    97  		case 6:
    98  			err = SetConfig(&Config{}, false)
    99  		case 7:
   100  			err = SetConfig(&Config{}, true)
   101  		}
   102  		testutils.VerifyNoError(t, err)
   103  		testutils.VerifyDeepEqual(t,
   104  			fmt.Sprintf("clear conf %d", clear),
   105  			emptyConf, classBlockIO)
   106  	}
   107  
   108  	err = SetConfigFromFile(goodConfFile, true)
   109  	testutils.VerifyNoError(t, err)
   110  	testutils.VerifyDeepEqual(t, "ok conf", goodConf, classBlockIO)
   111  }
   112  
   113  // copyConf returns a shallow copy of blockio class configuration.
   114  func copyConf(orig map[string]cgroups.BlockIOParameters) map[string]cgroups.BlockIOParameters {
   115  	result := map[string]cgroups.BlockIOParameters{}
   116  	for key, value := range orig {
   117  		result[key] = value
   118  	}
   119  	return result
   120  }
   121  
   122  func TestClassNames(t *testing.T) {
   123  	classBlockIO = map[string]cgroups.BlockIOParameters{
   124  		"a": cgroups.BlockIOParameters{},
   125  		"z": cgroups.BlockIOParameters{},
   126  		"b": cgroups.BlockIOParameters{},
   127  		"x": cgroups.BlockIOParameters{},
   128  		"c": cgroups.BlockIOParameters{},
   129  		"d": cgroups.BlockIOParameters{},
   130  	}
   131  	classes := GetClasses()
   132  	testutils.VerifyStringSlices(t,
   133  		[]string{"a", "b", "c", "d", "x", "z"},
   134  		classes)
   135  	classBlockIO = map[string]cgroups.BlockIOParameters{}
   136  	classes = GetClasses()
   137  	testutils.VerifyStringSlices(t,
   138  		[]string{},
   139  		classes)
   140  }
   141  
   142  // TestGetCurrentIOSchedulers: unit test for getCurrentIOSchedulers().
   143  func TestGetCurrentIOSchedulers(t *testing.T) {
   144  	currentIOSchedulers, err := getCurrentIOSchedulers()
   145  	testutils.VerifyError(t, err, 0, nil)
   146  	for blockDev, ioScheduler := range currentIOSchedulers {
   147  		s, ok := knownIOSchedulers[ioScheduler]
   148  		if !ok || !s {
   149  			t.Errorf("unknown io scheduler %#v on block device %#v", ioScheduler, blockDev)
   150  		}
   151  	}
   152  }
   153  
   154  // TestConfigurableBlockDevices: unit tests for configurableBlockDevices().
   155  func TestConfigurableBlockDevices(t *testing.T) {
   156  	sysfsBlockDevs, err := filepath.Glob("/sys/block/*")
   157  	if err != nil {
   158  		sysfsBlockDevs = []string{}
   159  	}
   160  	devBlockDevs := []string{}
   161  	for _, sysfsBlockDev := range sysfsBlockDevs {
   162  		if strings.HasPrefix(sysfsBlockDev, "/sys/block/sd") || strings.HasPrefix(sysfsBlockDev, "/sys/block/vd") {
   163  			devBlockDevs = append(devBlockDevs, strings.Replace(sysfsBlockDev, "/sys/block/", "/dev/", 1))
   164  		}
   165  	}
   166  	t.Logf("test real block devices: %v", devBlockDevs)
   167  	tcases := []struct {
   168  		name                    string
   169  		devWildcards            []string
   170  		expectedErrorCount      int
   171  		expectedErrorSubstrings []string
   172  		expectedMatches         int
   173  		disabled                bool
   174  		disabledReason          string
   175  	}{
   176  		{
   177  			name:               "no device wildcards",
   178  			devWildcards:       nil,
   179  			expectedErrorCount: 0,
   180  		},
   181  		{
   182  			name:                    "bad wildcard",
   183  			devWildcards:            []string{"/[-/verybadwildcard]"},
   184  			expectedErrorCount:      1,
   185  			expectedErrorSubstrings: []string{"verybadwildcard", "syntax error"},
   186  		},
   187  		{
   188  			name:                    "not matching wildcard",
   189  			devWildcards:            []string{"/dev/path that should not exist/*"},
   190  			expectedErrorCount:      1,
   191  			expectedErrorSubstrings: []string{"does not match any"},
   192  		},
   193  		{
   194  			name:                    "two wildcards: empty string and a character device",
   195  			devWildcards:            []string{"/dev/null", ""},
   196  			expectedErrorCount:      2,
   197  			expectedErrorSubstrings: []string{"\"/dev/null\" is a character device", "\"\" does not match any"},
   198  		},
   199  		{
   200  			name:                    "not a device or even a file",
   201  			devWildcards:            []string{"/proc", "/proc/meminfo", "/proc/notexistingfile"},
   202  			expectedErrorCount:      3,
   203  			expectedErrorSubstrings: []string{"\"/proc\" is not a device", "\"/proc/meminfo\" is not a device"},
   204  		},
   205  		{
   206  			name:            "real block devices",
   207  			devWildcards:    devBlockDevs,
   208  			expectedMatches: len(devBlockDevs),
   209  		},
   210  	}
   211  	for _, tc := range tcases {
   212  		t.Run(tc.name, func(t *testing.T) {
   213  			if tc.disabled {
   214  				t.Skip(tc.disabledReason)
   215  			}
   216  			realPlatform := defaultPlatform{}
   217  			bdis, err := realPlatform.configurableBlockDevices(tc.devWildcards)
   218  			testutils.VerifyError(t, err, tc.expectedErrorCount, tc.expectedErrorSubstrings)
   219  			if len(bdis) != tc.expectedMatches {
   220  				t.Errorf("expected %d matching block devices, got %d", tc.expectedMatches, len(bdis))
   221  			}
   222  		})
   223  	}
   224  }
   225  
   226  // TestDevicesParametersToCgBlockIO: unit tests for devicesParametersToCgBlockIO().
   227  func TestDevicesParametersToCgBlockIO(t *testing.T) {
   228  	// switch real devicesParametersToCgBlockIO to call mockPlatform.configurableBlockDevices
   229  	currentPlatform = mockPlatform{}
   230  	tcases := []struct {
   231  		name                    string
   232  		dps                     []DevicesParameters
   233  		iosched                 map[string]string
   234  		expectedOci             *cgroups.BlockIOParameters
   235  		expectedErrorCount      int
   236  		expectedErrorSubstrings []string
   237  	}{
   238  		{
   239  			name: "all OCI fields",
   240  			dps: []DevicesParameters{
   241  				{
   242  					Weight: "144",
   243  				},
   244  				{
   245  					Devices:           []string{"/dev/sda"},
   246  					ThrottleReadBps:   "1G",
   247  					ThrottleWriteBps:  "2M",
   248  					ThrottleReadIOPS:  "3k",
   249  					ThrottleWriteIOPS: "4",
   250  					Weight:            "50",
   251  				},
   252  			},
   253  			iosched: map[string]string{"/dev/sda": "bfq"},
   254  			expectedOci: &cgroups.BlockIOParameters{
   255  				Weight: 144,
   256  				WeightDevice: cgroups.DeviceWeights{
   257  					{Major: 11, Minor: 12, Weight: 50},
   258  				},
   259  				ThrottleReadBpsDevice: cgroups.DeviceRates{
   260  					{Major: 11, Minor: 12, Rate: 1000000000},
   261  				},
   262  				ThrottleWriteBpsDevice: cgroups.DeviceRates{
   263  					{Major: 11, Minor: 12, Rate: 2000000},
   264  				},
   265  				ThrottleReadIOPSDevice: cgroups.DeviceRates{
   266  					{Major: 11, Minor: 12, Rate: 3000},
   267  				},
   268  				ThrottleWriteIOPSDevice: cgroups.DeviceRates{
   269  					{Major: 11, Minor: 12, Rate: 4},
   270  				},
   271  			},
   272  		},
   273  		{
   274  			name: "later match overrides value",
   275  			dps: []DevicesParameters{
   276  				{
   277  					Devices:         []string{"/dev/sda", "/dev/sdb", "/dev/sdc"},
   278  					ThrottleReadBps: "100",
   279  					Weight:          "110",
   280  				},
   281  				{
   282  					Devices:         []string{"/dev/sdb", "/dev/sdc"},
   283  					ThrottleReadBps: "300",
   284  					Weight:          "330",
   285  				},
   286  				{
   287  					Devices:         []string{"/dev/sdb"},
   288  					ThrottleReadBps: "200",
   289  					Weight:          "220",
   290  				},
   291  			},
   292  			iosched: map[string]string{"/dev/sda": "bfq", "/dev/sdb": "bfq", "/dev/sdc": "cfq"},
   293  			expectedOci: &cgroups.BlockIOParameters{
   294  				Weight: -1,
   295  				WeightDevice: cgroups.DeviceWeights{
   296  					{Major: 11, Minor: 12, Weight: 110},
   297  					{Major: 21, Minor: 22, Weight: 220},
   298  					{Major: 31, Minor: 32, Weight: 330},
   299  				},
   300  				ThrottleReadBpsDevice: cgroups.DeviceRates{
   301  					{Major: 11, Minor: 12, Rate: 100},
   302  					{Major: 21, Minor: 22, Rate: 200},
   303  					{Major: 31, Minor: 32, Rate: 300},
   304  				},
   305  			},
   306  		},
   307  		{
   308  			name: "invalid weights, many errors in different parameter sets",
   309  			dps: []DevicesParameters{
   310  				{
   311  					Weight: "99999",
   312  				},
   313  				{
   314  					Devices: []string{"/dev/sda"},
   315  					Weight:  "1",
   316  				},
   317  				{
   318  					Devices: []string{"/dev/sdb"},
   319  					Weight:  "-2",
   320  				},
   321  			},
   322  			expectedErrorCount: 3,
   323  			expectedErrorSubstrings: []string{
   324  				"(99999) bigger than maximum",
   325  				"(1) smaller than minimum",
   326  				"(-2) smaller than minimum",
   327  			},
   328  		},
   329  		{
   330  			name: "throttling without listing Devices",
   331  			dps: []DevicesParameters{
   332  				{
   333  					ThrottleReadBps:   "100M",
   334  					ThrottleWriteIOPS: "20k",
   335  				},
   336  			},
   337  			expectedErrorCount: 1,
   338  			expectedErrorSubstrings: []string{
   339  				"Devices not listed",
   340  				"\"100M\"",
   341  				"\"20k\"",
   342  			},
   343  		},
   344  	}
   345  	for _, tc := range tcases {
   346  		t.Run(tc.name, func(t *testing.T) {
   347  			oci, err := devicesParametersToCgBlockIO(tc.dps, tc.iosched)
   348  			testutils.VerifyError(t, err, tc.expectedErrorCount, tc.expectedErrorSubstrings)
   349  			if tc.expectedOci != nil {
   350  				testutils.VerifyDeepEqual(t, "OCI parameters", *tc.expectedOci, oci)
   351  			}
   352  		})
   353  	}
   354  }
   355  
   356  // mockPlatform implements mock versions of platformInterface functions.
   357  type mockPlatform struct{}
   358  
   359  // configurableBlockDevices mock always returns a set of block devices.
   360  func (mpf mockPlatform) configurableBlockDevices(devWildcards []string) ([]tBlockDeviceInfo, error) {
   361  	blockDevices := []tBlockDeviceInfo{}
   362  	for _, devWildcard := range devWildcards {
   363  		if devWildcard == "/dev/sda" {
   364  			blockDevices = append(blockDevices, tBlockDeviceInfo{
   365  				Major:   11,
   366  				Minor:   12,
   367  				DevNode: devWildcard,
   368  				Origin:  fmt.Sprintf("from wildcards %v", devWildcard),
   369  			})
   370  		} else if devWildcard == "/dev/sdb" {
   371  			blockDevices = append(blockDevices, tBlockDeviceInfo{
   372  				Major:   21,
   373  				Minor:   22,
   374  				DevNode: devWildcard,
   375  				Origin:  fmt.Sprintf("from wildcards %v", devWildcard),
   376  			})
   377  		} else if devWildcard == "/dev/sdc" {
   378  			blockDevices = append(blockDevices, tBlockDeviceInfo{
   379  				Major:   31,
   380  				Minor:   32,
   381  				DevNode: devWildcard,
   382  				Origin:  fmt.Sprintf("from wildcards %v", devWildcard),
   383  			})
   384  		}
   385  	}
   386  	return blockDevices, nil
   387  }