mirror of
https://gitee.com/johng/gf
synced 2026-06-06 16:21:40 +08:00
feat(cmd/gf): improve gf run watching (#4573)
This pull request introduces a significant enhancement to the `gf run` command, focusing on improving the directory watching mechanism for hot-reload functionality. The main improvements include a more intelligent and efficient algorithm for determining which directories to watch (recursively or non-recursively), support for custom ignore patterns, and a comprehensive set of unit tests to ensure correctness. Additionally, some outdated database drivers were removed from the dependencies. **Key changes:** ### Directory Watching Improvements * Refactored the directory watching logic in `cmd_run.go` to use a DFS-based algorithm that minimizes the number of watched directories while respecting ignored patterns. Directories and their descendants without ignored subdirectories are watched recursively; otherwise, non-recursive watches are set, and valid children are recursed into. This results in more efficient and accurate hot-reload behavior. (`cmd/gf/internal/cmd/cmd_run.go`) * Added support for custom ignore patterns via the new `-i`/`--ignorePatterns` flag, allowing users to specify directories to be excluded from watching. Default ignored patterns include `node_modules`, `vendor`, hidden directories, and directories starting with an underscore. (`cmd/gf/internal/cmd/cmd_run.go`) [[1]](diffhunk://#diff-406a97355fde87f9a6fc118877430c2720632eb94eb2aaba72025571e5fe5146R97) [[2]](diffhunk://#diff-406a97355fde87f9a6fc118877430c2720632eb94eb2aaba72025571e5fe5146L61-R69) [[3]](diffhunk://#diff-406a97355fde87f9a6fc118877430c2720632eb94eb2aaba72025571e5fe5146L104-R132) * Improved parsing of comma-separated arguments for both watch paths and ignore patterns to support flexible CLI usage. (`cmd/gf/internal/cmd/cmd_run.go`) ### User Experience and Documentation * Updated help messages, usage examples, and documentation to reflect the new features and more intuitive CLI options for specifying watch paths and ignore patterns. (`cmd/gf/internal/cmd/cmd_run.go`) [[1]](diffhunk://#diff-406a97355fde87f9a6fc118877430c2720632eb94eb2aaba72025571e5fe5146L51-R58) [[2]](diffhunk://#diff-406a97355fde87f9a6fc118877430c2720632eb94eb2aaba72025571e5fe5146R85) ### Testing * Added a comprehensive unit test suite for the new `getWatchPaths` logic, covering various scenarios including custom ignore patterns, deeply nested structures, multiple roots, non-existent directories, and edge cases. (`cmd/gf/internal/cmd/cmd_z_unit_run_test.go`) ### Dependency Cleanup * Removed unused database driver dependencies from `go.mod` to streamline the project dependencies. (`cmd/gf/go.mod`) These changes collectively make the hot-reload feature more robust, configurable, and efficient, while ensuring maintainability through thorough testing. --------- 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:
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
336
cmd/gf/internal/cmd/cmd_z_unit_run_test.go
Normal file
336
cmd/gf/internal/cmd/cmd_z_unit_run_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
@ -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"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user