From 529604ac4d4776485a5296d514722afc823702c4 Mon Sep 17 00:00:00 2001 From: hailaz <739476267@qq.com> Date: Wed, 24 Dec 2025 18:05:31 +0800 Subject: [PATCH 1/5] feat(os/gfile): add MatchGlob function with globstar support (#4570) --- os/gfile/gfile_match.go | 132 ++++++++++++++++++++ os/gfile/gfile_z_unit_match_test.go | 183 ++++++++++++++++++++++++++++ 2 files changed, 315 insertions(+) create mode 100644 os/gfile/gfile_match.go create mode 100644 os/gfile/gfile_z_unit_match_test.go diff --git a/os/gfile/gfile_match.go b/os/gfile/gfile_match.go new file mode 100644 index 00000000000..7786c431603 --- /dev/null +++ b/os/gfile/gfile_match.go @@ -0,0 +1,132 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gfile + +import ( + "path/filepath" + "strings" +) + +// MatchGlob reports whether name matches the shell pattern. +// It extends filepath.Match (https://pkg.go.dev/path/filepath#Match) +// with support for "**" (globstar) pattern, similar to bash's globstar +// (https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html) +// and gitignore patterns (https://git-scm.com/docs/gitignore#_pattern_format). +// +// Pattern syntax: +// - '*' matches any sequence of non-separator characters +// - '**' matches any sequence of characters including separators (globstar) +// - '?' matches any single non-separator character +// - '[abc]' matches any character in the bracket +// - '[a-z]' matches any character in the range +// +// Example: +// +// MatchGlob("src/**/*.go", "src/foo/bar/main.go") => true +// MatchGlob("*.go", "main.go") => true +// MatchGlob("**", "any/path/file.go") => true +func MatchGlob(pattern, name string) (bool, error) { + // If no **, use standard filepath.Match + if !strings.Contains(pattern, "**") { + return filepath.Match(pattern, name) + } + return matchGlobstar(pattern, name) +} + +// matchGlobstar handles patterns containing "**". +func matchGlobstar(pattern, name string) (bool, error) { + // Normalize path separators to / (handle both Windows and Unix) + pattern = strings.ReplaceAll(pattern, "\\", "/") + name = strings.ReplaceAll(name, "\\", "/") + + // Clean up multiple slashes + for strings.Contains(pattern, "//") { + pattern = strings.ReplaceAll(pattern, "//", "/") + } + for strings.Contains(name, "//") { + name = strings.ReplaceAll(name, "//", "/") + } + + return doMatchGlobstar(pattern, name) +} + +// doMatchGlobstar recursively matches pattern with globstar support. +func doMatchGlobstar(pattern, name string) (bool, error) { + // Split pattern by "**" + parts := strings.SplitN(pattern, "**", 2) + if len(parts) == 1 { + // No "**" found, use standard match + return filepath.Match(pattern, name) + } + + prefix := parts[0] + suffix := parts[1] + + // Remove trailing slash from prefix + prefix = strings.TrimSuffix(prefix, "/") + // Remove leading slash from suffix + suffix = strings.TrimPrefix(suffix, "/") + + // Match prefix + if prefix != "" { + // Check if name starts with prefix pattern + if !strings.Contains(prefix, "*") && !strings.Contains(prefix, "?") && !strings.Contains(prefix, "[") { + // Prefix is literal, check directly + if !strings.HasPrefix(name, prefix) { + return false, nil + } + name = strings.TrimPrefix(name, prefix) + name = strings.TrimPrefix(name, "/") + } else { + // Prefix contains wildcards, need to match each segment + prefixParts := strings.Split(prefix, "/") + nameParts := strings.Split(name, "/") + + if len(nameParts) < len(prefixParts) { + return false, nil + } + + for i, pp := range prefixParts { + matched, err := filepath.Match(pp, nameParts[i]) + if err != nil { + return false, err + } + if !matched { + return false, nil + } + } + name = strings.Join(nameParts[len(prefixParts):], "/") + } + } + + // If suffix is empty, "**" matches everything remaining + if suffix == "" { + return true, nil + } + + // Try matching "**" with 0 to N path segments + if name == "" { + // No remaining name, check if suffix can match empty + return doMatchGlobstar(suffix, "") + } + + nameParts := strings.Split(name, "/") + + // Try "**" matching 0, 1, 2, ... N segments + for i := 0; i <= len(nameParts); i++ { + remaining := strings.Join(nameParts[i:], "/") + matched, err := doMatchGlobstar(suffix, remaining) + if err != nil { + return false, err + } + if matched { + return true, nil + } + } + + return false, nil +} diff --git a/os/gfile/gfile_z_unit_match_test.go b/os/gfile/gfile_z_unit_match_test.go new file mode 100644 index 00000000000..f85d55700ee --- /dev/null +++ b/os/gfile/gfile_z_unit_match_test.go @@ -0,0 +1,183 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gfile_test + +import ( + "testing" + + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/test/gtest" +) + +func Test_MatchGlob_Basic(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Basic glob patterns (no **) + matched, err := gfile.MatchGlob("*.go", "main.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("*.go", "main.txt") + t.AssertNil(err) + t.Assert(matched, false) + + matched, err = gfile.MatchGlob("test_*.go", "test_main.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("?est.go", "test.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("[abc].go", "a.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("[a-z].go", "x.go") + t.AssertNil(err) + t.Assert(matched, true) + }) +} + +func Test_MatchGlob_Globstar(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // ** matches everything + matched, err := gfile.MatchGlob("**", "any/path/to/file.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("**", "file.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("**", "") + t.AssertNil(err) + t.Assert(matched, true) + }) +} + +func Test_MatchGlob_GlobstarWithSuffix(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // **/*.go - matches .go files in any directory + matched, err := gfile.MatchGlob("**/*.go", "main.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("**/*.go", "src/main.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("**/*.go", "src/foo/bar/main.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("**/*.go", "src/main.txt") + t.AssertNil(err) + t.Assert(matched, false) + }) +} + +func Test_MatchGlob_GlobstarWithPrefix(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // src/** - matches everything under src/ + matched, err := gfile.MatchGlob("src/**", "src/main.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("src/**", "src/foo/bar/main.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("src/**", "other/main.go") + t.AssertNil(err) + t.Assert(matched, false) + }) +} + +func Test_MatchGlob_GlobstarWithPrefixAndSuffix(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // src/**/*.go - matches .go files under src/ + matched, err := gfile.MatchGlob("src/**/*.go", "src/main.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("src/**/*.go", "src/foo/main.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("src/**/*.go", "src/foo/bar/baz/main.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("src/**/*.go", "src/main.txt") + t.AssertNil(err) + t.Assert(matched, false) + + matched, err = gfile.MatchGlob("src/**/*.go", "other/main.go") + t.AssertNil(err) + t.Assert(matched, false) + }) +} + +func Test_MatchGlob_GlobstarMultiple(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Multiple ** in pattern + matched, err := gfile.MatchGlob("src/**/test/**/*.go", "src/foo/test/bar/main.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("src/**/test/**/*.go", "src/test/main.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("src/**/test/**/*.go", "src/a/b/test/c/d/main.go") + t.AssertNil(err) + t.Assert(matched, true) + }) +} + +func Test_MatchGlob_GlobstarEdgeCases(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // ** at the beginning + matched, err := gfile.MatchGlob("**/main.go", "main.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("**/main.go", "src/main.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("**/main.go", "src/foo/bar/main.go") + t.AssertNil(err) + t.Assert(matched, true) + + // Hidden directories + matched, err = gfile.MatchGlob(".*", ".git") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob(".*", ".vscode") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("_*", "_test") + t.AssertNil(err) + t.Assert(matched, true) + }) +} + +func Test_MatchGlob_WindowsPath(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Windows-style paths should also work + matched, err := gfile.MatchGlob("src/**/*.go", "src\\foo\\main.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("src\\**\\*.go", "src/foo/main.go") + t.AssertNil(err) + t.Assert(matched, true) + }) +} From 4b2f47d27d61a6f011fa145168a48187a028a61b Mon Sep 17 00:00:00 2001 From: hailaz <739476267@qq.com> Date: Fri, 26 Dec 2025 14:41:02 +0800 Subject: [PATCH 2/5] fix(os/gfile): enhance MatchGlob with proper globstar handling and error detection (#4570) --- os/gfile/gfile_match.go | 186 ++++++++++++++++++++++++---- os/gfile/gfile_z_unit_match_test.go | 179 ++++++++++++++++++++++++++ 2 files changed, 339 insertions(+), 26 deletions(-) diff --git a/os/gfile/gfile_match.go b/os/gfile/gfile_match.go index 7786c431603..c0f995703d0 100644 --- a/os/gfile/gfile_match.go +++ b/os/gfile/gfile_match.go @@ -7,6 +7,7 @@ package gfile import ( + "path" "path/filepath" "strings" ) @@ -18,17 +19,34 @@ import ( // and gitignore patterns (https://git-scm.com/docs/gitignore#_pattern_format). // // Pattern syntax: -// - '*' matches any sequence of non-separator characters -// - '**' matches any sequence of characters including separators (globstar) -// - '?' matches any single non-separator character -// - '[abc]' matches any character in the bracket -// - '[a-z]' matches any character in the range +// - '*' matches any sequence of non-separator characters +// - '**' matches any sequence of characters including separators (globstar) +// - '?' matches any single non-separator character +// - '[abc]' matches any character in the bracket +// - '[a-z]' matches any character in the range +// - '[^abc]' matches any character not in the bracket (negation) +// - '[^a-z]' matches any character not in the range (negation) +// +// Globstar rules: +// - "**" only has globstar semantics when it appears as a complete path component +// (e.g., "a/**/b", "**/a", "a/**", "**"). +// - Patterns like "a**b" or "**a" treat "**" as two regular "*" wildcards, +// matching only within a single path component. +// - Both "/" and "\" are treated as path separators (cross-platform support). +// +// Error handling: +// - Returns an error for malformed patterns (e.g., unclosed brackets "[abc"). +// - Errors from filepath.Match are propagated. // // Example: // -// MatchGlob("src/**/*.go", "src/foo/bar/main.go") => true -// MatchGlob("*.go", "main.go") => true -// MatchGlob("**", "any/path/file.go") => true +// MatchGlob("src/**/*.go", "src/foo/bar/main.go") => true, nil +// MatchGlob("*.go", "main.go") => true, nil +// MatchGlob("**", "any/path/file.go") => true, nil +// MatchGlob("a**b", "axxb") => true, nil (** as two *) +// MatchGlob("a**b", "a/b") => false, nil (no separator match) +// MatchGlob("[abc]", "a") => true, nil +// MatchGlob("[", "a") => false, error (malformed) func MatchGlob(pattern, name string) (bool, error) { // If no **, use standard filepath.Match if !strings.Contains(pattern, "**") { @@ -43,28 +61,135 @@ func matchGlobstar(pattern, name string) (bool, error) { pattern = strings.ReplaceAll(pattern, "\\", "/") name = strings.ReplaceAll(name, "\\", "/") - // Clean up multiple slashes - for strings.Contains(pattern, "//") { - pattern = strings.ReplaceAll(pattern, "//", "/") - } - for strings.Contains(name, "//") { - name = strings.ReplaceAll(name, "//", "/") + // Clean up paths (handles multiple slashes, . and ..) + // Using path.Clean for consistent cross-platform behavior with forward slashes + pattern = path.Clean(pattern) + name = path.Clean(name) + + // Check if "**" appears as a valid globstar (complete path component). + // If not, treat "**" as two regular "*" wildcards. + if !hasValidGlobstar(pattern) { + // Replace "**" with a placeholder, then use filepath.Match + // Since filepath.Match treats "*" as matching non-separator chars, + // "**" is equivalent to "*" in terms of matching (both match any + // sequence of non-separator characters). + normalizedPattern := strings.ReplaceAll(pattern, "**", "*") + return filepath.Match(normalizedPattern, name) } return doMatchGlobstar(pattern, name) } +// hasValidGlobstar checks if the pattern contains "**" as a valid globstar +// (i.e., as a complete path component). Valid globstar patterns: +// - "**" (the entire pattern) +// - "**/" (at the start) +// - "/**" (at the end) +// - "/**/" (in the middle) +func hasValidGlobstar(pattern string) bool { + // Check each occurrence of "**" + idx := 0 + for { + pos := strings.Index(pattern[idx:], "**") + if pos == -1 { + return false + } + pos += idx + + // Check if this "**" is a valid globstar + if isValidGlobstarAt(pattern, pos) { + return true + } + + idx = pos + 2 + if idx >= len(pattern) { + break + } + } + return false +} + +// isValidGlobstarAt checks if the "**" at position pos is a valid globstar. +// A valid globstar must be a complete path component: +// - At start: "**" or "**/" +// - At end: "/**" +// - In middle: "/**/" +func isValidGlobstarAt(pattern string, pos int) bool { + // Check character before "**" + if pos > 0 && pattern[pos-1] != '/' { + return false + } + + // Check character after "**" + endPos := pos + 2 + if endPos < len(pattern) && pattern[endPos] != '/' { + return false + } + + return true +} + +// findValidGlobstar finds the first valid globstar in the pattern. +// Returns the position or -1 if not found. +func findValidGlobstar(pattern string) int { + idx := 0 + for { + pos := strings.Index(pattern[idx:], "**") + if pos == -1 { + return -1 + } + pos += idx + + if isValidGlobstarAt(pattern, pos) { + return pos + } + + idx = pos + 2 + if idx >= len(pattern) { + break + } + } + return -1 +} + // doMatchGlobstar recursively matches pattern with globstar support. +// Uses memoization to avoid exponential time complexity with multiple "**" operators. func doMatchGlobstar(pattern, name string) (bool, error) { - // Split pattern by "**" - parts := strings.SplitN(pattern, "**", 2) - if len(parts) == 1 { - // No "**" found, use standard match - return filepath.Match(pattern, name) + memo := make(map[string]bool) + return doMatchGlobstarMemo(pattern, name, memo) +} + +// doMatchGlobstarMemo is the memoized implementation of globstar matching. +func doMatchGlobstarMemo(pattern, name string, memo map[string]bool) (bool, error) { + // Create cache key + cacheKey := pattern + "\x00" + name + if cached, ok := memo[cacheKey]; ok { + return cached, nil } - prefix := parts[0] - suffix := parts[1] + result, err := doMatchGlobstarCore(pattern, name, memo) + if err != nil { + return false, err + } + + memo[cacheKey] = result + return result, nil +} + +// doMatchGlobstarCore contains the core matching logic. +func doMatchGlobstarCore(pattern, name string, memo map[string]bool) (bool, error) { + // Find the first valid globstar + pos := findValidGlobstar(pattern) + if pos == -1 { + // No valid globstar, use standard match + // Replace any "**" with "*" since they're not valid globstars + normalizedPattern := strings.ReplaceAll(pattern, "**", "*") + return filepath.Match(normalizedPattern, name) + } + + // Split pattern at the valid globstar position + prefix := pattern[:pos] + suffix := pattern[pos+2:] // Remove trailing slash from prefix prefix = strings.TrimSuffix(prefix, "/") @@ -75,12 +200,21 @@ func doMatchGlobstar(pattern, name string) (bool, error) { if prefix != "" { // Check if name starts with prefix pattern if !strings.Contains(prefix, "*") && !strings.Contains(prefix, "?") && !strings.Contains(prefix, "[") { - // Prefix is literal, check directly + // Prefix is literal, check directly against full path component if !strings.HasPrefix(name, prefix) { return false, nil } - name = strings.TrimPrefix(name, prefix) - name = strings.TrimPrefix(name, "/") + if len(name) == len(prefix) { + // Name is exactly the prefix + name = "" + } else { + // Ensure the prefix ends at a path separator boundary + if name[len(prefix)] != '/' { + return false, nil + } + // Skip the separator as well + name = name[len(prefix)+1:] + } } else { // Prefix contains wildcards, need to match each segment prefixParts := strings.Split(prefix, "/") @@ -111,7 +245,7 @@ func doMatchGlobstar(pattern, name string) (bool, error) { // Try matching "**" with 0 to N path segments if name == "" { // No remaining name, check if suffix can match empty - return doMatchGlobstar(suffix, "") + return doMatchGlobstarMemo(suffix, "", memo) } nameParts := strings.Split(name, "/") @@ -119,7 +253,7 @@ func doMatchGlobstar(pattern, name string) (bool, error) { // Try "**" matching 0, 1, 2, ... N segments for i := 0; i <= len(nameParts); i++ { remaining := strings.Join(nameParts[i:], "/") - matched, err := doMatchGlobstar(suffix, remaining) + matched, err := doMatchGlobstarMemo(suffix, remaining, memo) if err != nil { return false, err } diff --git a/os/gfile/gfile_z_unit_match_test.go b/os/gfile/gfile_z_unit_match_test.go index f85d55700ee..e5b6ab871f3 100644 --- a/os/gfile/gfile_z_unit_match_test.go +++ b/os/gfile/gfile_z_unit_match_test.go @@ -181,3 +181,182 @@ func Test_MatchGlob_WindowsPath(t *testing.T) { t.Assert(matched, true) }) } + +func Test_MatchGlob_InvalidGlobstar(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // "**" not as complete path component should be treated as two "*" + // "a**b" should match "ab", "axb", "axxb", etc. (but not "a/b") + matched, err := gfile.MatchGlob("a**b", "ab") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("a**b", "axb") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("a**b", "axxb") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("a**b", "axxxb") + t.AssertNil(err) + t.Assert(matched, true) + + // "a**b" should NOT match paths with separators + matched, err = gfile.MatchGlob("a**b", "a/b") + t.AssertNil(err) + t.Assert(matched, false) + + matched, err = gfile.MatchGlob("a**b", "ax/yb") + t.AssertNil(err) + t.Assert(matched, false) + + // "**a" at start (not valid globstar) + matched, err = gfile.MatchGlob("**a", "a") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("**a", "xa") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("**a", "xxa") + t.AssertNil(err) + t.Assert(matched, true) + + // "a**" at end (not valid globstar) + matched, err = gfile.MatchGlob("a**", "a") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("a**", "ax") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("a**", "axx") + t.AssertNil(err) + t.Assert(matched, true) + + // Mixed valid and invalid globstars + // "src/**a" - "**" is valid globstar, "a" is suffix + matched, err = gfile.MatchGlob("src/**/a", "src/foo/a") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("src/**/a", "src/a") + t.AssertNil(err) + t.Assert(matched, true) + }) +} + +func Test_MatchGlob_PrefixBoundary(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // "abc/**" should NOT match "abcdef/file.go" (prefix must be complete path component) + matched, err := gfile.MatchGlob("abc/**", "abcdef/file.go") + t.AssertNil(err) + t.Assert(matched, false) + + // "abc/**" should match "abc/file.go" + matched, err = gfile.MatchGlob("abc/**", "abc/file.go") + t.AssertNil(err) + t.Assert(matched, true) + + // "abc/**" should match "abc/def/file.go" + matched, err = gfile.MatchGlob("abc/**", "abc/def/file.go") + t.AssertNil(err) + t.Assert(matched, true) + + // "abc/**" should match "abc" (prefix equals name) + matched, err = gfile.MatchGlob("abc/**", "abc") + t.AssertNil(err) + t.Assert(matched, true) + + // "src/foo/**" should NOT match "src/foobar/file.go" + matched, err = gfile.MatchGlob("src/foo/**", "src/foobar/file.go") + t.AssertNil(err) + t.Assert(matched, false) + + // "src/foo/**" should match "src/foo/bar/file.go" + matched, err = gfile.MatchGlob("src/foo/**", "src/foo/bar/file.go") + t.AssertNil(err) + t.Assert(matched, true) + }) +} + +func Test_MatchGlob_MultipleGlobstars(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test with multiple ** operators - this would be slow without memoization + matched, err := gfile.MatchGlob("a/**/b/**/c/**/d.go", "a/x/y/b/z/c/w/d.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("a/**/b/**/c/**/d.go", "a/b/c/d.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("a/**/b/**/c/**/d.go", "a/1/2/3/b/4/5/c/6/d.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("a/**/b/**/c/**/d.go", "a/b/c/e.go") + t.AssertNil(err) + t.Assert(matched, false) + + // Deep nesting test + matched, err = gfile.MatchGlob("**/*.go", "a/b/c/d/e/f/g/h/i/j/main.go") + t.AssertNil(err) + t.Assert(matched, true) + }) +} + +func Test_MatchGlob_MalformedPatterns(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Unclosed bracket - should return error + _, err := gfile.MatchGlob("[", "a") + t.AssertNE(err, nil) + + _, err = gfile.MatchGlob("[abc", "a") + t.AssertNE(err, nil) + + _, err = gfile.MatchGlob("[[", "a") + t.AssertNE(err, nil) + + // Malformed patterns with globstar - errors should propagate + _, err = gfile.MatchGlob("**/[", "a/b") + t.AssertNE(err, nil) + + _, err = gfile.MatchGlob("[/**", "a/b") + t.AssertNE(err, nil) + + _, err = gfile.MatchGlob("a/**/[abc", "a/b/c") + t.AssertNE(err, nil) + + // Malformed pattern in prefix with wildcards + _, err = gfile.MatchGlob("[a/**/b", "a/x/b") + t.AssertNE(err, nil) + + // Invalid escape sequence on non-Windows (backslash at end) + // Note: behavior may vary by platform + _, err = gfile.MatchGlob("test\\", "test") + // On Unix, this might not error but won't match + // The key is it shouldn't panic + + // Valid patterns should still work + matched, err := gfile.MatchGlob("[abc]", "a") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("[a-z]", "m") + t.AssertNil(err) + t.Assert(matched, true) + + // Note: filepath.Match uses [^...] for negation, not [!...] + matched, err = gfile.MatchGlob("[^abc]", "d") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("[^a-z]", "1") + t.AssertNil(err) + t.Assert(matched, true) + }) +} From 598943490c5573a6d49618da204753c2ff882274 Mon Sep 17 00:00:00 2001 From: hailaz <739476267@qq.com> Date: Fri, 26 Dec 2025 14:47:40 +0800 Subject: [PATCH 3/5] fix(os/gfile): add comprehensive test cases for MatchGlob edge cases and memoization cache scenarios --- os/gfile/gfile_z_unit_match_test.go | 238 ++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) diff --git a/os/gfile/gfile_z_unit_match_test.go b/os/gfile/gfile_z_unit_match_test.go index e5b6ab871f3..02d9d800ef1 100644 --- a/os/gfile/gfile_z_unit_match_test.go +++ b/os/gfile/gfile_z_unit_match_test.go @@ -360,3 +360,241 @@ func Test_MatchGlob_MalformedPatterns(t *testing.T) { t.Assert(matched, true) }) } + +func Test_MatchGlob_MemoizationCache(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test cases that exercise memoization cache hits + // Multiple ** with same suffix patterns will trigger cache reuse + matched, err := gfile.MatchGlob("a/**/b/**/c", "a/x/b/y/c") + t.AssertNil(err) + t.Assert(matched, true) + + // This pattern creates multiple paths that converge to same subproblems + matched, err = gfile.MatchGlob("**/a/**/a", "x/a/y/a") + t.AssertNil(err) + t.Assert(matched, true) + + // Deep recursion with cache hits + matched, err = gfile.MatchGlob("**/**/**", "a/b/c") + t.AssertNil(err) + t.Assert(matched, true) + }) +} + +func Test_MatchGlob_InvalidGlobstarAtEnd(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Pattern where "**" appears at the very end of string (idx >= len(pattern) after pos+2) + // "x**" - invalid globstar at end, should be treated as two "*" + matched, err := gfile.MatchGlob("x**", "x") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("x**", "xyz") + t.AssertNil(err) + t.Assert(matched, true) + + // Pattern ending with invalid globstar that exhausts the string + matched, err = gfile.MatchGlob("abc**", "abc") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("abc**", "abcdef") + t.AssertNil(err) + t.Assert(matched, true) + }) +} + +func Test_MatchGlob_PrefixWithWildcards(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Prefix contains wildcards - tests lines 220-236 + // Pattern: "s*c/**/file.go" - prefix "s*c" contains wildcard + matched, err := gfile.MatchGlob("s*c/**/*.go", "src/foo/main.go") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("s?c/**/*.go", "src/foo/main.go") + t.AssertNil(err) + t.Assert(matched, true) + + // Test line 223-225: name has fewer segments than prefix + matched, err = gfile.MatchGlob("a/b/c/**", "a/b") + t.AssertNil(err) + t.Assert(matched, false) + + matched, err = gfile.MatchGlob("a/b/c/**/d", "a") + t.AssertNil(err) + t.Assert(matched, false) + + // Test line 232-234: wildcard prefix doesn't match + matched, err = gfile.MatchGlob("x*c/**/*.go", "src/foo/main.go") + t.AssertNil(err) + t.Assert(matched, false) + + matched, err = gfile.MatchGlob("s?x/**/*.go", "src/foo/main.go") + t.AssertNil(err) + t.Assert(matched, false) + + // Test line 236: name update after prefix match + matched, err = gfile.MatchGlob("a*/b*/**/*.go", "abc/bcd/efg/main.go") + t.AssertNil(err) + t.Assert(matched, true) + }) +} + +func Test_MatchGlob_EmptyNameWithSuffix(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test line 246-249: name becomes empty after prefix match, check if suffix can match empty + // "abc/**" with name "abc" - after prefix match, name is empty + matched, err := gfile.MatchGlob("abc/**/", "abc") + t.AssertNil(err) + t.Assert(matched, true) + + // "abc/**/d" with name "abc" - after prefix match, name is empty but suffix is "d" + matched, err = gfile.MatchGlob("abc/**/d", "abc") + t.AssertNil(err) + t.Assert(matched, false) + + // Test with wildcard prefix that exactly matches + matched, err = gfile.MatchGlob("a*c/**/x", "abc") + t.AssertNil(err) + t.Assert(matched, false) + }) +} + +func Test_MatchGlob_FindValidGlobstarExhaust(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test lines 147-152: findValidGlobstar exhausts pattern without finding valid globstar + // Pattern with multiple invalid "**" that ends exactly at pattern length + matched, err := gfile.MatchGlob("a**b**", "ab") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("x**y**z", "xyz") + t.AssertNil(err) + t.Assert(matched, true) + + // Pattern where last "**" is at the very end but invalid + matched, err = gfile.MatchGlob("test**", "test") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("test**", "testing") + t.AssertNil(err) + t.Assert(matched, true) + }) +} + +func Test_MatchGlob_CacheHit(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test line 166-168: cache hit scenario + // Pattern that creates overlapping subproblems triggering cache hits + // "**/**" with multiple segments will have cache hits + matched, err := gfile.MatchGlob("**/x/**/x", "a/x/b/x") + t.AssertNil(err) + t.Assert(matched, true) + + // This pattern specifically creates cache hits due to overlapping subproblems + // when trying different combinations of ** matching + matched, err = gfile.MatchGlob("**/a/**/b/**/a", "x/a/y/b/z/a") + t.AssertNil(err) + t.Assert(matched, true) + + // Pattern with repeated suffix that will be checked multiple times + matched, err = gfile.MatchGlob("**/**/test", "a/b/c/test") + t.AssertNil(err) + t.Assert(matched, true) + + // Pattern that will cause same subproblem to be solved multiple times + // "**/**/**" matching "a/b/c/d" will have many overlapping subproblems + matched, err = gfile.MatchGlob("**/**/**/**", "a/b/c/d/e") + t.AssertNil(err) + t.Assert(matched, true) + }) +} + +func Test_MatchGlob_WildcardPrefixShortName(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test line 223-225: prefix with wildcards, name has fewer segments + // Pattern: "a*/b*/**/c" - prefix "a*/b*" has 2 segments + // Name: "ax" - only 1 segment + matched, err := gfile.MatchGlob("a*/b*/**/c", "ax") + t.AssertNil(err) + t.Assert(matched, false) + + // Pattern: "?/b/c/**/d" - prefix "?/b/c" has 3 segments + // Name: "x/y" - only 2 segments + matched, err = gfile.MatchGlob("?/b/c/**/d", "x/y") + t.AssertNil(err) + t.Assert(matched, false) + + // Pattern: "[abc]/[def]/**/x" - prefix has 2 segments with brackets + // Name: "a" - only 1 segment + matched, err = gfile.MatchGlob("[abc]/[def]/**/x", "a") + t.AssertNil(err) + t.Assert(matched, false) + }) +} + +func Test_MatchGlob_InvalidGlobstarInSuffix(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test lines 147-152: findValidGlobstar exhausts pattern in recursive call + // Pattern "a/**/b**" - first "**" is valid, suffix "b**" has invalid "**" at end + // When matching suffix "b**", findValidGlobstar will iterate and find "**" is invalid, + // then idx = pos + 2 = 3, len("b**") = 3, so idx >= len(pattern) triggers break + matched, err := gfile.MatchGlob("a/**/b**", "a/x/bcd") + t.AssertNil(err) + t.Assert(matched, true) + + matched, err = gfile.MatchGlob("a/**/b**", "a/x/b") + t.AssertNil(err) + t.Assert(matched, true) + + // Pattern with valid globstar followed by suffix with invalid globstar at end + matched, err = gfile.MatchGlob("x/**/y**z", "x/a/yabcz") + t.AssertNil(err) + t.Assert(matched, true) + + // Multiple invalid globstars in suffix + matched, err = gfile.MatchGlob("a/**/x**y**", "a/b/xcy") + t.AssertNil(err) + t.Assert(matched, true) + }) +} + +func Test_MatchGlob_MemoizationCacheHit(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test line 166-168: cache hit scenario + // To trigger cache hit, we need: + // 1. Same (pattern, name) pair called twice + // 2. First call must complete (not return early) + // 3. This happens when matching FAILS and we try all combinations + + // Pattern "**/**/z" with name "a/b/c/d" (no match) + // First ** tries 0,1,2,3,4 segments + // For each, second ** tries all remaining combinations + // This creates overlapping subproblems that fail: + // - ("**/z", "a/b/c/d"), ("**/z", "b/c/d"), ("**/z", "c/d"), ("**/z", "d"), ("**/z", "") + // - ("z", "a/b/c/d"), ("z", "b/c/d"), ("z", "c/d"), ("z", "d"), ("z", "") + // When first ** matches 0: check ("**/z", "a/b/c/d") + // -> second ** matches 0: check ("z", "a/b/c/d") - false, cached + // -> second ** matches 1: check ("z", "b/c/d") - false, cached + // -> second ** matches 2: check ("z", "c/d") - false, cached + // -> second ** matches 3: check ("z", "d") - false, cached + // -> second ** matches 4: check ("z", "") - false, cached + // When first ** matches 1: check ("**/z", "b/c/d") + // -> second ** matches 0: check ("z", "b/c/d") - CACHE HIT! + matched, err := gfile.MatchGlob("**/**/z", "a/b/c/d") + t.AssertNil(err) + t.Assert(matched, false) + + // Another failing pattern that creates cache hits + matched, err = gfile.MatchGlob("**/**/**/notexist", "a/b/c/d/e") + t.AssertNil(err) + t.Assert(matched, false) + + // Pattern with same suffix appearing multiple times in recursion (failing case) + matched, err = gfile.MatchGlob("**/x/**/x/**/x", "a/b/c/d/e/f") + t.AssertNil(err) + t.Assert(matched, false) + }) +} From 9393778def712fc1f56d784bd5c15d6e8882a93f Mon Sep 17 00:00:00 2001 From: hailaz <739476267@qq.com> Date: Fri, 26 Dec 2025 14:54:54 +0800 Subject: [PATCH 4/5] feat(os/gfile): add comprehensive unit tests for cache, replace, sort and utility functions --- os/gfile/gfile_z_unit_cache_test.go | 59 ++++++ os/gfile/gfile_z_unit_replace_test.go | 246 ++++++++++++++++++++++++++ os/gfile/gfile_z_unit_sort_test.go | 150 ++++++++++++++++ os/gfile/gfile_z_unit_test.go | 101 ++++++++++- os/gfile/gfile_z_unit_time_test.go | 33 ++++ 5 files changed, 583 insertions(+), 6 deletions(-) create mode 100644 os/gfile/gfile_z_unit_replace_test.go create mode 100644 os/gfile/gfile_z_unit_sort_test.go diff --git a/os/gfile/gfile_z_unit_cache_test.go b/os/gfile/gfile_z_unit_cache_test.go index b77b8610b44..eebebc98c69 100644 --- a/os/gfile/gfile_z_unit_cache_test.go +++ b/os/gfile/gfile_z_unit_cache_test.go @@ -84,3 +84,62 @@ func Test_GetContentsWithCache(t *testing.T) { } }) } + +func Test_GetBytesWithCache(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var f *os.File + var err error + fileName := "test_bytes" + byteContent := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f} // "Hello" + + if !gfile.Exists(fileName) { + f, err = os.CreateTemp("", fileName) + if err != nil { + t.Error("create file fail") + } + } + + defer f.Close() + defer os.Remove(f.Name()) + + if gfile.Exists(f.Name()) { + err = gfile.PutBytes(f.Name(), byteContent) + if err != nil { + t.Error("write error", err) + } + + // Test GetBytesWithCache with custom duration + cache := gfile.GetBytesWithCache(f.Name(), time.Second*1) + t.Assert(cache, byteContent) + + // Test cache hit - should return same content + cache2 := gfile.GetBytesWithCache(f.Name(), time.Second*1) + t.Assert(cache2, byteContent) + } + }) + + // Test with non-existent file + gtest.C(t, func(t *gtest.T) { + cache := gfile.GetBytesWithCache("/nonexistent_file_12345.txt") + t.Assert(cache, nil) + }) + + // Test with empty file + gtest.C(t, func(t *gtest.T) { + var f *os.File + var err error + fileName := "test_bytes_empty" + + f, err = os.CreateTemp("", fileName) + if err != nil { + t.Error("create file fail") + } + + defer f.Close() + defer os.Remove(f.Name()) + + // Read empty file + cache := gfile.GetBytesWithCache(f.Name(), time.Second*1) + t.Assert(len(cache), 0) + }) +} diff --git a/os/gfile/gfile_z_unit_replace_test.go b/os/gfile/gfile_z_unit_replace_test.go new file mode 100644 index 00000000000..748afc064a9 --- /dev/null +++ b/os/gfile/gfile_z_unit_replace_test.go @@ -0,0 +1,246 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gfile_test + +import ( + "testing" + + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/gconv" +) + +func Test_ReplaceFile(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + fileName = "/testfile_replace_" + gconv.String(gtime.TimestampNano()) + ".txt" + content = "hello world" + ) + createTestFile(fileName, content) + defer delTestFiles(fileName) + + // Test basic replacement + err := gfile.ReplaceFile("world", "gf", testpath()+fileName) + t.AssertNil(err) + t.Assert(gfile.GetContents(testpath()+fileName), "hello gf") + + // Test replacement with non-existent search string + err = gfile.ReplaceFile("notexist", "replaced", testpath()+fileName) + t.AssertNil(err) + t.Assert(gfile.GetContents(testpath()+fileName), "hello gf") + + // Test multiple occurrences replacement + err = gfile.PutContents(testpath()+fileName, "hello hello hello") + t.AssertNil(err) + err = gfile.ReplaceFile("hello", "hi", testpath()+fileName) + t.AssertNil(err) + t.Assert(gfile.GetContents(testpath()+fileName), "hi hi hi") + }) +} + +func Test_ReplaceFileFunc(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + fileName = "/testfile_replacefunc_" + gconv.String(gtime.TimestampNano()) + ".txt" + content = "hello world" + ) + createTestFile(fileName, content) + defer delTestFiles(fileName) + + // Test replacement with callback function + err := gfile.ReplaceFileFunc(func(path, content string) string { + t.Assert(gfile.Basename(path), gfile.Basename(fileName)) + return content + " - modified" + }, testpath()+fileName) + t.AssertNil(err) + t.Assert(gfile.GetContents(testpath()+fileName), "hello world - modified") + }) + + // Test when callback returns same content (no write should happen) + gtest.C(t, func(t *gtest.T) { + var ( + fileName = "/testfile_replacefunc2_" + gconv.String(gtime.TimestampNano()) + ".txt" + content = "unchanged content" + ) + createTestFile(fileName, content) + defer delTestFiles(fileName) + + err := gfile.ReplaceFileFunc(func(path, content string) string { + return content // Return same content + }, testpath()+fileName) + t.AssertNil(err) + t.Assert(gfile.GetContents(testpath()+fileName), "unchanged content") + }) + + // Test callback with path parameter + gtest.C(t, func(t *gtest.T) { + var ( + fileName = "/testfile_replacefunc3_" + gconv.String(gtime.TimestampNano()) + ".txt" + content = "test content" + ) + createTestFile(fileName, content) + defer delTestFiles(fileName) + + var receivedPath string + err := gfile.ReplaceFileFunc(func(path, content string) string { + receivedPath = path + return "new content" + }, testpath()+fileName) + t.AssertNil(err) + t.Assert(receivedPath, testpath()+fileName) + t.Assert(gfile.GetContents(testpath()+fileName), "new content") + }) +} + +func Test_ReplaceDir(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + dirName = "/testdir_replace_" + gconv.String(gtime.TimestampNano()) + fileName = dirName + "/test.txt" + content = "hello world" + ) + createDir(dirName) + createTestFile(fileName, content) + defer delTestFiles(dirName) + + // Test directory replacement with pattern + err := gfile.ReplaceDir("world", "gf", testpath()+dirName, "*.txt") + t.AssertNil(err) + t.Assert(gfile.GetContents(testpath()+fileName), "hello gf") + }) + + // Test recursive replacement + gtest.C(t, func(t *gtest.T) { + var ( + dirName = "/testdir_replace_recursive_" + gconv.String(gtime.TimestampNano()) + subDirName = dirName + "/subdir" + fileName1 = dirName + "/test1.txt" + fileName2 = subDirName + "/test2.txt" + content = "hello world" + ) + createDir(dirName) + createDir(subDirName) + createTestFile(fileName1, content) + createTestFile(fileName2, content) + defer delTestFiles(dirName) + + // Non-recursive replacement + err := gfile.ReplaceDir("world", "gf", testpath()+dirName, "*.txt", false) + t.AssertNil(err) + t.Assert(gfile.GetContents(testpath()+fileName1), "hello gf") + t.Assert(gfile.GetContents(testpath()+fileName2), "hello world") // Should not be changed + + // Reset content + err = gfile.PutContents(testpath()+fileName1, content) + t.AssertNil(err) + + // Recursive replacement + err = gfile.ReplaceDir("world", "gf", testpath()+dirName, "*.txt", true) + t.AssertNil(err) + t.Assert(gfile.GetContents(testpath()+fileName1), "hello gf") + t.Assert(gfile.GetContents(testpath()+fileName2), "hello gf") + }) + + // Test with pattern matching + gtest.C(t, func(t *gtest.T) { + var ( + dirName = "/testdir_replace_pattern_" + gconv.String(gtime.TimestampNano()) + fileName1 = dirName + "/test.txt" + fileName2 = dirName + "/test.log" + content = "hello world" + ) + createDir(dirName) + createTestFile(fileName1, content) + createTestFile(fileName2, content) + defer delTestFiles(dirName) + + // Only replace in .txt files + err := gfile.ReplaceDir("world", "gf", testpath()+dirName, "*.txt") + t.AssertNil(err) + t.Assert(gfile.GetContents(testpath()+fileName1), "hello gf") + t.Assert(gfile.GetContents(testpath()+fileName2), "hello world") // .log should not be changed + }) + + // Test with non-existent directory + gtest.C(t, func(t *gtest.T) { + err := gfile.ReplaceDir("search", "replace", "/nonexistent_dir_12345", "*.txt") + t.AssertNE(err, nil) + }) +} + +func Test_ReplaceDirFunc(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + dirName = "/testdir_replacefunc_" + gconv.String(gtime.TimestampNano()) + fileName1 = dirName + "/test1.txt" + fileName2 = dirName + "/test2.txt" + content1 = "content1" + content2 = "content2" + ) + createDir(dirName) + createTestFile(fileName1, content1) + createTestFile(fileName2, content2) + defer delTestFiles(dirName) + + // Test directory replacement with callback function + processedFiles := make(map[string]bool) + err := gfile.ReplaceDirFunc(func(path, content string) string { + processedFiles[gfile.Basename(path)] = true + return content + " - modified" + }, testpath()+dirName, "*.txt") + t.AssertNil(err) + t.Assert(gfile.GetContents(testpath()+fileName1), "content1 - modified") + t.Assert(gfile.GetContents(testpath()+fileName2), "content2 - modified") + t.Assert(processedFiles["test1.txt"], true) + t.Assert(processedFiles["test2.txt"], true) + }) + + // Test recursive replacement with callback + gtest.C(t, func(t *gtest.T) { + var ( + dirName = "/testdir_replacefunc_recursive_" + gconv.String(gtime.TimestampNano()) + subDirName = dirName + "/subdir" + fileName1 = dirName + "/test1.txt" + fileName2 = subDirName + "/test2.txt" + content = "original" + ) + createDir(dirName) + createDir(subDirName) + createTestFile(fileName1, content) + createTestFile(fileName2, content) + defer delTestFiles(dirName) + + // Non-recursive + err := gfile.ReplaceDirFunc(func(path, content string) string { + return "changed" + }, testpath()+dirName, "*.txt", false) + t.AssertNil(err) + t.Assert(gfile.GetContents(testpath()+fileName1), "changed") + t.Assert(gfile.GetContents(testpath()+fileName2), "original") // Should not be changed + + // Reset + err = gfile.PutContents(testpath()+fileName1, content) + t.AssertNil(err) + + // Recursive + err = gfile.ReplaceDirFunc(func(path, content string) string { + return "changed" + }, testpath()+dirName, "*.txt", true) + t.AssertNil(err) + t.Assert(gfile.GetContents(testpath()+fileName1), "changed") + t.Assert(gfile.GetContents(testpath()+fileName2), "changed") + }) + + // Test with non-existent directory + gtest.C(t, func(t *gtest.T) { + err := gfile.ReplaceDirFunc(func(path, content string) string { + return content + }, "/nonexistent_dir_12345", "*.txt") + t.AssertNE(err, nil) + }) +} diff --git a/os/gfile/gfile_z_unit_sort_test.go b/os/gfile/gfile_z_unit_sort_test.go new file mode 100644 index 00000000000..e0068b4a93a --- /dev/null +++ b/os/gfile/gfile_z_unit_sort_test.go @@ -0,0 +1,150 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gfile_test + +import ( + "testing" + + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/gconv" +) + +func Test_SortFiles(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + dirName = "/testdir_sort_" + gconv.String(gtime.TimestampNano()) + fileName1 = dirName + "/b.txt" + fileName2 = dirName + "/a.txt" + subDir1 = dirName + "/subdir_b" + subDir2 = dirName + "/subdir_a" + ) + createDir(dirName) + createDir(subDir1) + createDir(subDir2) + createTestFile(fileName1, "") + createTestFile(fileName2, "") + defer delTestFiles(dirName) + + // Test sorting: directories should come before files, then sorted alphabetically + files := []string{ + testpath() + fileName1, + testpath() + fileName2, + testpath() + subDir1, + testpath() + subDir2, + } + sorted := gfile.SortFiles(files) + + // Directories should come first, sorted alphabetically + t.Assert(sorted[0], testpath()+subDir2) // subdir_a + t.Assert(sorted[1], testpath()+subDir1) // subdir_b + // Files should come after, sorted alphabetically + t.Assert(sorted[2], testpath()+fileName2) // a.txt + t.Assert(sorted[3], testpath()+fileName1) // b.txt + }) + + // Test with only files + gtest.C(t, func(t *gtest.T) { + var ( + dirName = "/testdir_sort_files_" + gconv.String(gtime.TimestampNano()) + fileName1 = dirName + "/c.txt" + fileName2 = dirName + "/a.txt" + fileName3 = dirName + "/b.txt" + ) + createDir(dirName) + createTestFile(fileName1, "") + createTestFile(fileName2, "") + createTestFile(fileName3, "") + defer delTestFiles(dirName) + + files := []string{ + testpath() + fileName1, + testpath() + fileName2, + testpath() + fileName3, + } + sorted := gfile.SortFiles(files) + + t.Assert(sorted[0], testpath()+fileName2) // a.txt + t.Assert(sorted[1], testpath()+fileName3) // b.txt + t.Assert(sorted[2], testpath()+fileName1) // c.txt + }) + + // Test with only directories + gtest.C(t, func(t *gtest.T) { + var ( + dirName = "/testdir_sort_dirs_" + gconv.String(gtime.TimestampNano()) + subDir1 = dirName + "/c_dir" + subDir2 = dirName + "/a_dir" + subDir3 = dirName + "/b_dir" + ) + createDir(dirName) + createDir(subDir1) + createDir(subDir2) + createDir(subDir3) + defer delTestFiles(dirName) + + files := []string{ + testpath() + subDir1, + testpath() + subDir2, + testpath() + subDir3, + } + sorted := gfile.SortFiles(files) + + t.Assert(sorted[0], testpath()+subDir2) // a_dir + t.Assert(sorted[1], testpath()+subDir3) // b_dir + t.Assert(sorted[2], testpath()+subDir1) // c_dir + }) + + // Test with empty slice + gtest.C(t, func(t *gtest.T) { + files := []string{} + sorted := gfile.SortFiles(files) + t.Assert(len(sorted), 0) + }) + + // Test with single element + gtest.C(t, func(t *gtest.T) { + var ( + dirName = "/testdir_sort_single_" + gconv.String(gtime.TimestampNano()) + fileName = dirName + "/single.txt" + ) + createDir(dirName) + createTestFile(fileName, "") + defer delTestFiles(dirName) + + files := []string{testpath() + fileName} + sorted := gfile.SortFiles(files) + + t.Assert(len(sorted), 1) + t.Assert(sorted[0], testpath()+fileName) + }) + + // Test with mixed paths (some may not exist - testing sort behavior) + gtest.C(t, func(t *gtest.T) { + var ( + dirName = "/testdir_sort_mixed_" + gconv.String(gtime.TimestampNano()) + fileName = dirName + "/existing.txt" + subDir = dirName + "/existing_dir" + ) + createDir(dirName) + createDir(subDir) + createTestFile(fileName, "") + defer delTestFiles(dirName) + + // Mix of existing dir, existing file + files := []string{ + testpath() + fileName, + testpath() + subDir, + } + sorted := gfile.SortFiles(files) + + // Directory should come first + t.Assert(sorted[0], testpath()+subDir) + t.Assert(sorted[1], testpath()+fileName) + }) +} diff --git a/os/gfile/gfile_z_unit_test.go b/os/gfile/gfile_z_unit_test.go index 40696d871de..8090b88ec70 100644 --- a/os/gfile/gfile_z_unit_test.go +++ b/os/gfile/gfile_z_unit_test.go @@ -680,12 +680,6 @@ func Test_SelfName(t *testing.T) { }) } -func Test_MTimestamp(t *testing.T) { - gtest.C(t, func(t *gtest.T) { - t.Assert(gfile.MTimestamp(gfile.Temp()) > 0, true) - }) -} - func Test_RemoveFile_RemoveAll(t *testing.T) { // safe deleting single file. gtest.C(t, func(t *gtest.T) { @@ -725,3 +719,98 @@ func Test_RemoveFile_RemoveAll(t *testing.T) { t.Assert(gfile.Exists(filePath2), false) }) } + +func Test_Join(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Basic join + t.Assert(gfile.Join("a", "b", "c"), "a"+gfile.Separator+"b"+gfile.Separator+"c") + + // Join with trailing separator + t.Assert(gfile.Join("a"+gfile.Separator, "b"), "a"+gfile.Separator+"b") + + // Join with empty string + t.Assert(gfile.Join("", "a", "b"), "a"+gfile.Separator+"b") + + // Join single path + t.Assert(gfile.Join("single"), "single") + + // Join with absolute path + t.Assert(gfile.Join(gfile.Separator+"root", "path"), gfile.Separator+"root"+gfile.Separator+"path") + + // Join empty + t.Assert(gfile.Join(), "") + }) +} + +func Test_Chdir(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Save current working directory + originalPwd := gfile.Pwd() + defer func() { + // Restore original working directory + _ = gfile.Chdir(originalPwd) + }() + + // Test changing to temp directory + tempDir := gfile.Temp() + err := gfile.Chdir(tempDir) + t.AssertNil(err) + t.Assert(gfile.Pwd(), tempDir) + + // Test changing to non-existent directory + err = gfile.Chdir("/nonexistent_dir_12345") + t.AssertNE(err, nil) + }) +} + +func Test_Abs(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test with relative path + absPath := gfile.Abs(".") + t.Assert(len(absPath) > 0, true) + t.Assert(filepath.IsAbs(absPath), true) + + // Test with already absolute path + tempDir := gfile.Temp() + t.Assert(gfile.Abs(tempDir), tempDir) + + // Test with relative path components + absPath = gfile.Abs("./test") + t.Assert(filepath.IsAbs(absPath), true) + + // Test with parent directory reference + absPath = gfile.Abs("../test") + t.Assert(filepath.IsAbs(absPath), true) + + // Test with empty string + absPath = gfile.Abs("") + t.Assert(len(absPath) > 0, true) + t.Assert(filepath.IsAbs(absPath), true) + }) +} + +func Test_Name(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test with file extension + t.Assert(gfile.Name("/var/www/file.js"), "file") + t.Assert(gfile.Name("file.js"), "file") + + // Test with multiple dots + t.Assert(gfile.Name("/var/www/file.min.js"), "file.min") + t.Assert(gfile.Name("archive.tar.gz"), "archive.tar") + + // Test without extension + t.Assert(gfile.Name("/var/www/file"), "file") + t.Assert(gfile.Name("file"), "file") + + // Test with hidden file (dot file) + t.Assert(gfile.Name(".gitignore"), "") + t.Assert(gfile.Name(".hidden.txt"), ".hidden") + + // Test with directory path + t.Assert(gfile.Name("/var/www/"), "www") + + // Test with only extension + t.Assert(gfile.Name(".txt"), "") + }) +} diff --git a/os/gfile/gfile_z_unit_time_test.go b/os/gfile/gfile_z_unit_time_test.go index fb83d1faa6a..1e94976068a 100644 --- a/os/gfile/gfile_z_unit_time_test.go +++ b/os/gfile/gfile_z_unit_time_test.go @@ -55,3 +55,36 @@ func Test_MTimeMillisecond(t *testing.T) { t.Assert(gfile.MTimestampMilli(""), -1) }) } + +func Test_MTimestamp(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + file1 = "/testfile_mtimestamp.txt" + err error + fileobj os.FileInfo + ) + + createTestFile(file1, "") + defer delTestFiles(file1) + fileobj, err = os.Stat(testpath() + file1) + t.AssertNil(err) + + // Test MTimestamp returns correct unix timestamp + timestamp := gfile.MTimestamp(testpath() + file1) + t.Assert(timestamp, fileobj.ModTime().Unix()) + t.Assert(timestamp > 0, true) + + // Test with non-existent file + t.Assert(gfile.MTimestamp("/nonexistent_file_12345.txt"), -1) + + // Test with empty path + t.Assert(gfile.MTimestamp(""), -1) + }) + + // Test MTimestamp with directory + gtest.C(t, func(t *gtest.T) { + tempDir := gfile.Temp() + timestamp := gfile.MTimestamp(tempDir) + t.Assert(timestamp > 0, true) + }) +} From dd21d0dbceac2077e78398cfde82eacb4db83151 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Dec 2025 06:55:19 +0000 Subject: [PATCH 5/5] Apply gci import order changes --- os/gfile/gfile_z_unit_replace_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/os/gfile/gfile_z_unit_replace_test.go b/os/gfile/gfile_z_unit_replace_test.go index 748afc064a9..d3877970a78 100644 --- a/os/gfile/gfile_z_unit_replace_test.go +++ b/os/gfile/gfile_z_unit_replace_test.go @@ -117,11 +117,11 @@ func Test_ReplaceDir(t *testing.T) { // Test recursive replacement gtest.C(t, func(t *gtest.T) { var ( - dirName = "/testdir_replace_recursive_" + gconv.String(gtime.TimestampNano()) - subDirName = dirName + "/subdir" - fileName1 = dirName + "/test1.txt" - fileName2 = subDirName + "/test2.txt" - content = "hello world" + dirName = "/testdir_replace_recursive_" + gconv.String(gtime.TimestampNano()) + subDirName = dirName + "/subdir" + fileName1 = dirName + "/test1.txt" + fileName2 = subDirName + "/test2.txt" + content = "hello world" ) createDir(dirName) createDir(subDirName)