github.com/google/yamlfmt@v0.12.2-0.20240514121411-7f77800e2681/path_collector_test.go (about)

     1  // Copyright 2024 Google LLC
     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 yamlfmt_test
    16  
    17  import (
    18  	"fmt"
    19  	"os"
    20  	"path/filepath"
    21  	"strings"
    22  	"testing"
    23  
    24  	"github.com/google/yamlfmt"
    25  	"github.com/google/yamlfmt/internal/collections"
    26  	"github.com/google/yamlfmt/internal/tempfile"
    27  )
    28  
    29  func TestFilepathCollector(t *testing.T) {
    30  	testCaseTable{
    31  		{
    32  			name: "finds direct paths",
    33  			files: []tempfile.Path{
    34  				{FileName: "x.yaml"},
    35  				{FileName: "y.yaml"},
    36  				{FileName: "z.yml"},
    37  			},
    38  			includePatterns: testPatterns{
    39  				{pattern: "x.yaml"},
    40  				{pattern: "y.yaml"},
    41  				{pattern: "z.yml"},
    42  			},
    43  			expectedFiles: collections.Set[string]{
    44  				"x.yaml": {},
    45  				"y.yaml": {},
    46  				"z.yml":  {},
    47  			},
    48  		},
    49  		{
    50  			name: "finds all in directory one layer",
    51  			files: []tempfile.Path{
    52  				{FileName: "a", IsDir: true},
    53  				{FileName: "a/x.yaml"},
    54  				{FileName: "a/y.yaml"},
    55  				{FileName: "a/z.yml"},
    56  			},
    57  			includePatterns: testPatterns{
    58  				{pattern: "a"},
    59  			},
    60  			extensions: []string{
    61  				"yaml",
    62  				"yml",
    63  			},
    64  			expectedFiles: collections.Set[string]{
    65  				"a/x.yaml": {},
    66  				"a/y.yaml": {},
    67  				"a/z.yml":  {},
    68  			},
    69  		},
    70  		{
    71  			name: "finds direct path to subdirectory",
    72  			files: []tempfile.Path{
    73  				{FileName: "a", IsDir: true},
    74  				{FileName: "a/x.yaml"},
    75  				{FileName: "a/y.yaml"},
    76  				{FileName: "a/z.yml"},
    77  			},
    78  			includePatterns: testPatterns{
    79  				{pattern: "a/x.yaml"},
    80  				{pattern: "a/z.yml"},
    81  			},
    82  			expectedFiles: collections.Set[string]{
    83  				"a/x.yaml": {},
    84  				"a/z.yml":  {},
    85  			},
    86  		},
    87  		{
    88  			name: "finds all in layered directories",
    89  			files: []tempfile.Path{
    90  				{FileName: "a", IsDir: true},
    91  				{FileName: "a/b", IsDir: true},
    92  				{FileName: "x.yml"},
    93  				{FileName: "y.yml"},
    94  				{FileName: "z.yaml"},
    95  				{FileName: "a/x.yaml"},
    96  				{FileName: "a/b/x.yaml"},
    97  				{FileName: "a/b/y.yml"},
    98  			},
    99  			includePatterns: testPatterns{
   100  				{pattern: ""}, // with the test this functionally means the whole temp dir
   101  			},
   102  			extensions: []string{
   103  				"yaml",
   104  				"yml",
   105  			},
   106  			expectedFiles: collections.Set[string]{
   107  				"x.yml":      {},
   108  				"y.yml":      {},
   109  				"z.yaml":     {},
   110  				"a/x.yaml":   {},
   111  				"a/b/x.yaml": {},
   112  				"a/b/y.yml":  {},
   113  			},
   114  		},
   115  		{
   116  			name: "exclude files",
   117  			files: []tempfile.Path{
   118  				{FileName: "a", IsDir: true},
   119  				{FileName: "a/b", IsDir: true},
   120  				{FileName: "x.yml"},
   121  				{FileName: "y.yml"},
   122  				{FileName: "z.yaml"},
   123  				{FileName: "a/x.yaml"},
   124  				{FileName: "a/b/x.yaml"},
   125  				{FileName: "a/b/y.yml"},
   126  			},
   127  			includePatterns: testPatterns{
   128  				{pattern: ""}, // with the test this functionally means the whole temp dir
   129  			},
   130  			excludePatterns: testPatterns{
   131  				{pattern: "x.yml"},
   132  				{pattern: "a/x.yaml"},
   133  			},
   134  			extensions: []string{
   135  				"yaml",
   136  				"yml",
   137  			},
   138  			expectedFiles: collections.Set[string]{
   139  				"y.yml":      {},
   140  				"z.yaml":     {},
   141  				"a/b/x.yaml": {},
   142  				"a/b/y.yml":  {},
   143  			},
   144  		},
   145  		{
   146  			name:            "exclude directory",
   147  			changeToTempDir: true,
   148  			files: []tempfile.Path{
   149  				{FileName: "x.yml"},
   150  				{FileName: "y.yml"},
   151  				{FileName: "z.yaml"},
   152  
   153  				{FileName: "a", IsDir: true},
   154  				{FileName: "a/x.yaml"},
   155  
   156  				{FileName: "a/b", IsDir: true},
   157  				{FileName: "a/b/x.yaml"},
   158  				{FileName: "a/b/y.yml"},
   159  			},
   160  			includePatterns: testPatterns{
   161  				{pattern: ""}, // with the test this functionally means the whole temp dir
   162  			},
   163  			excludePatterns: testPatterns{
   164  				{pattern: "a/b"},
   165  			},
   166  			extensions: []string{
   167  				"yaml",
   168  				"yml",
   169  			},
   170  			expectedFiles: collections.Set[string]{
   171  				"x.yml":    {},
   172  				"y.yml":    {},
   173  				"z.yaml":   {},
   174  				"a/x.yaml": {},
   175  			},
   176  		},
   177  		{
   178  			name: "don't get files with wrong extension",
   179  			files: []tempfile.Path{
   180  				{FileName: "x.yml"},
   181  				{FileName: "y.yaml"},
   182  				{FileName: "z.json"},
   183  			},
   184  			includePatterns: testPatterns{
   185  				{pattern: ""}, // with the test this functionally means the whole temp dir
   186  			},
   187  			extensions: []string{
   188  				"yaml",
   189  				"yml",
   190  			},
   191  			expectedFiles: collections.Set[string]{
   192  				"x.yml":  {},
   193  				"y.yaml": {},
   194  			},
   195  		},
   196  	}.runAll(t, useFilepathCollector)
   197  }
   198  
   199  func TestDoublestarCollectorBasic(t *testing.T) {
   200  	testCaseTable{
   201  		{
   202  			name: "no excludes",
   203  			files: []tempfile.Path{
   204  				{FileName: "x.yaml"},
   205  				{FileName: "y.yaml"},
   206  				{FileName: "z.yaml"},
   207  			},
   208  			includePatterns: testPatterns{
   209  				{pattern: "**/*.yaml"},
   210  			},
   211  			expectedFiles: collections.Set[string]{
   212  				"x.yaml": {},
   213  				"y.yaml": {},
   214  				"z.yaml": {},
   215  			},
   216  		},
   217  	}.runAll(t, useDoublestarCollector)
   218  }
   219  
   220  func TestDoublestarCollectorExcludeDirectory(t *testing.T) {
   221  	testFiles := []tempfile.Path{
   222  		{FileName: "x.yaml"},
   223  
   224  		{FileName: "y", IsDir: true},
   225  		{FileName: "y/y.yaml"},
   226  
   227  		{FileName: "z", IsDir: true},
   228  		{FileName: "z/z.yaml"},
   229  		{FileName: "z/z1.yaml"},
   230  		{FileName: "z/z2.yaml"},
   231  	}
   232  
   233  	testCaseTable{
   234  		{
   235  			name:  "exclude_directory/start with doublestar",
   236  			files: testFiles,
   237  			includePatterns: testPatterns{
   238  				{pattern: "**/*.yaml"},
   239  			},
   240  			excludePatterns: testPatterns{
   241  				{pattern: "**/z/**/*.yaml", stayRelative: true},
   242  			},
   243  			expectedFiles: collections.Set[string]{
   244  				"x.yaml":   {},
   245  				"y/y.yaml": {},
   246  			},
   247  		},
   248  		{
   249  			name:            "exclude_directory/relative include and exclude",
   250  			changeToTempDir: true,
   251  			files:           testFiles,
   252  			includePatterns: testPatterns{
   253  				{pattern: "**/*.yaml", stayRelative: true},
   254  			},
   255  			excludePatterns: testPatterns{
   256  				{pattern: "z/**/*.yaml", stayRelative: true},
   257  			},
   258  			expectedFiles: collections.Set[string]{
   259  				"x.yaml":   {},
   260  				"y/y.yaml": {},
   261  			},
   262  		},
   263  		{
   264  			name:  "exclude_directory/absolute include and exclude",
   265  			files: testFiles,
   266  			includePatterns: testPatterns{
   267  				{pattern: "**/*.yaml"},
   268  			},
   269  			excludePatterns: testPatterns{
   270  				{pattern: "z/**/*.yaml"},
   271  			},
   272  			expectedFiles: collections.Set[string]{
   273  				"x.yaml":   {},
   274  				"y/y.yaml": {},
   275  			},
   276  		},
   277  		{
   278  			name:            "exclude_directory/absolute include relative exclude",
   279  			skip:            true,
   280  			changeToTempDir: true,
   281  			files:           testFiles,
   282  			includePatterns: testPatterns{
   283  				{pattern: "**/*.yaml"},
   284  			},
   285  			excludePatterns: testPatterns{
   286  				{pattern: "z/**/*.yaml", stayRelative: true},
   287  			},
   288  			expectedFiles: collections.Set[string]{
   289  				"x.yaml":   {},
   290  				"y/y.yaml": {},
   291  			},
   292  		},
   293  		{
   294  			name:            "exclude_directory/relative include absolute exclude",
   295  			skip:            true,
   296  			changeToTempDir: true,
   297  			files:           testFiles,
   298  			includePatterns: testPatterns{
   299  				{pattern: "**/*.yaml", stayRelative: true},
   300  			},
   301  			excludePatterns: testPatterns{
   302  				{pattern: "z/**/*.yaml"},
   303  			},
   304  			expectedFiles: collections.Set[string]{
   305  				"x.yaml":   {},
   306  				"y/y.yaml": {},
   307  			},
   308  		},
   309  	}.runAll(t, useDoublestarCollector)
   310  }
   311  
   312  type testPatterns []struct {
   313  	pattern      string
   314  	stayRelative bool
   315  }
   316  
   317  func (tps testPatterns) allPatterns(path string) []string {
   318  	result := make([]string, len(tps))
   319  	for i := 0; i < len(tps); i++ {
   320  		if tps[i].stayRelative {
   321  			result[i] = tps[i].pattern
   322  		} else {
   323  			result[i] = fmt.Sprintf("%s/%s", path, tps[i].pattern)
   324  		}
   325  	}
   326  	return result
   327  }
   328  
   329  // In some test scenarios we want to ignore whether a pattern is marked stayRelative
   330  // and always treat them as relative by formatting the base path on them.
   331  func (tps testPatterns) allPatternsForceAbsolute(path string) []string {
   332  	result := make([]string, len(tps))
   333  	for i := 0; i < len(tps); i++ {
   334  		result[i] = fmt.Sprintf("%s/%s", path, tps[i].pattern)
   335  	}
   336  	return result
   337  }
   338  
   339  type testCase struct {
   340  	name            string
   341  	skip            bool
   342  	changeToTempDir bool
   343  	files           []tempfile.Path
   344  	includePatterns testPatterns
   345  	extensions      []string
   346  	excludePatterns testPatterns
   347  	expectedFiles   collections.Set[string]
   348  }
   349  
   350  func (tc testCase) run(t *testing.T, makeCollector makeCollectorFunc) {
   351  	testStartDir, err := os.Getwd()
   352  	if err != nil {
   353  		t.Fatalf("could not get working directory: %v", err)
   354  	}
   355  	t.Run(tc.name, func(t *testing.T) {
   356  		if tc.skip {
   357  			t.Skip()
   358  		}
   359  		tempPath := t.TempDir()
   360  
   361  		if tc.changeToTempDir {
   362  			os.Chdir(tempPath)
   363  		}
   364  
   365  		for _, file := range tc.files {
   366  			file.BasePath = tempPath
   367  			if err := file.Create(); err != nil {
   368  				t.Fatalf("Failed to create file")
   369  			}
   370  		}
   371  
   372  		collector := makeCollector(tc, tempPath)
   373  		paths, err := collector.CollectPaths()
   374  		if err != nil {
   375  			t.Fatalf("Test case failed: %v", err)
   376  		}
   377  
   378  		filesToFormat := collections.Set[string]{}
   379  		for _, path := range paths {
   380  			formatPath := path
   381  			if strings.HasPrefix(formatPath, "/") {
   382  				formatPath, err = filepath.Rel(tempPath, path)
   383  				if err != nil {
   384  					t.Fatalf("Path %s could not match to path %s", tempPath, path)
   385  				}
   386  			}
   387  			filesToFormat.Add(formatPath)
   388  		}
   389  		if !filesToFormat.Equals(tc.expectedFiles) {
   390  			t.Fatalf("Expected to receive paths %v\nbut got %v", tc.expectedFiles, filesToFormat)
   391  		}
   392  	})
   393  
   394  	// Restore the starting directory if we changed in the test.
   395  	if tc.changeToTempDir {
   396  		os.Chdir(testStartDir)
   397  	}
   398  }
   399  
   400  type testCaseTable []testCase
   401  
   402  func (tcs testCaseTable) runAll(t *testing.T, makeCollector makeCollectorFunc) {
   403  	for _, tc := range tcs {
   404  		tc.run(t, makeCollector)
   405  	}
   406  }
   407  
   408  type makeCollectorFunc func(tc testCase, path string) yamlfmt.PathCollector
   409  
   410  func useFilepathCollector(tc testCase, path string) yamlfmt.PathCollector {
   411  	return &yamlfmt.FilepathCollector{
   412  		Include:    tc.includePatterns.allPatterns(path),
   413  		Exclude:    tc.excludePatterns.allPatterns(path),
   414  		Extensions: tc.extensions,
   415  	}
   416  }
   417  
   418  func useDoublestarCollector(tc testCase, path string) yamlfmt.PathCollector {
   419  	var includePatterns []string
   420  	if tc.changeToTempDir {
   421  		includePatterns = tc.includePatterns.allPatterns(path)
   422  	} else {
   423  		// If we didn't change to temp dir, disallow relative paths so we don't pick up
   424  		// something confusing from the main working directory.
   425  		includePatterns = tc.includePatterns.allPatternsForceAbsolute(path)
   426  	}
   427  	return &yamlfmt.DoublestarCollector{
   428  		Include: includePatterns,
   429  		Exclude: tc.excludePatterns.allPatterns(path),
   430  	}
   431  }