...

Source file src/github.com/cilium/ebpf/btf/core_test.go

Documentation: github.com/cilium/ebpf/btf

     1  package btf
     2  
     3  import (
     4  	"errors"
     5  	"math/rand"
     6  	"os"
     7  	"strings"
     8  	"testing"
     9  
    10  	"github.com/cilium/ebpf/internal/testutils"
    11  	"github.com/google/go-cmp/cmp"
    12  
    13  	qt "github.com/frankban/quicktest"
    14  )
    15  
    16  func TestCOREAreTypesCompatible(t *testing.T) {
    17  	tests := []struct {
    18  		a, b       Type
    19  		compatible bool
    20  	}{
    21  		{&Void{}, &Void{}, true},
    22  		{&Struct{Name: "a"}, &Struct{Name: "b"}, true},
    23  		{&Union{Name: "a"}, &Union{Name: "b"}, true},
    24  		{&Union{Name: "a"}, &Struct{Name: "b"}, false},
    25  		{&Enum{Name: "a"}, &Enum{Name: "b"}, true},
    26  		{&Fwd{Name: "a"}, &Fwd{Name: "b"}, true},
    27  		{&Int{Name: "a", Size: 2}, &Int{Name: "b", Size: 4}, true},
    28  		{&Pointer{Target: &Void{}}, &Pointer{Target: &Void{}}, true},
    29  		{&Pointer{Target: &Void{}}, &Void{}, false},
    30  		{&Array{Index: &Void{}, Type: &Void{}}, &Array{Index: &Void{}, Type: &Void{}}, true},
    31  		{&Array{Index: &Void{}, Type: &Int{}}, &Array{Index: &Void{}, Type: &Void{}}, false},
    32  		{&FuncProto{Return: &Int{}}, &FuncProto{Return: &Void{}}, false},
    33  		{
    34  			&FuncProto{Return: &Void{}, Params: []FuncParam{{Name: "a", Type: &Void{}}}},
    35  			&FuncProto{Return: &Void{}, Params: []FuncParam{{Name: "b", Type: &Void{}}}},
    36  			true,
    37  		},
    38  		{
    39  			&FuncProto{Return: &Void{}, Params: []FuncParam{{Type: &Void{}}}},
    40  			&FuncProto{Return: &Void{}, Params: []FuncParam{{Type: &Int{}}}},
    41  			false,
    42  		},
    43  		{
    44  			&FuncProto{Return: &Void{}, Params: []FuncParam{{Type: &Void{}}, {Type: &Void{}}}},
    45  			&FuncProto{Return: &Void{}, Params: []FuncParam{{Type: &Void{}}}},
    46  			false,
    47  		},
    48  	}
    49  
    50  	for _, test := range tests {
    51  		err := coreAreTypesCompatible(test.a, test.b)
    52  		if test.compatible {
    53  			if err != nil {
    54  				t.Errorf("Expected types to be compatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
    55  				continue
    56  			}
    57  		} else {
    58  			if !errors.Is(err, errImpossibleRelocation) {
    59  				t.Errorf("Expected types to be incompatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
    60  				continue
    61  			}
    62  		}
    63  
    64  		err = coreAreTypesCompatible(test.b, test.a)
    65  		if test.compatible {
    66  			if err != nil {
    67  				t.Errorf("Expected reversed types to be compatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
    68  			}
    69  		} else {
    70  			if !errors.Is(err, errImpossibleRelocation) {
    71  				t.Errorf("Expected reversed types to be incompatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
    72  			}
    73  		}
    74  	}
    75  
    76  	for _, invalid := range []Type{&Var{}, &Datasec{}} {
    77  		err := coreAreTypesCompatible(invalid, invalid)
    78  		if errors.Is(err, errImpossibleRelocation) {
    79  			t.Errorf("Expected an error for %T, not errImpossibleRelocation", invalid)
    80  		} else if err == nil {
    81  			t.Errorf("Expected an error for %T", invalid)
    82  		}
    83  	}
    84  }
    85  
    86  func TestCOREAreMembersCompatible(t *testing.T) {
    87  	tests := []struct {
    88  		a, b       Type
    89  		compatible bool
    90  	}{
    91  		{&Struct{Name: "a"}, &Struct{Name: "b"}, true},
    92  		{&Union{Name: "a"}, &Union{Name: "b"}, true},
    93  		{&Union{Name: "a"}, &Struct{Name: "b"}, true},
    94  		{&Enum{Name: "a"}, &Enum{Name: "b"}, false},
    95  		{&Enum{Name: "a"}, &Enum{Name: "a___foo"}, true},
    96  		{&Enum{Name: "a"}, &Enum{Name: ""}, true},
    97  		{&Fwd{Name: "a"}, &Fwd{Name: "b"}, false},
    98  		{&Fwd{Name: "a"}, &Fwd{Name: "a___foo"}, true},
    99  		{&Fwd{Name: "a"}, &Fwd{Name: ""}, true},
   100  		{&Int{Name: "a", Size: 2}, &Int{Name: "b", Size: 4}, true},
   101  		{&Pointer{Target: &Void{}}, &Pointer{Target: &Void{}}, true},
   102  		{&Pointer{Target: &Void{}}, &Void{}, false},
   103  		{&Array{Type: &Int{Size: 1}}, &Array{Type: &Int{Encoding: Signed}}, true},
   104  		{&Float{Size: 2}, &Float{Size: 4}, true},
   105  	}
   106  
   107  	for _, test := range tests {
   108  		err := coreAreMembersCompatible(test.a, test.b)
   109  		if test.compatible {
   110  			if err != nil {
   111  				t.Errorf("Expected members to be compatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
   112  				continue
   113  			}
   114  		} else {
   115  			if !errors.Is(err, errImpossibleRelocation) {
   116  				t.Errorf("Expected members to be incompatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
   117  				continue
   118  			}
   119  		}
   120  
   121  		err = coreAreMembersCompatible(test.b, test.a)
   122  		if test.compatible {
   123  			if err != nil {
   124  				t.Errorf("Expected reversed members to be compatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
   125  			}
   126  		} else {
   127  			if !errors.Is(err, errImpossibleRelocation) {
   128  				t.Errorf("Expected reversed members to be incompatible: %s\na = %#v\nb = %#v", err, test.a, test.b)
   129  			}
   130  		}
   131  	}
   132  
   133  	for _, invalid := range []Type{&Void{}, &FuncProto{}, &Var{}, &Datasec{}} {
   134  		err := coreAreMembersCompatible(invalid, invalid)
   135  		if errors.Is(err, errImpossibleRelocation) {
   136  			t.Errorf("Expected an error for %T, not errImpossibleRelocation", invalid)
   137  		} else if err == nil {
   138  			t.Errorf("Expected an error for %T", invalid)
   139  		}
   140  	}
   141  }
   142  
   143  func TestCOREAccessor(t *testing.T) {
   144  	for _, valid := range []string{
   145  		"0",
   146  		"1:0",
   147  		"1:0:3:34:10:1",
   148  	} {
   149  		_, err := parseCOREAccessor(valid)
   150  		if err != nil {
   151  			t.Errorf("Parse %q: %s", valid, err)
   152  		}
   153  	}
   154  
   155  	for _, invalid := range []string{
   156  		"",
   157  		"-1",
   158  		":",
   159  		"0:",
   160  		":12",
   161  		"4294967296",
   162  	} {
   163  		_, err := parseCOREAccessor(invalid)
   164  		if err == nil {
   165  			t.Errorf("Accepted invalid accessor %q", invalid)
   166  		}
   167  	}
   168  }
   169  
   170  func TestCOREFindEnumValue(t *testing.T) {
   171  	a := &Enum{Values: []EnumValue{{"foo", 23}, {"bar", 42}}}
   172  	b := &Enum{Values: []EnumValue{
   173  		{"foo___flavour", 0},
   174  		{"bar", 123},
   175  		{"garbage", 3},
   176  	}}
   177  
   178  	invalid := []struct {
   179  		name   string
   180  		local  Type
   181  		target Type
   182  		acc    coreAccessor
   183  		err    error
   184  	}{
   185  		{"o-o-b accessor", a, b, coreAccessor{len(a.Values)}, nil},
   186  		{"long accessor", a, b, coreAccessor{0, 1}, nil},
   187  		{"wrong target", a, &Void{}, coreAccessor{0, 1}, nil},
   188  		{
   189  			"no matching value",
   190  			b, a,
   191  			coreAccessor{2},
   192  			errImpossibleRelocation,
   193  		},
   194  	}
   195  
   196  	for _, test := range invalid {
   197  		t.Run(test.name, func(t *testing.T) {
   198  			_, _, err := coreFindEnumValue(test.local, test.acc, test.target)
   199  			if test.err != nil && !errors.Is(err, test.err) {
   200  				t.Fatalf("Expected %s, got %s", test.err, err)
   201  			}
   202  			if err == nil {
   203  				t.Fatal("Accepted invalid case")
   204  			}
   205  		})
   206  	}
   207  
   208  	valid := []struct {
   209  		name                    string
   210  		local, target           Type
   211  		acc                     coreAccessor
   212  		localValue, targetValue int32
   213  	}{
   214  		{"a to b", a, b, coreAccessor{0}, 23, 0},
   215  		{"b to a", b, a, coreAccessor{1}, 123, 42},
   216  	}
   217  
   218  	for _, test := range valid {
   219  		t.Run(test.name, func(t *testing.T) {
   220  			local, target, err := coreFindEnumValue(test.local, test.acc, test.target)
   221  			qt.Assert(t, err, qt.IsNil)
   222  			qt.Check(t, local.Value, qt.Equals, test.localValue)
   223  			qt.Check(t, target.Value, qt.Equals, test.targetValue)
   224  		})
   225  	}
   226  }
   227  
   228  func TestCOREFindField(t *testing.T) {
   229  	ptr := &Pointer{}
   230  	u16 := &Int{Size: 2}
   231  	u32 := &Int{Size: 4}
   232  	aFields := []Member{
   233  		{Name: "foo", Type: ptr, Offset: 8},
   234  		{Name: "bar", Type: u16, Offset: 16},
   235  		{Name: "baz", Type: u32, Offset: 32, BitfieldSize: 3},
   236  		{Name: "quux", Type: u32, Offset: 35, BitfieldSize: 10},
   237  		{Name: "quuz", Type: u32, Offset: 45, BitfieldSize: 8},
   238  	}
   239  	bFields := []Member{
   240  		{Name: "foo", Type: ptr, Offset: 16},
   241  		{Name: "bar", Type: u32, Offset: 8},
   242  		{Name: "other", Offset: 4},
   243  		// baz is separated out from the other bitfields
   244  		{Name: "baz", Type: u32, Offset: 64, BitfieldSize: 3},
   245  		// quux's type changes u32->u16
   246  		{Name: "quux", Type: u16, Offset: 96, BitfieldSize: 10},
   247  		// quuz becomes a normal field
   248  		{Name: "quuz", Type: u16, Offset: 112},
   249  	}
   250  
   251  	aStruct := &Struct{Members: aFields, Size: 48}
   252  	bStruct := &Struct{Members: bFields, Size: 80}
   253  	aArray := &Array{Nelems: 4, Type: u16}
   254  	bArray := &Array{Nelems: 3, Type: u32}
   255  
   256  	invalid := []struct {
   257  		name          string
   258  		local, target Type
   259  		acc           coreAccessor
   260  		err           error
   261  	}{
   262  		{
   263  			"unsupported type",
   264  			&Void{}, &Void{},
   265  			coreAccessor{0, 0},
   266  			ErrNotSupported,
   267  		},
   268  		{
   269  			"different types",
   270  			&Union{}, &Array{Type: u16},
   271  			coreAccessor{0},
   272  			errImpossibleRelocation,
   273  		},
   274  		{
   275  			"invalid composite accessor",
   276  			aStruct, aStruct,
   277  			coreAccessor{0, len(aStruct.Members)},
   278  			nil,
   279  		},
   280  		{
   281  			"invalid array accessor",
   282  			aArray, aArray,
   283  			coreAccessor{0, int(aArray.Nelems)},
   284  			nil,
   285  		},
   286  		{
   287  			"o-o-b array accessor",
   288  			aArray, bArray,
   289  			coreAccessor{0, int(bArray.Nelems)},
   290  			errImpossibleRelocation,
   291  		},
   292  		{
   293  			"no match",
   294  			bStruct, aStruct,
   295  			coreAccessor{0, 2},
   296  			errImpossibleRelocation,
   297  		},
   298  		{
   299  			"incompatible match",
   300  			&Union{Members: []Member{{Name: "foo", Type: &Pointer{}}}},
   301  			&Union{Members: []Member{{Name: "foo", Type: &Int{}}}},
   302  			coreAccessor{0, 0},
   303  			errImpossibleRelocation,
   304  		},
   305  	}
   306  
   307  	for _, test := range invalid {
   308  		t.Run(test.name, func(t *testing.T) {
   309  			_, _, err := coreFindField(test.local, test.acc, test.target)
   310  			if test.err != nil && !errors.Is(err, test.err) {
   311  				t.Fatalf("Expected %s, got %s", test.err, err)
   312  			}
   313  			if err == nil {
   314  				t.Fatal("Accepted invalid case")
   315  			}
   316  			t.Log(err)
   317  		})
   318  	}
   319  
   320  	bytes := func(typ Type) uint32 {
   321  		sz, err := Sizeof(typ)
   322  		if err != nil {
   323  			t.Fatal(err)
   324  		}
   325  		return uint32(sz)
   326  	}
   327  
   328  	anon := func(t Type, offset Bits) []Member {
   329  		return []Member{{Type: t, Offset: offset}}
   330  	}
   331  
   332  	anonStruct := func(m ...Member) Member {
   333  		return Member{Type: &Struct{Members: m}}
   334  	}
   335  
   336  	anonUnion := func(m ...Member) Member {
   337  		return Member{Type: &Union{Members: m}}
   338  	}
   339  
   340  	valid := []struct {
   341  		name                    string
   342  		local                   Type
   343  		target                  Type
   344  		acc                     coreAccessor
   345  		localField, targetField coreField
   346  	}{
   347  		{
   348  			"array[0]",
   349  			aArray,
   350  			bArray,
   351  			coreAccessor{0, 0},
   352  			coreField{u16, 0, 0, 0},
   353  			coreField{u32, 0, 0, 0},
   354  		},
   355  		{
   356  			"array[1]",
   357  			aArray,
   358  			bArray,
   359  			coreAccessor{0, 1},
   360  			coreField{u16, bytes(aArray.Type), 0, 0},
   361  			coreField{u32, bytes(bArray.Type), 0, 0},
   362  		},
   363  		{
   364  			"array[0] with base offset",
   365  			aArray,
   366  			bArray,
   367  			coreAccessor{1, 0},
   368  			coreField{u16, bytes(aArray), 0, 0},
   369  			coreField{u32, bytes(bArray), 0, 0},
   370  		},
   371  		{
   372  			"array[2] with base offset",
   373  			aArray,
   374  			bArray,
   375  			coreAccessor{1, 2},
   376  			coreField{u16, bytes(aArray) + 2*bytes(aArray.Type), 0, 0},
   377  			coreField{u32, bytes(bArray) + 2*bytes(bArray.Type), 0, 0},
   378  		},
   379  		{
   380  			"flex array",
   381  			&Struct{Members: []Member{{Name: "foo", Type: &Array{Nelems: 0, Type: u16}}}},
   382  			&Struct{Members: []Member{{Name: "foo", Type: &Array{Nelems: 0, Type: u32}}}},
   383  			coreAccessor{0, 0, 9000},
   384  			coreField{u16, bytes(u16) * 9000, 0, 0},
   385  			coreField{u32, bytes(u32) * 9000, 0, 0},
   386  		},
   387  		{
   388  			"struct.0",
   389  			aStruct, bStruct,
   390  			coreAccessor{0, 0},
   391  			coreField{ptr, 1, 0, 0},
   392  			coreField{ptr, 2, 0, 0},
   393  		},
   394  		{
   395  			"struct.0 anon",
   396  			aStruct, &Struct{Members: anon(bStruct, 24)},
   397  			coreAccessor{0, 0},
   398  			coreField{ptr, 1, 0, 0},
   399  			coreField{ptr, 3 + 2, 0, 0},
   400  		},
   401  		{
   402  			"struct.0 with base offset",
   403  			aStruct, bStruct,
   404  			coreAccessor{3, 0},
   405  			coreField{ptr, 3*bytes(aStruct) + 1, 0, 0},
   406  			coreField{ptr, 3*bytes(bStruct) + 2, 0, 0},
   407  		},
   408  		{
   409  			"struct.1",
   410  			aStruct, bStruct,
   411  			coreAccessor{0, 1},
   412  			coreField{u16, 2, 0, 0},
   413  			coreField{u32, 1, 0, 0},
   414  		},
   415  		{
   416  			"struct.1 anon",
   417  			aStruct, &Struct{Members: anon(bStruct, 24)},
   418  			coreAccessor{0, 1},
   419  			coreField{u16, 2, 0, 0},
   420  			coreField{u32, 3 + 1, 0, 0},
   421  		},
   422  		{
   423  			"union.1",
   424  			&Union{Members: aFields, Size: 32},
   425  			&Union{Members: bFields, Size: 32},
   426  			coreAccessor{0, 1},
   427  			coreField{u16, 2, 0, 0},
   428  			coreField{u32, 1, 0, 0},
   429  		},
   430  		{
   431  			"interchangeable composites",
   432  			&Struct{
   433  				Members: []Member{
   434  					anonStruct(anonUnion(Member{Name: "_1", Type: u16})),
   435  				},
   436  			},
   437  			&Struct{
   438  				Members: []Member{
   439  					anonUnion(anonStruct(Member{Name: "_1", Type: u16})),
   440  				},
   441  			},
   442  			coreAccessor{0, 0, 0, 0},
   443  			coreField{u16, 0, 0, 0},
   444  			coreField{u16, 0, 0, 0},
   445  		},
   446  		{
   447  			"struct.2 (bitfield baz)",
   448  			aStruct, bStruct,
   449  			coreAccessor{0, 2},
   450  			coreField{u32, 4, 0, 3},
   451  			coreField{u32, 8, 0, 3},
   452  		},
   453  		{
   454  			"struct.3 (bitfield quux)",
   455  			aStruct, bStruct,
   456  			coreAccessor{0, 3},
   457  			coreField{u32, 4, 3, 10},
   458  			coreField{u16, 12, 0, 10},
   459  		},
   460  		{
   461  			"struct.4 (bitfield quuz)",
   462  			aStruct, bStruct,
   463  			coreAccessor{0, 4},
   464  			coreField{u32, 4, 13, 8},
   465  			coreField{u16, 14, 0, 0},
   466  		},
   467  	}
   468  
   469  	allowCoreField := cmp.AllowUnexported(coreField{})
   470  
   471  	checkCOREField := func(t *testing.T, which string, got, want coreField) {
   472  		t.Helper()
   473  		if diff := cmp.Diff(want, got, allowCoreField); diff != "" {
   474  			t.Errorf("%s mismatch (-want +got):\n%s", which, diff)
   475  		}
   476  	}
   477  
   478  	for _, test := range valid {
   479  		t.Run(test.name, func(t *testing.T) {
   480  			localField, targetField, err := coreFindField(test.local, test.acc, test.target)
   481  			qt.Assert(t, err, qt.IsNil)
   482  			checkCOREField(t, "local", localField, test.localField)
   483  			checkCOREField(t, "target", targetField, test.targetField)
   484  		})
   485  	}
   486  }
   487  
   488  func TestCOREFindFieldCyclical(t *testing.T) {
   489  	members := []Member{{Name: "foo", Type: &Pointer{}}}
   490  
   491  	cyclicStruct := &Struct{}
   492  	cyclicStruct.Members = []Member{{Type: cyclicStruct}}
   493  
   494  	cyclicUnion := &Union{}
   495  	cyclicUnion.Members = []Member{{Type: cyclicUnion}}
   496  
   497  	cyclicArray := &Array{Nelems: 1}
   498  	cyclicArray.Type = &Pointer{Target: cyclicArray}
   499  
   500  	tests := []struct {
   501  		name          string
   502  		local, cyclic Type
   503  	}{
   504  		{"struct", &Struct{Members: members}, cyclicStruct},
   505  		{"union", &Union{Members: members}, cyclicUnion},
   506  		{"array", &Array{Nelems: 2, Type: &Int{}}, cyclicArray},
   507  	}
   508  
   509  	for _, test := range tests {
   510  		t.Run(test.name, func(t *testing.T) {
   511  			_, _, err := coreFindField(test.local, coreAccessor{0, 0}, test.cyclic)
   512  			if !errors.Is(err, errImpossibleRelocation) {
   513  				t.Fatal("Should return errImpossibleRelocation, got", err)
   514  			}
   515  		})
   516  	}
   517  }
   518  
   519  func TestCORERelocation(t *testing.T) {
   520  	testutils.Files(t, testutils.Glob(t, "testdata/*.elf"), func(t *testing.T, file string) {
   521  		rd, err := os.Open(file)
   522  		if err != nil {
   523  			t.Fatal(err)
   524  		}
   525  		defer rd.Close()
   526  
   527  		spec, extInfos, err := LoadSpecAndExtInfosFromReader(rd)
   528  		if err != nil {
   529  			t.Fatal(err)
   530  		}
   531  
   532  		if extInfos == nil {
   533  			t.Skip("No ext_infos")
   534  		}
   535  
   536  		errs := map[string]error{
   537  			"err_ambiguous":         errAmbiguousRelocation,
   538  			"err_ambiguous_flavour": errAmbiguousRelocation,
   539  		}
   540  
   541  		for section := range extInfos.funcInfos {
   542  			name := strings.TrimPrefix(section, "socket_filter/")
   543  			t.Run(name, func(t *testing.T) {
   544  				var relos []*CORERelocation
   545  				for _, reloInfo := range extInfos.relocationInfos[section] {
   546  					relos = append(relos, reloInfo.relo)
   547  				}
   548  
   549  				fixups, err := CORERelocate(spec, spec, relos)
   550  				if want := errs[name]; want != nil {
   551  					if !errors.Is(err, want) {
   552  						t.Fatal("Expected", want, "got", err)
   553  					}
   554  					return
   555  				}
   556  
   557  				if err != nil {
   558  					t.Fatal("Can't relocate against itself:", err)
   559  				}
   560  
   561  				for offset, fixup := range fixups {
   562  					if want := fixup.local; !fixup.skipLocalValidation && want != fixup.target {
   563  						// Since we're relocating against ourselves both values
   564  						// should match.
   565  						t.Errorf("offset %d: local %v doesn't match target %d (kind %s)", offset, fixup.local, fixup.target, fixup.kind)
   566  					}
   567  				}
   568  			})
   569  		}
   570  	})
   571  }
   572  
   573  func TestCORECopyWithoutQualifiers(t *testing.T) {
   574  	qualifiers := []struct {
   575  		name string
   576  		fn   func(Type) Type
   577  	}{
   578  		{"const", func(t Type) Type { return &Const{Type: t} }},
   579  		{"volatile", func(t Type) Type { return &Volatile{Type: t} }},
   580  		{"restrict", func(t Type) Type { return &Restrict{Type: t} }},
   581  		{"typedef", func(t Type) Type { return &Typedef{Type: t} }},
   582  	}
   583  
   584  	for _, test := range qualifiers {
   585  		t.Run(test.name+" cycle", func(t *testing.T) {
   586  			root := &Volatile{}
   587  			root.Type = test.fn(root)
   588  
   589  			cycle, ok := Copy(root, UnderlyingType).(*cycle)
   590  			qt.Assert(t, ok, qt.IsTrue)
   591  			qt.Assert(t, cycle.root, qt.Equals, root)
   592  		})
   593  	}
   594  
   595  	for _, a := range qualifiers {
   596  		for _, b := range qualifiers {
   597  			t.Run(a.name+" "+b.name, func(t *testing.T) {
   598  				v := a.fn(&Pointer{Target: b.fn(&Int{Name: "z"})})
   599  				want := &Pointer{Target: &Int{Name: "z"}}
   600  
   601  				got := Copy(v, UnderlyingType)
   602  				qt.Assert(t, got, qt.DeepEquals, want)
   603  			})
   604  		}
   605  	}
   606  
   607  	t.Run("long chain", func(t *testing.T) {
   608  		root := &Int{Name: "abc"}
   609  		v := Type(root)
   610  		for i := 0; i < maxTypeDepth; i++ {
   611  			q := qualifiers[rand.Intn(len(qualifiers))]
   612  			v = q.fn(v)
   613  			t.Log(q.name)
   614  		}
   615  
   616  		got := Copy(v, UnderlyingType)
   617  		qt.Assert(t, got, qt.DeepEquals, root)
   618  	})
   619  }
   620  

View as plain text