mirror of
https://gitee.com/johng/gf
synced 2026-06-06 02:25:47 +08:00
This pull request introduces a new glob pattern matching utility to the `gfile` package, adding support for advanced glob patterns including the "**" (globstar) operator, which matches across directory boundaries, similar to bash and gitignore. It also includes a comprehensive set of unit tests to verify the correctness and cross-platform compatibility of the new functionality. **Glob pattern matching feature:** * Added `MatchGlob` function to `gfile`, which extends `filepath.Match` with support for the "**" (globstar) pattern, enabling recursive directory matching and more flexible file pattern matching. * Implemented internal helpers (`matchGlobstar` and `doMatchGlobstar`) to handle normalization of path separators and recursive matching logic for patterns containing "**". **Testing and validation:** * Added `gfile_z_unit_match_test.go` with extensive unit tests covering basic glob patterns, globstar usage, prefix/suffix combinations, multiple globstars, edge cases, and Windows path compatibility to ensure robust and cross-platform behavior. --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
266
os/gfile/gfile_match.go
Normal file
266
os/gfile/gfile_match.go
Normal file
@ -0,0 +1,266 @@
|
||||
// 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"
|
||||
"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
|
||||
// - '[^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, 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, "**") {
|
||||
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 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) {
|
||||
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
|
||||
}
|
||||
|
||||
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, "/")
|
||||
// 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 against full path component
|
||||
if !strings.HasPrefix(name, prefix) {
|
||||
return false, nil
|
||||
}
|
||||
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, "/")
|
||||
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 doMatchGlobstarMemo(suffix, "", memo)
|
||||
}
|
||||
|
||||
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 := doMatchGlobstarMemo(suffix, remaining, memo)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if matched {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
600
os/gfile/gfile_z_unit_match_test.go
Normal file
600
os/gfile/gfile_z_unit_match_test.go
Normal file
@ -0,0 +1,600 @@
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
246
os/gfile/gfile_z_unit_replace_test.go
Normal file
246
os/gfile/gfile_z_unit_replace_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
150
os/gfile/gfile_z_unit_sort_test.go
Normal file
150
os/gfile/gfile_z_unit_sort_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
@ -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"), "")
|
||||
})
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user