github.com/sagernet/netlink@v0.0.0-20240612041022-b9a21c07ac6a/class_test.go (about)

     1  //go:build linux
     2  // +build linux
     3  
     4  package netlink
     5  
     6  import (
     7  	"reflect"
     8  	"testing"
     9  )
    10  
    11  func SafeQdiscList(link Link) ([]Qdisc, error) {
    12  	qdiscs, err := QdiscList(link)
    13  	if err != nil {
    14  		return nil, err
    15  	}
    16  	result := []Qdisc{}
    17  	for _, qdisc := range qdiscs {
    18  		// fmt.Printf("%+v\n", qdisc)
    19  		// filter default qdisc created by kernel when custom one deleted
    20  		attrs := qdisc.Attrs()
    21  		if attrs.Handle == HANDLE_NONE && attrs.Parent == HANDLE_ROOT {
    22  			continue
    23  		}
    24  		result = append(result, qdisc)
    25  	}
    26  	return result, nil
    27  }
    28  
    29  func SafeClassList(link Link, handle uint32) ([]Class, error) {
    30  	classes, err := ClassList(link, handle)
    31  	if err != nil {
    32  		return nil, err
    33  	}
    34  	result := []Class{}
    35  	for ind := range classes {
    36  		double := false
    37  		for _, class2 := range classes[ind+1:] {
    38  			if classes[ind].Attrs().Handle == class2.Attrs().Handle {
    39  				double = true
    40  			}
    41  		}
    42  		if !double {
    43  			result = append(result, classes[ind])
    44  		}
    45  	}
    46  	return result, nil
    47  }
    48  
    49  func testClassStats(this, that *ClassStatistics, t *testing.T) {
    50  	ok := reflect.DeepEqual(this, that)
    51  	if !ok {
    52  		t.Fatalf("%#v is expected but it actually was %#v", that, this)
    53  	}
    54  }
    55  
    56  func TestClassAddDel(t *testing.T) {
    57  	tearDown := setUpNetlinkTest(t)
    58  	defer tearDown()
    59  	if err := LinkAdd(&Ifb{LinkAttrs{Name: "foo"}}); err != nil {
    60  		t.Fatal(err)
    61  	}
    62  	if err := LinkAdd(&Ifb{LinkAttrs{Name: "bar"}}); err != nil {
    63  		t.Fatal(err)
    64  	}
    65  	link, err := LinkByName("foo")
    66  	if err != nil {
    67  		t.Fatal(err)
    68  	}
    69  	if err := LinkSetUp(link); err != nil {
    70  		t.Fatal(err)
    71  	}
    72  	attrs := QdiscAttrs{
    73  		LinkIndex: link.Attrs().Index,
    74  		Handle:    MakeHandle(0xffff, 0),
    75  		Parent:    HANDLE_ROOT,
    76  	}
    77  	qdisc := NewHtb(attrs)
    78  	if err := QdiscAdd(qdisc); err != nil {
    79  		t.Fatal(err)
    80  	}
    81  	qdiscs, err := SafeQdiscList(link)
    82  	if err != nil {
    83  		t.Fatal(err)
    84  	}
    85  	if len(qdiscs) != 1 {
    86  		t.Fatal("Failed to add qdisc")
    87  	}
    88  	_, ok := qdiscs[0].(*Htb)
    89  	if !ok {
    90  		t.Fatal("Qdisc is the wrong type")
    91  	}
    92  
    93  	classattrs := ClassAttrs{
    94  		LinkIndex: link.Attrs().Index,
    95  		Parent:    MakeHandle(0xffff, 0),
    96  		Handle:    MakeHandle(0xffff, 2),
    97  	}
    98  
    99  	htbclassattrs := HtbClassAttrs{
   100  		Rate:    1234000,
   101  		Cbuffer: 1690,
   102  		Prio:    2,
   103  		Quantum: 1000,
   104  	}
   105  	class := NewHtbClass(classattrs, htbclassattrs)
   106  	if err := ClassAdd(class); err != nil {
   107  		t.Fatal(err)
   108  	}
   109  	classes, err := SafeClassList(link, MakeHandle(0xffff, 0))
   110  	if err != nil {
   111  		t.Fatal(err)
   112  	}
   113  	if len(classes) != 1 {
   114  		t.Fatal("Failed to add class")
   115  	}
   116  
   117  	htb, ok := classes[0].(*HtbClass)
   118  	if !ok {
   119  		t.Fatal("Class is the wrong type")
   120  	}
   121  	if htb.Rate != class.Rate {
   122  		t.Fatal("Rate doesn't match")
   123  	}
   124  	if htb.Ceil != class.Ceil {
   125  		t.Fatal("Ceil doesn't match")
   126  	}
   127  	if htb.Buffer != class.Buffer {
   128  		t.Fatal("Buffer doesn't match")
   129  	}
   130  	if htb.Cbuffer != class.Cbuffer {
   131  		t.Fatal("Cbuffer doesn't match")
   132  	}
   133  	if htb.Prio != class.Prio {
   134  		t.Fatal("Prio doesn't match")
   135  	}
   136  	if htb.Quantum != class.Quantum {
   137  		t.Fatal("Quantum doesn't match")
   138  	}
   139  
   140  	testClassStats(htb.ClassAttrs.Statistics, NewClassStatistics(), t)
   141  
   142  	qattrs := QdiscAttrs{
   143  		LinkIndex: link.Attrs().Index,
   144  		Handle:    MakeHandle(0x2, 0),
   145  		Parent:    MakeHandle(0xffff, 2),
   146  	}
   147  	nattrs := NetemQdiscAttrs{
   148  		Latency:     20000,
   149  		Loss:        23.4,
   150  		Duplicate:   14.3,
   151  		LossCorr:    8.34,
   152  		Jitter:      1000,
   153  		DelayCorr:   12.3,
   154  		ReorderProb: 23.4,
   155  		CorruptProb: 10.0,
   156  		CorruptCorr: 10,
   157  	}
   158  	qdiscnetem := NewNetem(qattrs, nattrs)
   159  	if err := QdiscAdd(qdiscnetem); err != nil {
   160  		t.Fatal(err)
   161  	}
   162  
   163  	qdiscs, err = SafeQdiscList(link)
   164  	if err != nil {
   165  		t.Fatal(err)
   166  	}
   167  	if len(qdiscs) != 2 {
   168  		t.Fatal("Failed to add qdisc")
   169  	}
   170  	_, ok = qdiscs[0].(*Htb)
   171  	if !ok {
   172  		t.Fatal("Qdisc is the wrong type")
   173  	}
   174  
   175  	netem, ok := qdiscs[1].(*Netem)
   176  	if !ok {
   177  		t.Fatal("Qdisc is the wrong type")
   178  	}
   179  	// Compare the record we got from the list with the one we created
   180  	if netem.Loss != qdiscnetem.Loss {
   181  		t.Fatal("Loss does not match")
   182  	}
   183  	if netem.Latency != qdiscnetem.Latency {
   184  		t.Fatal("Latency does not match")
   185  	}
   186  	if netem.CorruptProb != qdiscnetem.CorruptProb {
   187  		t.Fatal("CorruptProb does not match")
   188  	}
   189  	if netem.Jitter != qdiscnetem.Jitter {
   190  		t.Fatal("Jitter does not match")
   191  	}
   192  	if netem.LossCorr != qdiscnetem.LossCorr {
   193  		t.Fatal("Loss does not match")
   194  	}
   195  	if netem.DuplicateCorr != qdiscnetem.DuplicateCorr {
   196  		t.Fatal("DuplicateCorr does not match")
   197  	}
   198  
   199  	// Deletion
   200  	// automatically removes netem qdisc
   201  	if err := ClassDel(class); err != nil {
   202  		t.Fatal(err)
   203  	}
   204  	classes, err = SafeClassList(link, MakeHandle(0xffff, 0))
   205  	if err != nil {
   206  		t.Fatal(err)
   207  	}
   208  	if len(classes) != 0 {
   209  		t.Fatal("Failed to remove class")
   210  	}
   211  	if err := QdiscDel(qdisc); err != nil {
   212  		t.Fatal(err)
   213  	}
   214  	qdiscs, err = SafeQdiscList(link)
   215  	if err != nil {
   216  		t.Fatal(err)
   217  	}
   218  	if len(qdiscs) != 0 {
   219  		t.Fatal("Failed to remove qdisc")
   220  	}
   221  }
   222  
   223  func TestHtbClassAddHtbClassChangeDel(t *testing.T) {
   224  	/**
   225  	This test first set up a interface ans set up a Htb qdisc
   226  	A HTB class is attach to it and a Netem qdisc is attached to that class
   227  	Next, we test changing the HTB class in place and confirming the Netem is
   228  	still attached. We also check that invoting ClassChange with a non-existing
   229  	class will fail.
   230  	Finally, we test ClassReplace. We confirm it correctly behave like
   231  	ClassChange when the parent/handle pair exists and that it will create a
   232  	new class if the handle is modified.
   233  	*/
   234  	tearDown := setUpNetlinkTest(t)
   235  	defer tearDown()
   236  	if err := LinkAdd(&Ifb{LinkAttrs{Name: "foo"}}); err != nil {
   237  		t.Fatal(err)
   238  	}
   239  	link, err := LinkByName("foo")
   240  	if err != nil {
   241  		t.Fatal(err)
   242  	}
   243  	if err := LinkSetUp(link); err != nil {
   244  		t.Fatal(err)
   245  	}
   246  	attrs := QdiscAttrs{
   247  		LinkIndex: link.Attrs().Index,
   248  		Handle:    MakeHandle(0xffff, 0),
   249  		Parent:    HANDLE_ROOT,
   250  	}
   251  	qdisc := NewHtb(attrs)
   252  	if err := QdiscAdd(qdisc); err != nil {
   253  		t.Fatal(err)
   254  	}
   255  	qdiscs, err := SafeQdiscList(link)
   256  	if err != nil {
   257  		t.Fatal(err)
   258  	}
   259  	if len(qdiscs) != 1 {
   260  		t.Fatal("Failed to add qdisc")
   261  	}
   262  	_, ok := qdiscs[0].(*Htb)
   263  	if !ok {
   264  		t.Fatal("Qdisc is the wrong type")
   265  	}
   266  
   267  	classattrs := ClassAttrs{
   268  		LinkIndex: link.Attrs().Index,
   269  		Parent:    MakeHandle(0xffff, 0),
   270  		Handle:    MakeHandle(0xffff, 2),
   271  	}
   272  
   273  	htbclassattrs := HtbClassAttrs{
   274  		Rate:    uint64(1<<32) + 10,
   275  		Ceil:    uint64(1<<32) + 20,
   276  		Cbuffer: 1690,
   277  	}
   278  	class := NewHtbClass(classattrs, htbclassattrs)
   279  	if err := ClassAdd(class); err != nil {
   280  		t.Fatal(err)
   281  	}
   282  	classes, err := SafeClassList(link, 0)
   283  	if err != nil {
   284  		t.Fatal(err)
   285  	}
   286  	if len(classes) != 1 {
   287  		t.Fatal("Failed to add class")
   288  	}
   289  
   290  	htb, ok := classes[0].(*HtbClass)
   291  	if !ok {
   292  		t.Fatal("Class is the wrong type")
   293  	}
   294  
   295  	testClassStats(htb.ClassAttrs.Statistics, NewClassStatistics(), t)
   296  
   297  	qattrs := QdiscAttrs{
   298  		LinkIndex: link.Attrs().Index,
   299  		Handle:    MakeHandle(0x2, 0),
   300  		Parent:    MakeHandle(0xffff, 2),
   301  	}
   302  	nattrs := NetemQdiscAttrs{
   303  		Latency:     20000,
   304  		Loss:        23.4,
   305  		Duplicate:   14.3,
   306  		LossCorr:    8.34,
   307  		Jitter:      1000,
   308  		DelayCorr:   12.3,
   309  		ReorderProb: 23.4,
   310  		CorruptProb: 10.0,
   311  		CorruptCorr: 10,
   312  	}
   313  	qdiscnetem := NewNetem(qattrs, nattrs)
   314  	if err := QdiscAdd(qdiscnetem); err != nil {
   315  		t.Fatal(err)
   316  	}
   317  
   318  	qdiscs, err = SafeQdiscList(link)
   319  	if err != nil {
   320  		t.Fatal(err)
   321  	}
   322  	if len(qdiscs) != 2 {
   323  		t.Fatal("Failed to add qdisc")
   324  	}
   325  
   326  	_, ok = qdiscs[1].(*Netem)
   327  	if !ok {
   328  		t.Fatal("Qdisc is the wrong type")
   329  	}
   330  
   331  	// Change
   332  	// For change to work, the handle and parent cannot be changed.
   333  
   334  	// First, test it fails if we change the Handle.
   335  	oldHandle := classattrs.Handle
   336  	classattrs.Handle = MakeHandle(0xffff, 3)
   337  	class = NewHtbClass(classattrs, htbclassattrs)
   338  	if err := ClassChange(class); err == nil {
   339  		t.Fatal("ClassChange should not work when using a different handle.")
   340  	}
   341  	// It should work with the same handle
   342  	classattrs.Handle = oldHandle
   343  	htbclassattrs.Rate = 4321000
   344  	class = NewHtbClass(classattrs, htbclassattrs)
   345  	if err := ClassChange(class); err != nil {
   346  		t.Fatal(err)
   347  	}
   348  
   349  	classes, err = SafeClassList(link, MakeHandle(0xffff, 0))
   350  	if err != nil {
   351  		t.Fatal(err)
   352  	}
   353  	if len(classes) != 1 {
   354  		t.Fatalf(
   355  			"1 class expected, %d found",
   356  			len(classes),
   357  		)
   358  	}
   359  
   360  	htb, ok = classes[0].(*HtbClass)
   361  	if !ok {
   362  		t.Fatal("Class is the wrong type")
   363  	}
   364  	// Verify that the rate value has changed.
   365  	if htb.Rate != class.Rate {
   366  		t.Fatal("Rate did not get changed while changing the class.")
   367  	}
   368  
   369  	// Check that we still have the netem child qdisc
   370  	qdiscs, err = SafeQdiscList(link)
   371  	if err != nil {
   372  		t.Fatal(err)
   373  	}
   374  
   375  	if len(qdiscs) != 2 {
   376  		t.Fatalf("2 qdisc expected, %d found", len(qdiscs))
   377  	}
   378  	_, ok = qdiscs[0].(*Htb)
   379  	if !ok {
   380  		t.Fatal("Qdisc is the wrong type")
   381  	}
   382  
   383  	_, ok = qdiscs[1].(*Netem)
   384  	if !ok {
   385  		t.Fatal("Qdisc is the wrong type")
   386  	}
   387  
   388  	// Replace
   389  	// First replace by keeping the same handle, class will be changed.
   390  	// Then, replace by providing a new handle, n new class will be created.
   391  
   392  	// Replace acting as Change
   393  	class = NewHtbClass(classattrs, htbclassattrs)
   394  	if err := ClassReplace(class); err != nil {
   395  		t.Fatal("Failed to replace class that is existing.")
   396  	}
   397  
   398  	classes, err = SafeClassList(link, MakeHandle(0xffff, 0))
   399  	if err != nil {
   400  		t.Fatal(err)
   401  	}
   402  	if len(classes) != 1 {
   403  		t.Fatalf(
   404  			"1 class expected, %d found",
   405  			len(classes),
   406  		)
   407  	}
   408  
   409  	htb, ok = classes[0].(*HtbClass)
   410  	if !ok {
   411  		t.Fatal("Class is the wrong type")
   412  	}
   413  	// Verify that the rate value has changed.
   414  	if htb.Rate != class.Rate {
   415  		t.Fatal("Rate did not get changed while changing the class.")
   416  	}
   417  
   418  	// It should work with the same handle
   419  	classattrs.Handle = MakeHandle(0xffff, 3)
   420  	class = NewHtbClass(classattrs, htbclassattrs)
   421  	if err := ClassReplace(class); err != nil {
   422  		t.Fatal(err)
   423  	}
   424  
   425  	classes, err = SafeClassList(link, MakeHandle(0xffff, 0))
   426  	if err != nil {
   427  		t.Fatal(err)
   428  	}
   429  	if len(classes) != 2 {
   430  		t.Fatalf(
   431  			"2 classes expected, %d found",
   432  			len(classes),
   433  		)
   434  	}
   435  
   436  	htb, ok = classes[1].(*HtbClass)
   437  	if !ok {
   438  		t.Fatal("Class is the wrong type")
   439  	}
   440  	// Verify that the rate value has changed.
   441  	if htb.Rate != class.Rate {
   442  		t.Fatal("Rate did not get changed while changing the class.")
   443  	}
   444  
   445  	// Deletion
   446  	for _, class := range classes {
   447  		if err := ClassDel(class); err != nil {
   448  			t.Fatal(err)
   449  		}
   450  	}
   451  
   452  	classes, err = SafeClassList(link, MakeHandle(0xffff, 0))
   453  	if err != nil {
   454  		t.Fatal(err)
   455  	}
   456  	if len(classes) != 0 {
   457  		t.Fatal("Failed to remove class")
   458  	}
   459  	if err := QdiscDel(qdisc); err != nil {
   460  		t.Fatal(err)
   461  	}
   462  	qdiscs, err = SafeQdiscList(link)
   463  	if err != nil {
   464  		t.Fatal(err)
   465  	}
   466  	if len(qdiscs) != 0 {
   467  		t.Fatal("Failed to remove qdisc")
   468  	}
   469  }
   470  
   471  func TestClassHfsc(t *testing.T) {
   472  	// New network namespace for tests
   473  	tearDown := setUpNetlinkTestWithKModule(t, "sch_hfsc")
   474  	defer tearDown()
   475  
   476  	// Set up testing link and check if succeeded
   477  	if err := LinkAdd(&Ifb{LinkAttrs{Name: "foo"}}); err != nil {
   478  		t.Fatal(err)
   479  	}
   480  	link, err := LinkByName("foo")
   481  	if err != nil {
   482  		t.Fatal(err)
   483  	}
   484  	if err := LinkSetUp(link); err != nil {
   485  		t.Fatal(err)
   486  	}
   487  
   488  	// Adding HFSC qdisc
   489  	qdiscAttrs := QdiscAttrs{
   490  		LinkIndex: link.Attrs().Index,
   491  		Handle:    MakeHandle(1, 0),
   492  		Parent:    HANDLE_ROOT,
   493  	}
   494  	hfscQdisc := NewHfsc(qdiscAttrs)
   495  	hfscQdisc.Defcls = 2
   496  
   497  	err = QdiscAdd(hfscQdisc)
   498  	if err != nil {
   499  		t.Fatal(err)
   500  	}
   501  	qdiscs, err := SafeQdiscList(link)
   502  	if err != nil {
   503  		t.Fatal(err)
   504  	}
   505  	if len(qdiscs) != 1 {
   506  		t.Fatal("Failed to add qdisc")
   507  	}
   508  	_, ok := qdiscs[0].(*Hfsc)
   509  	if !ok {
   510  		t.Fatal("Qdisc is the wrong type")
   511  	}
   512  
   513  	// Adding some HFSC classes
   514  	classAttrs := ClassAttrs{
   515  		LinkIndex: link.Attrs().Index,
   516  		Parent:    MakeHandle(1, 0),
   517  		Handle:    MakeHandle(1, 1),
   518  	}
   519  	hfscClass := NewHfscClass(classAttrs)
   520  	hfscClass.SetLS(5e6, 10, 5e6)
   521  
   522  	err = ClassAdd(hfscClass)
   523  	if err != nil {
   524  		t.Fatal(err)
   525  	}
   526  
   527  	hfscClass2 := hfscClass
   528  	hfscClass2.SetLS(0, 0, 0)
   529  	hfscClass2.Attrs().Parent = MakeHandle(1, 1)
   530  	hfscClass2.Attrs().Handle = MakeHandle(1, 2)
   531  	hfscClass2.SetRsc(0, 0, 2e6)
   532  
   533  	err = ClassAdd(hfscClass2)
   534  	if err != nil {
   535  		t.Fatal(err)
   536  	}
   537  
   538  	hfscClass3 := hfscClass
   539  	hfscClass3.SetLS(0, 0, 0)
   540  	hfscClass3.Attrs().Parent = MakeHandle(1, 1)
   541  	hfscClass3.Attrs().Handle = MakeHandle(1, 3)
   542  
   543  	err = ClassAdd(hfscClass3)
   544  	if err != nil {
   545  		t.Fatal(err)
   546  	}
   547  
   548  	// Check the classes
   549  	classes, err := SafeClassList(link, MakeHandle(1, 0))
   550  	if err != nil {
   551  		t.Fatal(err)
   552  	}
   553  	if len(classes) != 4 {
   554  		t.Fatal("Failed to add classes")
   555  	}
   556  	for _, c := range classes {
   557  		class, ok := c.(*HfscClass)
   558  		if !ok {
   559  			t.Fatal("Wrong type of class")
   560  		}
   561  		if class.ClassAttrs.Handle == hfscClass.ClassAttrs.Handle {
   562  			if class.Fsc != hfscClass.Fsc {
   563  				t.Fatal("HfscClass FSC don't match")
   564  			}
   565  			if class.Usc != hfscClass.Usc {
   566  				t.Fatal("HfscClass USC don't match")
   567  			}
   568  			if class.Rsc != hfscClass.Rsc {
   569  				t.Fatal("HfscClass RSC don't match")
   570  			}
   571  		}
   572  		if class.ClassAttrs.Handle == hfscClass2.ClassAttrs.Handle {
   573  			if class.Fsc != hfscClass2.Fsc {
   574  				t.Fatal("HfscClass2 FSC don't match")
   575  			}
   576  			if class.Usc != hfscClass2.Usc {
   577  				t.Fatal("HfscClass2 USC don't match")
   578  			}
   579  			if class.Rsc != hfscClass2.Rsc {
   580  				t.Fatal("HfscClass2 RSC don't match")
   581  			}
   582  		}
   583  		if class.ClassAttrs.Handle == hfscClass3.ClassAttrs.Handle {
   584  			if class.Fsc != hfscClass3.Fsc {
   585  				t.Fatal("HfscClass3 FSC don't match")
   586  			}
   587  			if class.Usc != hfscClass3.Usc {
   588  				t.Fatal("HfscClass3 USC don't match")
   589  			}
   590  			if class.Rsc != hfscClass3.Rsc {
   591  				t.Fatal("HfscClass3 RSC don't match")
   592  			}
   593  		}
   594  	}
   595  
   596  	// Terminating the leafs with fq_codel qdiscs
   597  	fqcodelAttrs := QdiscAttrs{
   598  		LinkIndex: link.Attrs().Index,
   599  		Parent:    MakeHandle(1, 2),
   600  		Handle:    MakeHandle(2, 0),
   601  	}
   602  	fqcodel1 := NewFqCodel(fqcodelAttrs)
   603  	fqcodel1.ECN = 0
   604  	fqcodel1.Limit = 1200
   605  	fqcodel1.Flows = 65535
   606  	fqcodel1.Target = 5
   607  
   608  	err = QdiscAdd(fqcodel1)
   609  	if err != nil {
   610  		t.Fatal(err)
   611  	}
   612  
   613  	fqcodel2 := fqcodel1
   614  	fqcodel2.Attrs().Handle = MakeHandle(3, 0)
   615  	fqcodel2.Attrs().Parent = MakeHandle(1, 3)
   616  
   617  	err = QdiscAdd(fqcodel2)
   618  	if err != nil {
   619  		t.Fatal(err)
   620  	}
   621  
   622  	// Check the amount of qdiscs
   623  	qdiscs, _ = SafeQdiscList(link)
   624  	if len(qdiscs) != 3 {
   625  		t.Fatal("Failed to add qdisc")
   626  	}
   627  	for _, q := range qdiscs[1:] {
   628  		_, ok = q.(*FqCodel)
   629  		if !ok {
   630  			t.Fatal("Qdisc is the wrong type")
   631  		}
   632  	}
   633  
   634  	// removing a class
   635  	if err := ClassDel(hfscClass3); err != nil {
   636  		t.Fatal(err)
   637  	}
   638  	// Check the classes
   639  	classes, err = SafeClassList(link, MakeHandle(1, 0))
   640  	if err != nil {
   641  		t.Fatal(err)
   642  	}
   643  	if len(classes) != 3 {
   644  		t.Fatal("Failed to delete classes")
   645  	}
   646  	// Check qdisc
   647  	qdiscs, _ = SafeQdiscList(link)
   648  	if len(qdiscs) != 2 {
   649  		t.Fatal("Failed to delete qdisc")
   650  	}
   651  
   652  	// Changing a class
   653  	hfscClass2.SetRsc(0, 0, 0)
   654  	hfscClass2.SetSC(5e6, 100, 1e6)
   655  	hfscClass2.SetUL(6e6, 50, 2e6)
   656  	hfscClass2.Attrs().Handle = MakeHandle(1, 8)
   657  	if err := ClassChange(hfscClass2); err == nil {
   658  		t.Fatal("Class change shouldn't work with a different handle")
   659  	}
   660  	hfscClass2.Attrs().Handle = MakeHandle(1, 2)
   661  	if err := ClassChange(hfscClass2); err != nil {
   662  		t.Fatal(err)
   663  	}
   664  
   665  	// Replacing a class
   666  	// If the handle doesn't exist, create it
   667  	hfscClass2.SetSC(6e6, 100, 2e6)
   668  	hfscClass2.SetUL(8e6, 500, 4e6)
   669  	hfscClass2.Attrs().Handle = MakeHandle(1, 8)
   670  	if err := ClassReplace(hfscClass2); err != nil {
   671  		t.Fatal(err)
   672  	}
   673  	// If the handle exists, replace it
   674  	hfscClass.SetLS(5e6, 200, 1e6)
   675  	if err := ClassChange(hfscClass); err != nil {
   676  		t.Fatal(err)
   677  	}
   678  
   679  }