sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/genyaml/genyaml_test.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package genyaml
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"os"
    23  	"path/filepath"
    24  	"strings"
    25  	"testing"
    26  
    27  	yaml3 "gopkg.in/yaml.v3"
    28  
    29  	simplealiases "sigs.k8s.io/prow/pkg/genyaml/testdata/alias_simple_types"
    30  	aliases "sigs.k8s.io/prow/pkg/genyaml/testdata/alias_types"
    31  	embedded "sigs.k8s.io/prow/pkg/genyaml/testdata/embedded_structs"
    32  	inlines "sigs.k8s.io/prow/pkg/genyaml/testdata/inline_structs"
    33  	interfaces "sigs.k8s.io/prow/pkg/genyaml/testdata/interface_types"
    34  	multiline "sigs.k8s.io/prow/pkg/genyaml/testdata/multiline_comments"
    35  	nested "sigs.k8s.io/prow/pkg/genyaml/testdata/nested_structs"
    36  	tags "sigs.k8s.io/prow/pkg/genyaml/testdata/no_tags"
    37  	omit "sigs.k8s.io/prow/pkg/genyaml/testdata/omit_if_empty"
    38  	pointers "sigs.k8s.io/prow/pkg/genyaml/testdata/pointer_types"
    39  	primitives "sigs.k8s.io/prow/pkg/genyaml/testdata/primitive_types"
    40  	private "sigs.k8s.io/prow/pkg/genyaml/testdata/private_members"
    41  	sequence "sigs.k8s.io/prow/pkg/genyaml/testdata/sequence_items"
    42  )
    43  
    44  const (
    45  	testDir = "testdata"
    46  )
    47  
    48  func resolvePath(t *testing.T, filename string) string {
    49  	name := filepath.Base(t.Name())
    50  	return strings.ToLower(filepath.Join(testDir, name, filename))
    51  }
    52  
    53  func fileName(t *testing.T, extension string) string {
    54  	name := filepath.Base(t.Name())
    55  	return strings.ToLower(filepath.Join(testDir, name, name+"."+extension))
    56  }
    57  
    58  func readFile(t *testing.T, extension string) []byte {
    59  	data, err := os.ReadFile(fileName(t, extension))
    60  	if err != nil {
    61  		t.Errorf("Failed reading .%s file: %v.", extension, err)
    62  	}
    63  
    64  	return data
    65  }
    66  
    67  func TestFmtRawDoc(t *testing.T) {
    68  	tests := []struct {
    69  		name     string
    70  		rawDoc   string
    71  		expected string
    72  	}{
    73  		{
    74  			name:     "Single line comment",
    75  			rawDoc:   "Owners of the cat.",
    76  			expected: "Owners of the cat.",
    77  		},
    78  		{
    79  			name:   "Multi line comment",
    80  			rawDoc: "StringField comment\nsecond line\nthird line",
    81  			expected: `StringField comment
    82  second line
    83  third line`,
    84  		},
    85  		{
    86  			name:     "Delete trailing space(s)",
    87  			rawDoc:   "Some comment    ",
    88  			expected: "Some comment",
    89  		},
    90  		{
    91  			name:     "Delete trailing newline(s)",
    92  			rawDoc:   "Some comment\n\n\n\n",
    93  			expected: "Some comment",
    94  		},
    95  		{
    96  			name:     "Escape double quote(s)",
    97  			rawDoc:   `"Some comment"`,
    98  			expected: `"Some comment"`,
    99  		},
   100  		{
   101  			name:     "Convert tab to space",
   102  			rawDoc:   "tab	tab		tabtab",
   103  			expected: "tab tab tabtab",
   104  		},
   105  		{
   106  			name:     "Strip TODO prefixed comment",
   107  			rawDoc:   "TODO: some future work",
   108  			expected: "",
   109  		},
   110  		{
   111  			name:     "Strip + prefixed comment",
   112  			rawDoc:   "+: some future work",
   113  			expected: "",
   114  		},
   115  		{
   116  			name:     "Strip TODO prefixed comment from multi line comment",
   117  			rawDoc:   "TODO: some future work\nmore comment",
   118  			expected: "more comment",
   119  		},
   120  		{
   121  			name:     "Strip + prefixed comment from multi line comment",
   122  			rawDoc:   "+: some future work\nmore comment",
   123  			expected: "more comment",
   124  		},
   125  	}
   126  
   127  	for _, test := range tests {
   128  		t.Run(test.name, func(t *testing.T) {
   129  			actualFormattedRawDoc := fmtRawDoc(test.rawDoc)
   130  
   131  			if actualFormattedRawDoc != test.expected {
   132  				t.Fatalf("Expected %q, but got result %q", test.expected, actualFormattedRawDoc)
   133  			}
   134  		})
   135  	}
   136  }
   137  
   138  func TestInjectComment(t *testing.T) {
   139  	tests := []struct {
   140  		name         string
   141  		typeSpec     string
   142  		actualNode   *yaml3.Node
   143  		expectedNode *yaml3.Node
   144  	}{
   145  		{
   146  			name:     "Inject comments",
   147  			typeSpec: "ExampleStruct",
   148  			actualNode: &yaml3.Node{
   149  				Kind: yaml3.DocumentNode,
   150  				Content: []*yaml3.Node{
   151  					{
   152  						Kind: yaml3.MappingNode,
   153  						Tag:  "!!map",
   154  						Content: []*yaml3.Node{
   155  							{
   156  								Kind:  yaml3.ScalarNode,
   157  								Tag:   "!!str",
   158  								Value: "exampleKey",
   159  							},
   160  							{
   161  								Kind:  yaml3.ScalarNode,
   162  								Tag:   "!!bool",
   163  								Value: "true",
   164  							},
   165  						},
   166  					},
   167  				},
   168  			},
   169  			expectedNode: &yaml3.Node{
   170  				Kind: yaml3.DocumentNode,
   171  				Content: []*yaml3.Node{
   172  					{
   173  						Kind: yaml3.MappingNode,
   174  						Tag:  "!!map",
   175  						Content: []*yaml3.Node{
   176  							{
   177  								Kind:        yaml3.ScalarNode,
   178  								Tag:         "!!str",
   179  								Value:       "exampleKey",
   180  								HeadComment: "Some comment",
   181  							},
   182  							{
   183  								Kind:  yaml3.ScalarNode,
   184  								Tag:   "!!bool",
   185  								Value: "true",
   186  							},
   187  						},
   188  					},
   189  				},
   190  			},
   191  		},
   192  	}
   193  
   194  	for _, test := range tests {
   195  		t.Run(test.name, func(t *testing.T) {
   196  			cm, err := NewCommentMap(nil)
   197  			if err != nil {
   198  				t.Fatalf("Failed to construct comment map: %v", err)
   199  			}
   200  
   201  			if err := json.Unmarshal(readFile(t, "json"), &cm.comments); err != nil {
   202  				t.Errorf("Unexpected error unmarshalling JSON to comments: %v.", err)
   203  			}
   204  
   205  			cm.injectComment(test.actualNode, []string{test.typeSpec}, 0)
   206  
   207  			expectedYaml, err := yaml3.Marshal(test.expectedNode)
   208  			if err != nil {
   209  				t.Errorf("Unexpected error marshalling Node to YAML: %v.", err)
   210  			}
   211  
   212  			actualYaml, err := yaml3.Marshal(test.actualNode)
   213  			if err != nil {
   214  				t.Errorf("Unexpected error marshalling Node to YAML: %v.", err)
   215  			}
   216  
   217  			if !bytes.Equal(expectedYaml, actualYaml) {
   218  				t.Error("Expected yaml snippets to not be equal.")
   219  			}
   220  		})
   221  	}
   222  }
   223  
   224  func TestAddPath(t *testing.T) {
   225  	tests := []struct {
   226  		name     string
   227  		paths    []string
   228  		expected bool
   229  	}{
   230  		{
   231  			name:     "Single path",
   232  			paths:    []string{"example_config.go"},
   233  			expected: true,
   234  		},
   235  		{
   236  			name:     "Multiple paths",
   237  			paths:    []string{"example_config1.go", "example_config2.go"},
   238  			expected: true,
   239  		},
   240  	}
   241  
   242  	for _, test := range tests {
   243  		t.Run(test.name, func(t *testing.T) {
   244  			var resolved []string
   245  			for _, file := range test.paths {
   246  				resolved = append(resolved, resolvePath(t, file))
   247  			}
   248  			cm, err := NewCommentMap(nil, resolved...)
   249  			if err != nil {
   250  				t.Fatalf("failed to construct comment map: %v", err)
   251  			}
   252  
   253  			expectedComments := readFile(t, "json")
   254  			actualComments, err := json.MarshalIndent(cm.comments, "", "  ")
   255  
   256  			if err != nil {
   257  				t.Errorf("Unexpected error generating JSON from comments: %v.", err)
   258  			}
   259  
   260  			equal := bytes.Equal(expectedComments, actualComments)
   261  
   262  			if equal != test.expected {
   263  				t.Errorf("Expected comments equality to be: %t.", test.expected)
   264  			}
   265  		})
   266  	}
   267  
   268  }
   269  
   270  func TestGenYAML(t *testing.T) {
   271  	tests := []struct {
   272  		name            string
   273  		paths           []string
   274  		rawContents     map[string][]byte
   275  		structObj       interface{}
   276  		expectedRawYaml []byte
   277  		expected        bool
   278  	}{
   279  		{
   280  			name:  "alias types",
   281  			paths: []string{"example_config.go"},
   282  			structObj: &aliases.Alias{
   283  				StringField: "string",
   284  			},
   285  			expected: true,
   286  		},
   287  		{
   288  			name: "also-read-raw",
   289  			rawContents: map[string][]byte{
   290  				"alias_types.yaml": []byte(`package alias_types
   291  type Alias = AliasedType
   292  type AliasedType struct {
   293    // StringField comment
   294    StringField string ` + "`json:\"string\"`" + `
   295  }`),
   296  			},
   297  			structObj: &aliases.Alias{
   298  				StringField: "string",
   299  			},
   300  			expectedRawYaml: []byte(`# StringField comment
   301  string: string
   302  `),
   303  			expected: true,
   304  		},
   305  		{
   306  			name:  "alias simple types",
   307  			paths: []string{"example_config.go"},
   308  			structObj: &simplealiases.SimpleAliases{
   309  				AliasField: simplealiases.Alias("string"),
   310  			},
   311  			expected: true,
   312  		},
   313  		{
   314  			name:  "primitive types",
   315  			paths: []string{"example_config.go"},
   316  			structObj: &primitives.Primitives{
   317  				StringField:  "string",
   318  				BooleanField: true,
   319  				IntegerField: 1,
   320  			},
   321  			expected: true,
   322  		},
   323  		{
   324  			name:  "multiline comments",
   325  			paths: []string{"example_config.go"},
   326  			structObj: &multiline.Multiline{
   327  				StringField1: "string1",
   328  				StringField2: "string2",
   329  				StringField3: "string3",
   330  				StringField4: "string4",
   331  				StringField5: "string5",
   332  				StringField6: "string6",
   333  			},
   334  			expected: true,
   335  		},
   336  		{
   337  			name:  "nested structs",
   338  			paths: []string{"example_config.go"},
   339  			structObj: &nested.Parent{
   340  				Age: 35,
   341  				Children: []nested.Child{
   342  					{Name: "Jimbo", Age: 4},
   343  					{Name: "Jenny", Age: 5},
   344  				},
   345  				Name: "Mildred",
   346  			},
   347  			expected: true,
   348  		},
   349  		{
   350  			name:  "inline structs",
   351  			paths: []string{"example_config.go"},
   352  			structObj: &inlines.Resource{
   353  				Metadata: inlines.Metadata{
   354  					Name: "test",
   355  				},
   356  			},
   357  			expected: true,
   358  		},
   359  		{
   360  			name:  "embedded structs",
   361  			paths: []string{"example_config.go"},
   362  			structObj: &embedded.Building{
   363  				Address:  "123 North Main Street",
   364  				Bathroom: embedded.Bathroom{Width: 100, Height: 200},
   365  				Bedroom:  embedded.Bedroom{Width: 100, Height: 200},
   366  			},
   367  			expected: true,
   368  		},
   369  		{
   370  			name:  "no tags",
   371  			paths: []string{"example_config.go"},
   372  			structObj: &tags.Tagless{
   373  				StringField:  "string",
   374  				BooleanField: true,
   375  				IntegerField: 1,
   376  			},
   377  			expected: true,
   378  		},
   379  		{
   380  			name:  "omit if empty",
   381  			paths: []string{"example_config.go"},
   382  			structObj: &omit.OmitEmptyStrings{
   383  				StringFieldOmitEmpty: "",
   384  				StringFieldKeepEmpty: "",
   385  				BooleanField:         true,
   386  				IntegerField:         1,
   387  			},
   388  			expected: true,
   389  		},
   390  		{
   391  			name:  "pointer types",
   392  			paths: []string{"example_config.go"},
   393  			structObj: &pointers.Zoo{
   394  				Employees: []*pointers.Employee{
   395  					{
   396  						Name: "Jim",
   397  						Age:  22,
   398  					},
   399  					{
   400  						Name: "Jane",
   401  						Age:  21,
   402  					},
   403  				},
   404  			},
   405  			expected: true,
   406  		},
   407  		{
   408  			name:      "private members",
   409  			paths:     []string{"example_config.go"},
   410  			structObj: private.NewPerson("gamer123", "password123"),
   411  			expected:  true,
   412  		},
   413  		{
   414  			name:  "sequence items",
   415  			paths: []string{"example_config.go"},
   416  			structObj: &sequence.Recipe{
   417  				Ingredients: []sequence.Ingredient{
   418  					{
   419  						Name:   "potatoes",
   420  						Amount: 1,
   421  					},
   422  					{
   423  						Name:   "eggs",
   424  						Amount: 2,
   425  					},
   426  				},
   427  			},
   428  			expected: true,
   429  		},
   430  		{
   431  			name:  "interface types",
   432  			paths: []string{"example_config.go"},
   433  			structObj: &interfaces.Zoo{
   434  				Animals: []interfaces.Animal{
   435  					&interfaces.Lion{
   436  						Name: "Leo",
   437  					},
   438  					&interfaces.Cheetah{
   439  						Name: "Charles",
   440  					},
   441  				},
   442  			},
   443  			// INFO: Interface type comments are not implemented.
   444  			expected: false,
   445  		},
   446  	}
   447  
   448  	for _, test := range tests {
   449  		t.Run(test.name, func(t *testing.T) {
   450  			var paths []string
   451  			for _, path := range test.paths {
   452  				paths = append(paths, resolvePath(t, path))
   453  			}
   454  			cm, err := NewCommentMap(test.rawContents, paths...)
   455  			if err != nil {
   456  				t.Fatalf("failed to construct comment map: %v", err)
   457  			}
   458  			expectedYaml := test.expectedRawYaml
   459  			if len(expectedYaml) == 0 {
   460  				expectedYaml = readFile(t, "yaml")
   461  			}
   462  
   463  			actualYaml, err := cm.GenYaml(test.structObj)
   464  
   465  			if err != nil {
   466  				t.Errorf("Unexpected error generating YAML from struct: %v.", err)
   467  			}
   468  
   469  			equal := bytes.Equal(expectedYaml, []byte(actualYaml))
   470  
   471  			if equal != test.expected {
   472  				t.Errorf("Expected yaml snippets equality to be: %t.", test.expected)
   473  			}
   474  		})
   475  	}
   476  }