feat(os/gfile): add MatchGlob function with globstar support (#4570) (#4574)

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:
hailaz
2025-12-26 16:37:45 +08:00
committed by GitHub
parent 4d6c7e3d3a
commit 90564f9fb0
7 changed files with 1449 additions and 6 deletions

266
os/gfile/gfile_match.go Normal file
View 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
}

View File

@ -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)
})
}

View 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)
})
}

View 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)
})
}

View 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)
})
}

View File

@ -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"), "")
})
}

View File

@ -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)
})
}