github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/find_test.go (about)

     1  // Copyright (c) 2015-2022 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"context"
    22  	"os/exec"
    23  	"regexp"
    24  	"runtime"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  )
    29  
    30  // Tests match find function with all supported inputs on
    31  // file pattern, size and time.
    32  func TestMatchFind(t *testing.T) {
    33  	// List of various contexts used in each tests,
    34  	// tests are run in the same order as this list.
    35  	listFindContexts := []*findContext{
    36  		{
    37  			clnt: &S3Client{
    38  				targetURL: &ClientURL{},
    39  			},
    40  			ignorePattern: "*.go",
    41  		},
    42  		{
    43  			clnt: &S3Client{
    44  				targetURL: &ClientURL{},
    45  			},
    46  			namePattern: "console",
    47  		},
    48  		{
    49  			clnt: &S3Client{
    50  				targetURL: &ClientURL{},
    51  			},
    52  			pathPattern: "*console*",
    53  		},
    54  		{
    55  			clnt: &S3Client{
    56  				targetURL: &ClientURL{},
    57  			},
    58  			regexPattern: regexp.MustCompile(`^(\d+\.){3}\d+$`),
    59  		},
    60  		{
    61  			clnt: &S3Client{
    62  				targetURL: &ClientURL{},
    63  			},
    64  			olderThan: "1d",
    65  		},
    66  		{
    67  			clnt: &S3Client{
    68  				targetURL: &ClientURL{},
    69  			},
    70  			newerThan: "32000d",
    71  		},
    72  		{
    73  			clnt: &S3Client{
    74  				targetURL: &ClientURL{},
    75  			},
    76  			largerSize: 1024 * 1024,
    77  		},
    78  		{
    79  			clnt: &S3Client{
    80  				targetURL: &ClientURL{},
    81  			},
    82  			smallerSize: 1024,
    83  		},
    84  		{
    85  			clnt: &S3Client{
    86  				targetURL: &ClientURL{},
    87  			},
    88  			ignorePattern: "*.txt",
    89  		},
    90  		{
    91  			clnt: &S3Client{
    92  				targetURL: &ClientURL{},
    93  			},
    94  		},
    95  	}
    96  
    97  	testCases := []struct {
    98  		content       contentMessage
    99  		expectedMatch bool
   100  	}{
   101  		// Matches ignore pattern, so match will be false - Test 1.
   102  		{
   103  			content: contentMessage{
   104  				Key: "pkg/console/console.go",
   105  			},
   106  			expectedMatch: false,
   107  		},
   108  		// Matches name pattern - Test 2.
   109  		{
   110  			content: contentMessage{
   111  				Key: "pkg/console/console.go",
   112  			},
   113  			expectedMatch: true,
   114  		},
   115  		// Matches path pattern - Test 3.
   116  		{
   117  			content: contentMessage{
   118  				Key: "pkg/console/console.go",
   119  			},
   120  			expectedMatch: true,
   121  		},
   122  		// Matches regex pattern - Test 4.
   123  		{
   124  			content: contentMessage{
   125  				Key: "192.168.1.1",
   126  			},
   127  			expectedMatch: true,
   128  		},
   129  		// Matches older than time - Test 5.
   130  		{
   131  			content: contentMessage{
   132  				Time: time.Unix(11999, 0).UTC(),
   133  			},
   134  			expectedMatch: true,
   135  		},
   136  		// Matches newer than time - Test 6.
   137  		{
   138  			content: contentMessage{
   139  				Time: time.Unix(12001, 0).UTC(),
   140  			},
   141  			expectedMatch: true,
   142  		},
   143  		// Matches size larger - Test 7.
   144  		{
   145  			content: contentMessage{
   146  				Size: 1024 * 1024 * 2,
   147  			},
   148  			expectedMatch: true,
   149  		},
   150  		// Matches size smaller - Test 8.
   151  		{
   152  			content: contentMessage{
   153  				Size: 1023,
   154  			},
   155  			expectedMatch: true,
   156  		},
   157  		// Does not match ignore pattern, so match will be true - Test 9.
   158  		{
   159  			content: contentMessage{
   160  				Key: "pkg/console/console.go",
   161  			},
   162  			expectedMatch: true,
   163  		},
   164  		// No matching inputs were provided, so nothing to match return value is true - Test 10.
   165  		{
   166  			content:       contentMessage{},
   167  			expectedMatch: true,
   168  		},
   169  	}
   170  
   171  	// Runs all the test cases and validate the expected conditions.
   172  	for i, testCase := range testCases {
   173  		gotMatch := matchFind(listFindContexts[i], testCase.content)
   174  		if testCase.expectedMatch != gotMatch {
   175  			t.Errorf("Test: %d, expected match %t, got %t", i+1, testCase.expectedMatch, gotMatch)
   176  		}
   177  	}
   178  }
   179  
   180  // Tests suffix strings trimmed off correctly at maxdepth.
   181  func TestSuffixTrimmingAtMaxDepth(t *testing.T) {
   182  	testCases := []struct {
   183  		startPrefix     string
   184  		path            string
   185  		separator       string
   186  		maxDepth        uint
   187  		expectedNewPath string
   188  	}{
   189  		// Tests at max depth 0.
   190  		{
   191  			startPrefix:     "./",
   192  			path:            ".git/refs/remotes",
   193  			separator:       "/",
   194  			maxDepth:        0,
   195  			expectedNewPath: ".git/refs/remotes",
   196  		},
   197  		// Tests at max depth 1.
   198  		{
   199  			startPrefix:     "./",
   200  			path:            ".git/refs/remotes",
   201  			separator:       "/",
   202  			maxDepth:        1,
   203  			expectedNewPath: "./.git/",
   204  		},
   205  		// Tests at max depth 2.
   206  		{
   207  			startPrefix:     "./",
   208  			path:            ".git/refs/remotes",
   209  			separator:       "/",
   210  			maxDepth:        2,
   211  			expectedNewPath: "./.git/refs/",
   212  		},
   213  		// Tests at max depth 3.
   214  		{
   215  			startPrefix:     "./",
   216  			path:            ".git/refs/remotes",
   217  			separator:       "/",
   218  			maxDepth:        3,
   219  			expectedNewPath: "./.git/refs/remotes",
   220  		},
   221  		// Tests with startPrefix empty.
   222  		{
   223  			startPrefix:     "",
   224  			path:            ".git/refs/remotes",
   225  			separator:       "/",
   226  			maxDepth:        2,
   227  			expectedNewPath: ".git/refs/",
   228  		},
   229  		// Tests with separator empty.
   230  		{
   231  			startPrefix:     "",
   232  			path:            ".git/refs/remotes",
   233  			separator:       "",
   234  			maxDepth:        2,
   235  			expectedNewPath: ".g",
   236  		},
   237  		// Tests with nested startPrefix paths - 1.
   238  		{
   239  			startPrefix:     ".git/refs/",
   240  			path:            ".git/refs/remotes",
   241  			separator:       "/",
   242  			maxDepth:        1,
   243  			expectedNewPath: ".git/refs/remotes",
   244  		},
   245  		// Tests with nested startPrefix paths - 2.
   246  		{
   247  			startPrefix:     ".git/refs",
   248  			path:            ".git/refs/remotes",
   249  			separator:       "/",
   250  			maxDepth:        1,
   251  			expectedNewPath: ".git/refs/",
   252  		},
   253  	}
   254  
   255  	// Run all the test cases and validate for returned new path.
   256  	for i, testCase := range testCases {
   257  		gotNewPath := trimSuffixAtMaxDepth(testCase.startPrefix, testCase.path, testCase.separator, testCase.maxDepth)
   258  		if testCase.expectedNewPath != gotNewPath {
   259  			t.Errorf("Test: %d, expected path %s, got %s", i+1, testCase.expectedNewPath, gotNewPath)
   260  		}
   261  	}
   262  }
   263  
   264  // Tests matching functions for name, path and regex.
   265  func TestFindMatch(t *testing.T) {
   266  	// testFind is the structure used to contain params pertinent to find related tests
   267  	type testFind struct {
   268  		pattern, filePath, flagName string
   269  		match                       bool
   270  	}
   271  
   272  	basicTests := []testFind{
   273  		// Name match tests - success cases.
   274  		{"*.jpg", "carter.jpg", "name", true},
   275  		{"console", "pkg/console/console.go", "name", true},
   276  		{"console.go", "pkg/console/console.go", "name", true},
   277  		{"*XA==", "I/enjoy/morning/walks/XA==", "name ", true},
   278  		{"*parser", "/This/might/mess up./the/parser", "name", true},
   279  		{"*LTIxNDc0ODM2NDgvLTE=", "What/A/Naughty/String/LTIxNDc0ODM2NDgvLTE=", "name", true},
   280  		{"*", "/bla/bla/bla/ ", "name", true},
   281  
   282  		// Name match tests - failure cases.
   283  		{"*.jpg", "carter.jpeg", "name", false},
   284  		{"*/test/*", "/test/bob/likes/cake", "name", false},
   285  		{"*test/*", "bob/test/likes/cake", "name", false},
   286  		{"*test/*", "bob/likes/test/cake", "name", false},
   287  		{"*/test/*", "bob/likes/cake/test", "name", false},
   288  		{"*.jpg", ".jpg/elves/are/evil", "name", false},
   289  		{
   290  			"wq3YgNiB2ILYg9iE2IXYnNud3I/hoI7igIvigIzigI3igI7igI/igKrigKvigKzigK3igK7igaDi",
   291  			"An/Even/Bigger/String/wq3YgNiB2ILYg9iE2IXYnNud3I/hoI7igIvigIzigI3igI7igI/igKrigKvigKzigK3igK7igaDi", "name", false,
   292  		},
   293  		{"๐•ฟ๐–๐–Š", "well/this/isAN/odd/font/THE", "name", false},
   294  		{"๐•ฟ๐–๐–Š", "well/this/isAN/odd/font/The", "name", false},
   295  		{"๐•ฟ๐–๐–Š", "well/this/isAN/odd/font/๐“ฃ๐“ฑ๐“ฎ", "name", false},
   296  		{"๐•ฟ๐–๐–Š", "what/a/strange/turn/of/events/๐“ฃhe", "name", false},
   297  		{"๐•ฟ๐–๐–Š", "well/this/isAN/odd/font/๐•ฟ๐–๐–Š", "name", true},
   298  
   299  		// Path match tests - success cases.
   300  		{"*/test/*", "bob/test/likes/cake", "path", true},
   301  		{"*/test/*", "/test/bob/likes/cake", "path", true},
   302  
   303  		// Path match tests - failure cases.
   304  		{"*.jpg", ".jpg/elves/are/evil", "path", false},
   305  		{"*/test/*", "test1/test2/test3/test", "path", false},
   306  		{"*/ test /*", "test/test1/test2/test3/test", "path", false},
   307  		{"*/test/*", " test /I/have/Really/Long/hair", "path", false},
   308  		{"*XA==", "XA==/Height/is/a/social/construct", "path", false},
   309  		{"*W", "/Word//this/is a/trickyTest", "path", false},
   310  		{"LTIxNDc0ODM2NDgvLTE=", "LTIxNDc0ODM2NDgvLTE=/I/Am/One/Baaaaad/String", "path", false},
   311  		{"/", "funky/path/name", "path", false},
   312  	}
   313  
   314  	for _, test := range basicTests {
   315  		switch test.flagName {
   316  		case "name":
   317  			testMatch := nameMatch(test.pattern, test.filePath)
   318  			if testMatch != test.match {
   319  				t.Fatalf("Unexpected result %t, with pattern %s, flag %s  and filepath %s \n",
   320  					!test.match, test.pattern, test.flagName, test.filePath)
   321  			}
   322  		case "path":
   323  			testMatch := pathMatch(test.pattern, test.filePath)
   324  			if testMatch != test.match {
   325  				t.Fatalf("Unexpected result %t, with pattern %s, flag %s and filepath %s \n",
   326  					!test.match, test.pattern, test.flagName, test.filePath)
   327  			}
   328  		}
   329  	}
   330  }
   331  
   332  // Tests string substitution function.
   333  func TestStringReplace(t *testing.T) {
   334  	testCases := []struct {
   335  		str         string
   336  		expectedStr string
   337  		content     contentMessage
   338  	}{
   339  		// Tests string replace {} without quotes.
   340  		{
   341  			str:         "{}",
   342  			expectedStr: "path/1",
   343  			content:     contentMessage{Key: "path/1"},
   344  		},
   345  		// Tests string replace {} with quotes.
   346  		{
   347  			str:         `{""}`,
   348  			expectedStr: `"path/1"`,
   349  			content:     contentMessage{Key: "path/1"},
   350  		},
   351  		// Tests string replace {base}
   352  		{
   353  			str:         "{base}",
   354  			expectedStr: "1",
   355  			content:     contentMessage{Key: "path/1"},
   356  		},
   357  		// Tests string replace {"base"} with quotes.
   358  		{
   359  			str:         `{"base"}`,
   360  			expectedStr: `"1"`,
   361  			content:     contentMessage{Key: "path/1"},
   362  		},
   363  		// Tests string replace {dir}
   364  		{
   365  			str:         `{dir}`,
   366  			expectedStr: `path`,
   367  			content:     contentMessage{Key: "path/1"},
   368  		},
   369  		// Tests string replace {"dir"} with quotes.
   370  		{
   371  			str:         `{"dir"}`,
   372  			expectedStr: `"path"`,
   373  			content:     contentMessage{Key: "path/1"},
   374  		},
   375  		// Tests string replace {"size"} with quotes.
   376  		{
   377  			str:         `{"size"}`,
   378  			expectedStr: `"0 B"`,
   379  			content:     contentMessage{Size: 0},
   380  		},
   381  		// Tests string replace {"time"} with quotes.
   382  		{
   383  			str:         `{"time"}`,
   384  			expectedStr: `"2038-01-19 03:14:07 UTC"`,
   385  			content: contentMessage{
   386  				Time: time.Unix(2147483647, 0).UTC(),
   387  			},
   388  		},
   389  		// Tests string replace {size}
   390  		{
   391  			str:         `{size}`,
   392  			expectedStr: `1.0 MiB`,
   393  			content:     contentMessage{Size: 1024 * 1024},
   394  		},
   395  		// Tests string replace {time}
   396  		{
   397  			str:         `{time}`,
   398  			expectedStr: `2038-01-19 03:14:07 UTC`,
   399  			content: contentMessage{
   400  				Time: time.Unix(2147483647, 0).UTC(),
   401  			},
   402  		},
   403  	}
   404  	for i, testCase := range testCases {
   405  		gotStr := stringsReplace(context.Background(), testCase.str, testCase.content)
   406  		if gotStr != testCase.expectedStr {
   407  			t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.expectedStr, gotStr)
   408  		}
   409  	}
   410  }
   411  
   412  // Tests exit status, getExitStatus() function
   413  func TestGetExitStatus(t *testing.T) {
   414  	if runtime.GOOS != "linux" {
   415  		t.Skip("Skipping on non-linux")
   416  		return
   417  	}
   418  	testCases := []struct {
   419  		command            string
   420  		expectedExitStatus int
   421  	}{
   422  		// Tests "No such file or directory", exit status code 2
   423  		{
   424  			command:            "ls asdf",
   425  			expectedExitStatus: 2,
   426  		},
   427  		{
   428  			command:            "cp x x",
   429  			expectedExitStatus: 1,
   430  		},
   431  		// expectedExitStatus for "command not found" case is 127,
   432  		// but exec command cannot capture anything since a process
   433  		// for the command could not be started at all,
   434  		// so the expectedExitStatus is 1
   435  		{
   436  			command:            "asdf",
   437  			expectedExitStatus: 1,
   438  		},
   439  	}
   440  	for i, testCase := range testCases {
   441  		commandArgs := strings.Split(testCase.command, " ")
   442  		cmd := exec.Command(commandArgs[0], commandArgs[1:]...)
   443  		// Return exit status of the command run
   444  		exitStatus := getExitStatus(cmd.Run())
   445  		if exitStatus != testCase.expectedExitStatus {
   446  			t.Errorf("Test %d: Expected error status code for command \"%v\" is %v, got %v",
   447  				i+1, testCase.command, testCase.expectedExitStatus, exitStatus)
   448  		}
   449  	}
   450  }