diff --git a/cmd/gf/go.mod b/cmd/gf/go.mod index 1f091cf678e..a46f187466f 100644 --- a/cmd/gf/go.mod +++ b/cmd/gf/go.mod @@ -9,10 +9,6 @@ require ( github.com/gogf/gf/contrib/drivers/oracle/v2 v2.9.6 github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.9.6 github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.6 - github.com/gogf/gf/contrib/drivers/tidb/v2 v2.9.6 - github.com/gogf/gf/contrib/drivers/oceanbase/v2 v2.9.6 - github.com/gogf/gf/contrib/drivers/gaussdb/v2 v2.9.6 - github.com/gogf/gf/contrib/drivers/mariadb/v2 v2.9.6 github.com/gogf/gf/v2 v2.9.6 github.com/gogf/selfupdate v0.0.0-20231215043001-5c48c528462f github.com/olekukonko/tablewriter v1.1.0 diff --git a/cmd/gf/internal/cmd/cmd_run.go b/cmd/gf/internal/cmd/cmd_run.go index 224972d8901..7306b7e5e00 100644 --- a/cmd/gf/internal/cmd/cmd_run.go +++ b/cmd/gf/internal/cmd/cmd_run.go @@ -33,12 +33,18 @@ type cRun struct { g.Meta `name:"run" usage:"{cRunUsage}" brief:"{cRunBrief}" eg:"{cRunEg}" dc:"{cRunDc}"` } +type watchPath struct { + Path string + Recursive bool +} + type cRunApp struct { - File string // Go run file name. - Path string // Directory storing built binary. - Options string // Extra "go run" options. - Args string // Custom arguments. - WatchPaths []string // Watch paths for live reload. + File string // Go run file name. + Path string // Directory storing built binary. + Options string // Extra "go run" options. + Args string // Custom arguments. + WatchPaths []string // Watch paths for live reload. + IgnorePatterns []string // Custom ignore patterns. } const ( @@ -48,43 +54,47 @@ const ( gf run main.go gf run main.go --args "server -p 8080" gf run main.go -mod=vendor -gf run main.go -w "manifest/config/*.yaml" +gf run main.go -w internal,api +gf run main.go -i ".git,node_modules" ` cRunDc = ` The "run" command is used for running go codes with hot-compiled-like feature, which compiles and runs the go codes asynchronously when codes change. ` - cRunFileBrief = `building file path.` - cRunPathBrief = `output directory path for built binary file. it's "./" in default` - cRunExtraBrief = `the same options as "go run"/"go build" except some options as follows defined` - cRunArgsBrief = `custom arguments for your process` - cRunWatchPathsBrief = `watch additional paths for live reload, separated by ",". i.e. "manifest/config/*.yaml"` + cRunFileBrief = `building file path.` + cRunPathBrief = `output directory path for built binary file. it's "./" in default` + cRunExtraBrief = `the same options as "go run"/"go build" except some options as follows defined` + cRunArgsBrief = `custom arguments for your process` + cRunWatchPathsBrief = `watch additional paths for live reload, separated by ",". i.e. "internal,api"` + cRunIgnorePatternBrief = `custom ignore patterns for watch, separated by ",". i.e. ".git,node_modules". default patterns: node_modules, vendor, .*, _*. Glob syntax: "*" matches any chars, "?" matches single char, "[abc]" matches char class. Note: patterns match directory names only, not paths` ) var process *gproc.Process func init() { gtag.Sets(g.MapStrStr{ - `cRunUsage`: cRunUsage, - `cRunBrief`: cRunBrief, - `cRunEg`: cRunEg, - `cRunDc`: cRunDc, - `cRunFileBrief`: cRunFileBrief, - `cRunPathBrief`: cRunPathBrief, - `cRunExtraBrief`: cRunExtraBrief, - `cRunArgsBrief`: cRunArgsBrief, - `cRunWatchPathsBrief`: cRunWatchPathsBrief, + `cRunUsage`: cRunUsage, + `cRunBrief`: cRunBrief, + `cRunEg`: cRunEg, + `cRunDc`: cRunDc, + `cRunFileBrief`: cRunFileBrief, + `cRunPathBrief`: cRunPathBrief, + `cRunExtraBrief`: cRunExtraBrief, + `cRunArgsBrief`: cRunArgsBrief, + `cRunWatchPathsBrief`: cRunWatchPathsBrief, + `cRunIgnorePatternBrief`: cRunIgnorePatternBrief, }) } type ( cRunInput struct { - g.Meta `name:"run" config:"gfcli.run"` - File string `name:"FILE" arg:"true" brief:"{cRunFileBrief}" v:"required"` - Path string `name:"path" short:"p" brief:"{cRunPathBrief}" d:"./"` - Extra string `name:"extra" short:"e" brief:"{cRunExtraBrief}"` - Args string `name:"args" short:"a" brief:"{cRunArgsBrief}"` - WatchPaths []string `name:"watchPaths" short:"w" brief:"{cRunWatchPathsBrief}"` + g.Meta `name:"run" config:"gfcli.run"` + File string `name:"FILE" arg:"true" brief:"{cRunFileBrief}" v:"required"` + Path string `name:"path" short:"p" brief:"{cRunPathBrief}" d:"./"` + Extra string `name:"extra" short:"e" brief:"{cRunExtraBrief}"` + Args string `name:"args" short:"a" brief:"{cRunArgsBrief}"` + WatchPaths []string `name:"watchPaths" short:"w" brief:"{cRunWatchPathsBrief}"` + IgnorePatterns []string `name:"ignorePatterns" short:"i" brief:"{cRunIgnorePatternBrief}"` } cRunOutput struct{} ) @@ -101,17 +111,25 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err mlog.Fatalf(`command "go" not found in your environment, please install golang first to proceed this command`) } - if len(in.WatchPaths) == 1 { - in.WatchPaths = strings.Split(in.WatchPaths[0], ",") + // Parse comma-separated values in WatchPaths + if len(in.WatchPaths) > 0 { + in.WatchPaths = parseCommaSeparatedArgs(in.WatchPaths) mlog.Printf("watchPaths: %v", in.WatchPaths) } + // Parse comma-separated values in IgnorePatterns + if len(in.IgnorePatterns) > 0 { + in.IgnorePatterns = parseCommaSeparatedArgs(in.IgnorePatterns) + mlog.Printf("ignorePatterns: %v", in.IgnorePatterns) + } + app := &cRunApp{ - File: in.File, - Path: filepath.FromSlash(in.Path), - Options: in.Extra, - Args: in.Args, - WatchPaths: in.WatchPaths, + File: in.File, + Path: filepath.FromSlash(in.Path), + Options: in.Extra, + Args: in.Args, + WatchPaths: in.WatchPaths, + IgnorePatterns: in.IgnorePatterns, } dirty := gtype.NewBool() @@ -121,6 +139,7 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err return } + // Check if the file extension is 'go'. if gfile.ExtName(event.Path) != "go" { return } @@ -138,15 +157,11 @@ func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err err }) } - if len(app.WatchPaths) > 0 { - for _, path := range app.WatchPaths { - _, err = gfsnotify.Add(gfile.RealPath(path), callbackFunc) - if err != nil { - mlog.Fatal(err) - } - } - } else { - _, err = gfsnotify.Add(gfile.RealPath("."), callbackFunc) + // Get directories to watch (recursive or non-recursive monitoring). + watchPaths := app.getWatchPaths() + for _, wp := range watchPaths { + option := gfsnotify.WatchOption{NoRecursive: !wp.Recursive} + _, err = gfsnotify.Add(wp.Path, callbackFunc, option) if err != nil { mlog.Fatal(err) } @@ -249,35 +264,181 @@ func (app *cRunApp) End(ctx context.Context, sig os.Signal, outputPath string) { } func (app *cRunApp) genOutputPath() (outputPath string) { - var renamePath string outputPath = gfile.Join(app.Path, gfile.Name(app.File)) if runtime.GOOS == "windows" { outputPath += ".exe" if gfile.Exists(outputPath) { - renamePath = outputPath + "~" + renamePath := outputPath + "~" if err := gfile.Rename(outputPath, renamePath); err != nil { mlog.Print(err) } + // Clean up the renamed old binary file + defer func() { + if gfile.Exists(renamePath) { + _ = gfile.Remove(renamePath) + } + }() } } return filepath.FromSlash(outputPath) } -func matchWatchPaths(watchPaths []string, eventPath string) bool { - for _, path := range watchPaths { - absPath, err := filepath.Abs(path) - if err != nil { - mlog.Printf("match watchPath '%s' error: %s", path, err.Error()) +// getWatchPaths uses DFS to find the minimal set of directories to watch. +// Rule: if a directory and all its descendants have no ignored subdirectories, watch it; +// otherwise, recurse into valid children and watch the current directory non-recursively. +func (app *cRunApp) getWatchPaths() []watchPath { + roots := []string{"."} + if len(app.WatchPaths) > 0 { + roots = app.WatchPaths + } + + // Use custom ignore patterns if provided, otherwise use default. + ignorePatterns := defaultIgnorePatterns + if len(app.IgnorePatterns) > 0 { + ignorePatterns = app.IgnorePatterns + } + + var watchPaths []watchPath + + for _, root := range roots { + absRoot := gfile.RealPath(root) + if absRoot == "" { + mlog.Printf("watch path '%s' not found, skipping", root) continue } - matched, err := filepath.Match(absPath, eventPath) - if err != nil { - mlog.Printf("match watchPath '%s' error: %s", path, err.Error()) + if isIgnoredDirName(absRoot, ignorePatterns) { continue } - if matched { + app.collectWatchPaths(absRoot, ignorePatterns, &watchPaths) + } + + if len(watchPaths) == 0 { + mlog.Printf("no directories to watch, using current directory") + if absCur := gfile.RealPath("."); absCur != "" { + return []watchPath{{Path: absCur, Recursive: true}} + } + return []watchPath{{Path: ".", Recursive: true}} + } + + mlog.Printf("watching %d paths", len(watchPaths)) + for _, wp := range watchPaths { + recursiveStr := "recursive" + if !wp.Recursive { + recursiveStr = "non-recursive" + } + mlog.Debugf(" - %s (%s)", wp.Path, recursiveStr) + } + return watchPaths +} + +// collectWatchPaths performs a DFS traversal to collect the minimal set of directories to watch. +// Returns true if the directory or any of its descendants contains ignored directories. +// Rule: if a directory has no ignored descendants at any depth, watch it recursively; +// otherwise, watch it non-recursively and recurse into valid children. +func (app *cRunApp) collectWatchPaths(dir string, ignorePatterns []string, watchPaths *[]watchPath) bool { + entries, err := gfile.ScanDir(dir, "*", false) + if err != nil { + mlog.Printf("scan directory '%s' error: %s", dir, err.Error()) + // If we can't scan the directory, add it to watch list as fallback + *watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: true}) + return false + } + + // First pass: identify valid subdirectories and check for directly ignored children + var validSubDirs []string + hasIgnoredChild := false + for _, entry := range entries { + if !gfile.IsDir(entry) { + continue + } + if isIgnoredDirName(entry, ignorePatterns) { + hasIgnoredChild = true + } else { + validSubDirs = append(validSubDirs, entry) + } + } + + // If already has ignored child, we know this dir needs non-recursive watch + if hasIgnoredChild { + *watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: false}) + for _, subDir := range validSubDirs { + app.collectWatchPaths(subDir, ignorePatterns, watchPaths) + } + return true + } + + // No ignored children, but need to check descendants recursively + // Collect results from all subdirectories first + subResults := make([]bool, len(validSubDirs)) + subWatchPaths := make([][]watchPath, len(validSubDirs)) + hasIgnoredDescendant := false + + for i, subDir := range validSubDirs { + var subPaths []watchPath + subResults[i] = app.collectWatchPaths(subDir, ignorePatterns, &subPaths) + subWatchPaths[i] = subPaths + if subResults[i] { + hasIgnoredDescendant = true + } + } + + if !hasIgnoredDescendant { + // No ignored descendants at any depth, watch this directory recursively + *watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: true}) + return false + } + + // Has ignored descendants, watch current directory non-recursively + // and add all collected subdirectory watch paths + *watchPaths = append(*watchPaths, watchPath{Path: dir, Recursive: false}) + for _, subPaths := range subWatchPaths { + *watchPaths = append(*watchPaths, subPaths...) + } + return true +} + +// defaultIgnorePatterns contains glob patterns for directory names that should be ignored when watching. +// These directories typically contain third-party code or non-source files. +// Supported glob syntax (filepath.Match): +// - "*" matches any sequence of non-separator characters +// - "?" matches any single non-separator character +// - "[abc]" matches any character in the bracket +// - "[a-z]" matches any character in the range +// - "[^abc]" or "[!abc]" matches any character not in the bracket +// +// Note: patterns match directory base names only, not full paths (no "/" or path separators allowed). +var defaultIgnorePatterns = []string{ + "node_modules", + "vendor", + ".*", // All hidden directories (covers .git, .svn, .hg, .idea, .vscode, etc.) + "_*", // Directories starting with underscore +} + +// isIgnoredDirName checks if a directory name matches any ignored pattern. +// It accepts either a full path or just the directory name, but only matches against the base name. +// Note: patterns should not contain "/" as they only match directory names, not paths. +func isIgnoredDirName(name string, ignorePatterns []string) bool { + baseName := gfile.Basename(name) + for _, pattern := range ignorePatterns { + if matched, _ := filepath.Match(pattern, baseName); matched { return true } } return false } + +// parseCommaSeparatedArgs parses command line arguments that may contain comma-separated values. +// It handles both single argument with commas (e.g., "a,b,c") and multiple arguments. +func parseCommaSeparatedArgs(args []string) []string { + var result []string + for _, arg := range args { + parts := strings.Split(arg, ",") + for _, part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed != "" { + result = append(result, trimmed) + } + } + } + return result +} diff --git a/cmd/gf/internal/cmd/cmd_z_unit_run_test.go b/cmd/gf/internal/cmd/cmd_z_unit_run_test.go new file mode 100644 index 00000000000..b5fc4b13ef8 --- /dev/null +++ b/cmd/gf/internal/cmd/cmd_z_unit_run_test.go @@ -0,0 +1,336 @@ +// Copyright GoFrame gf 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 cmd + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/test/gtest" +) + +func Test_cRunApp_getWatchPaths_Basic(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + app := &cRunApp{ + WatchPaths: []string{"."}, + } + watchPaths := app.getWatchPaths() + + t.AssertGT(len(watchPaths), 0) + for _, v := range watchPaths { + t.Log(v) + } + }) +} + +func Test_cRunApp_getWatchPaths_EmptyWatchPaths(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + app := &cRunApp{ + WatchPaths: []string{}, + } + watchPaths := app.getWatchPaths() + + // Should default to current directory "." + t.AssertGT(len(watchPaths), 0) + }) +} + +func Test_cRunApp_getWatchPaths_CustomIgnorePattern(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + app := &cRunApp{ + WatchPaths: []string{"testdata"}, + IgnorePatterns: []string{"2572"}, + } + watchPaths := app.getWatchPaths() + + // Ensure the "2572" directory is not watched directly. + for _, wp := range watchPaths { + t.Log("watch path:", wp) + t.Assert(strings.HasSuffix(wp.Path, "2572"), false) + } + t.AssertGT(len(watchPaths), 0) + }) +} + +func Test_cRunApp_getWatchPaths_WithIgnoredDirectories(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create a temporary directory structure for testing + tempDir := gfile.Temp("gf_run_test") + defer gfile.Remove(tempDir) + + // Create directory structure: + // tempDir/ + // ├── src/ + // │ ├── api/ + // │ └── internal/ + // ├── vendor/ <-- ignored + // └── node_modules/ <-- ignored + gfile.Mkdir(filepath.Join(tempDir, "src", "api")) + gfile.Mkdir(filepath.Join(tempDir, "src", "internal")) + gfile.Mkdir(filepath.Join(tempDir, "vendor")) + gfile.Mkdir(filepath.Join(tempDir, "node_modules")) + + app := &cRunApp{ + WatchPaths: []string{tempDir}, + } + watchPaths := app.getWatchPaths() + + // Should watch tempDir non-recursively (to catch top-level files) and src recursively + t.Assert(len(watchPaths), 2) + // First path is tempDir (non-recursive) + t.Assert(watchPaths[0].Path, tempDir) + t.Assert(watchPaths[0].Recursive, false) + // Second path is src (recursive, since it has no ignored descendants) + t.Assert(watchPaths[1].Path, filepath.Join(tempDir, "src")) + t.Assert(watchPaths[1].Recursive, true) + }) +} + +func Test_cRunApp_getWatchPaths_NoIgnoredDirectories(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create a temporary directory structure without ignored directories + tempDir := gfile.Temp("gf_run_test_no_ignore") + defer gfile.Remove(tempDir) + + // Create directory structure without ignored patterns: + // tempDir/ + // ├── src/ + // │ ├── api/ + // │ └── internal/ + gfile.Mkdir(filepath.Join(tempDir, "src", "api")) + gfile.Mkdir(filepath.Join(tempDir, "src", "internal")) + + app := &cRunApp{ + WatchPaths: []string{tempDir}, + } + watchPaths := app.getWatchPaths() + + // Should watch the root directory recursively since no ignored directories exist + t.Assert(len(watchPaths), 1) + t.Assert(watchPaths[0].Path, tempDir) + t.Assert(watchPaths[0].Recursive, true) + }) +} + +func Test_cRunApp_getWatchPaths_CustomIgnorePatterns(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create a temporary directory structure + tempDir := gfile.Temp("gf_run_test_custom_ignore") + defer gfile.Remove(tempDir) + + // Create directory structure: + // tempDir/ + // ├── src/ + // │ ├── api/ + // │ └── internal/ + // ├── build/ <-- ignored + // └── dist/ <-- ignored + gfile.Mkdir(filepath.Join(tempDir, "src", "api")) + gfile.Mkdir(filepath.Join(tempDir, "src", "internal")) + gfile.Mkdir(filepath.Join(tempDir, "build")) + gfile.Mkdir(filepath.Join(tempDir, "dist")) + + app := &cRunApp{ + WatchPaths: []string{tempDir}, + IgnorePatterns: []string{"build", "dist"}, + } + watchPaths := app.getWatchPaths() + + // Should watch tempDir non-recursively and src recursively + t.Assert(len(watchPaths), 2) + t.Assert(watchPaths[0].Path, tempDir) + t.Assert(watchPaths[0].Recursive, false) + t.Assert(watchPaths[1].Path, filepath.Join(tempDir, "src")) + t.Assert(watchPaths[1].Recursive, true) + }) +} + +func Test_cRunApp_getWatchPaths_DeepNestedStructure(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create a deep nested directory structure + tempDir := gfile.Temp("gf_run_test_deep") + defer gfile.Remove(tempDir) + + // Create deep directory structure: + // tempDir/ + // ├── a/ + // │ ├── b/ + // │ │ └── c/ + // │ └── vendor/ <-- ignored + // └── d/ + gfile.Mkdir(filepath.Join(tempDir, "a", "b", "c")) + gfile.Mkdir(filepath.Join(tempDir, "a", "vendor")) + gfile.Mkdir(filepath.Join(tempDir, "d")) + + app := &cRunApp{ + WatchPaths: []string{tempDir}, + } + watchPaths := app.getWatchPaths() + + // Should watch individual valid directories due to ignored vendor directory + t.AssertGT(len(watchPaths), 0) + + // Verify that vendor directory is not in watch list + for _, wp := range watchPaths { + t.Assert(strings.Contains(wp.Path, "vendor"), false) + } + }) +} + +func Test_cRunApp_getWatchPaths_MultipleRoots(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create multiple temporary directories + tempDir1 := gfile.Temp("gf_run_test_multi1") + tempDir2 := gfile.Temp("gf_run_test_multi2") + defer gfile.Remove(tempDir1) + defer gfile.Remove(tempDir2) + + gfile.Mkdir(filepath.Join(tempDir1, "src")) + gfile.Mkdir(filepath.Join(tempDir2, "api")) + + app := &cRunApp{ + WatchPaths: []string{tempDir1, tempDir2}, + } + watchPaths := app.getWatchPaths() + + // Should watch both root directories recursively + t.Assert(len(watchPaths), 2) + + // Both directories should be in the watch list + foundDir1, foundDir2 := false, false + for _, wp := range watchPaths { + if wp.Path == tempDir1 { + foundDir1 = true + t.Assert(wp.Recursive, true) + } + if wp.Path == tempDir2 { + foundDir2 = true + t.Assert(wp.Recursive, true) + } + } + t.Assert(foundDir1, true) + t.Assert(foundDir2, true) + }) +} + +func Test_cRunApp_getWatchPaths_NonExistentDirectory(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + app := &cRunApp{ + WatchPaths: []string{"/non/existent/path"}, + } + watchPaths := app.getWatchPaths() + + // Should fall back to current directory when no valid paths found + t.AssertGT(len(watchPaths), 0) + + // Should contain current directory + currentDir, _ := os.Getwd() + foundCurrentDir := false + for _, wp := range watchPaths { + if wp.Path == currentDir { + foundCurrentDir = true + break + } + } + t.Assert(foundCurrentDir, true) + }) +} + +func Test_isIgnoredDirName(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test default ignore patterns + t.Assert(isIgnoredDirName("node_modules", defaultIgnorePatterns), true) + t.Assert(isIgnoredDirName("vendor", defaultIgnorePatterns), true) + t.Assert(isIgnoredDirName(".git", defaultIgnorePatterns), true) + t.Assert(isIgnoredDirName("_private", defaultIgnorePatterns), true) + t.Assert(isIgnoredDirName("src", defaultIgnorePatterns), false) + t.Assert(isIgnoredDirName("api", defaultIgnorePatterns), false) + + // Test custom ignore patterns + customPatterns := []string{"build", "dist", "*.tmp"} + t.Assert(isIgnoredDirName("build", customPatterns), true) + t.Assert(isIgnoredDirName("dist", customPatterns), true) + t.Assert(isIgnoredDirName("test.tmp", customPatterns), true) + t.Assert(isIgnoredDirName("src", customPatterns), false) + }) +} + +func Test_cRunApp_getWatchPaths_DeeplyNestedIgnore(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create a temporary directory structure with deeply nested ignored directory + tempDir := gfile.Temp("gf_run_test_deeply_nested") + defer gfile.Remove(tempDir) + + // Create directory structure: + // tempDir/ + // ├── a/ + // │ ├── b/ + // │ │ ├── c/ + // │ │ │ └── vendor/ <-- deeply nested ignored (4 levels) + // │ │ └── d/ + // │ └── e/ + // └── f/ + gfile.Mkdir(filepath.Join(tempDir, "a", "b", "c", "vendor")) + gfile.Mkdir(filepath.Join(tempDir, "a", "b", "d")) + gfile.Mkdir(filepath.Join(tempDir, "a", "e")) + gfile.Mkdir(filepath.Join(tempDir, "f")) + + app := &cRunApp{ + WatchPaths: []string{tempDir}, + } + watchPaths := app.getWatchPaths() + + // Expected watch paths: + // 1. tempDir (non-recursive) - has ignored descendant + // 2. a (non-recursive) - has ignored descendant in b/c/vendor + // 3. b (non-recursive) - has ignored descendant in c/vendor + // 4. c (non-recursive) - has ignored child vendor + // 5. d (recursive) - no ignored descendants + // 6. e (recursive) - no ignored descendants + // 7. f (recursive) - no ignored descendants + + t.AssertGT(len(watchPaths), 0) + + // Verify vendor is not in watch paths + for _, wp := range watchPaths { + t.Assert(strings.Contains(wp.Path, "vendor"), false) + } + + // Find specific paths and verify their recursive flags + foundF := false + for _, wp := range watchPaths { + if wp.Path == filepath.Join(tempDir, "f") { + foundF = true + t.Assert(wp.Recursive, true) // f should be recursive (no ignored descendants) + } + } + t.Assert(foundF, true) + }) +} + +func Test_cRunApp_getWatchPaths_EmptyDirectory(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create an empty temporary directory + tempDir := gfile.Temp("gf_run_test_empty") + defer gfile.Remove(tempDir) + + gfile.Mkdir(tempDir) + + app := &cRunApp{ + WatchPaths: []string{tempDir}, + } + watchPaths := app.getWatchPaths() + + // Empty directory should be watched recursively (no ignored descendants) + t.Assert(len(watchPaths), 1) + t.Assert(watchPaths[0].Path, tempDir) + t.Assert(watchPaths[0].Recursive, true) + }) +} diff --git a/cmd/gf/internal/cmd/testdata/fix/fix25_content.go b/cmd/gf/internal/cmd/testdata/fix/fix25_content.go index 934ebd2a831..9b555245850 100644 --- a/cmd/gf/internal/cmd/testdata/fix/fix25_content.go +++ b/cmd/gf/internal/cmd/testdata/fix/fix25_content.go @@ -1,13 +1,10 @@ package testdata import ( - "fmt" "testing" - "time" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" - "github.com/gogf/gf/v2/test/gtest" "github.com/gogf/gf/v2/util/guid" )