From 4f43b40a18093f23ab5511e4c3e570a736c82f09 Mon Sep 17 00:00:00 2001 From: Jack Ling <34231795+lingcoder@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:07:52 +0800 Subject: [PATCH 01/11] test(cmd/gf): add unit tests for geninit package (#4640) ## Summary - Add comprehensive unit tests for the `geninit` package which handles project initialization from templates - 17 new test cases covering core functionality ## Test Coverage | Function | Tests | Description | |----------|-------|-------------| | `ParseGitURL` | 7 | Git URL parsing with various formats | | `IsSubdirRepo` | 3 | Subdirectory detection | | `GetModuleNameFromGoMod` | 3 | Module name extraction | | `ASTReplacer` | 2 | Import path replacement | | `findGoFiles` | 2 | Go file discovery | ## Test plan - [x] All 17 tests pass locally - [x] No modifications to existing code - [x] New test file only: `geninit_z_unit_test.go` --- .../cmd/geninit/geninit_z_unit_test.go | 359 ++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 cmd/gf/internal/cmd/geninit/geninit_z_unit_test.go diff --git a/cmd/gf/internal/cmd/geninit/geninit_z_unit_test.go b/cmd/gf/internal/cmd/geninit/geninit_z_unit_test.go new file mode 100644 index 000000000..6f05eddf0 --- /dev/null +++ b/cmd/gf/internal/cmd/geninit/geninit_z_unit_test.go @@ -0,0 +1,359 @@ +// 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 geninit + +import ( + "context" + "path/filepath" + "testing" + + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/guid" +) + +func Test_ParseGitURL_Basic(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test basic github URL + info, err := ParseGitURL("github.com/gogf/gf") + t.AssertNil(err) + t.Assert(info.Host, "github.com") + t.Assert(info.Owner, "gogf") + t.Assert(info.Repo, "gf") + t.Assert(info.SubPath, "") + t.Assert(info.Branch, "main") + t.Assert(info.CloneURL, "https://github.com/gogf/gf.git") + }) +} + +func Test_ParseGitURL_WithHTTPS(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test URL with https prefix + info, err := ParseGitURL("https://github.com/gogf/gf") + t.AssertNil(err) + t.Assert(info.Host, "github.com") + t.Assert(info.Owner, "gogf") + t.Assert(info.Repo, "gf") + }) +} + +func Test_ParseGitURL_WithGitSuffix(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test URL with .git suffix + info, err := ParseGitURL("github.com/gogf/gf.git") + t.AssertNil(err) + t.Assert(info.Host, "github.com") + t.Assert(info.Owner, "gogf") + t.Assert(info.Repo, "gf") + }) +} + +func Test_ParseGitURL_WithSubPath(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test URL with subdirectory + info, err := ParseGitURL("github.com/gogf/examples/httpserver/jwt") + t.AssertNil(err) + t.Assert(info.Host, "github.com") + t.Assert(info.Owner, "gogf") + t.Assert(info.Repo, "examples") + t.Assert(info.SubPath, "httpserver/jwt") + t.Assert(info.CloneURL, "https://github.com/gogf/examples.git") + }) +} + +func Test_ParseGitURL_WithTreeBranch(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test GitHub web URL with /tree/branch/ + info, err := ParseGitURL("github.com/gogf/examples/tree/develop/httpserver/jwt") + t.AssertNil(err) + t.Assert(info.Host, "github.com") + t.Assert(info.Owner, "gogf") + t.Assert(info.Repo, "examples") + t.Assert(info.Branch, "develop") + t.Assert(info.SubPath, "httpserver/jwt") + }) +} + +func Test_ParseGitURL_WithVersion(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test URL with version suffix + info, err := ParseGitURL("github.com/gogf/gf/cmd/gf/v2@v2.9.7") + t.AssertNil(err) + t.Assert(info.Host, "github.com") + t.Assert(info.Owner, "gogf") + t.Assert(info.Repo, "gf") + t.Assert(info.SubPath, "cmd/gf/v2") + }) +} + +func Test_ParseGitURL_Invalid(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test invalid URL (too short) + _, err := ParseGitURL("github.com/gogf") + t.AssertNE(err, nil) + }) +} + +func Test_IsSubdirRepo_NotSubdir(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Standard Go module paths should not be detected as subdirectory + t.Assert(IsSubdirRepo("github.com/gogf/gf"), false) + t.Assert(IsSubdirRepo("github.com/gogf/gf/v2"), false) + }) +} + +func Test_IsSubdirRepo_GoModuleWithCmd(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Go module paths with common patterns should not be detected as subdirectory + t.Assert(IsSubdirRepo("github.com/gogf/gf/cmd/gf/v2"), false) + t.Assert(IsSubdirRepo("github.com/gogf/gf/contrib/drivers/mysql/v2"), false) + }) +} + +func Test_IsSubdirRepo_ActualSubdir(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Actual subdirectories should be detected + t.Assert(IsSubdirRepo("github.com/gogf/examples/httpserver/jwt"), true) + t.Assert(IsSubdirRepo("github.com/gogf/examples/grpc/basic"), true) + }) +} + +func Test_GetModuleNameFromGoMod_Valid(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create temp directory with go.mod + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Write go.mod file + goModContent := `module github.com/test/myproject + +go 1.21 + +require ( + github.com/gogf/gf/v2 v2.9.0 +) +` + err = gfile.PutContents(filepath.Join(tempDir, "go.mod"), goModContent) + t.AssertNil(err) + + // Test extraction + moduleName := GetModuleNameFromGoMod(tempDir) + t.Assert(moduleName, "github.com/test/myproject") + }) +} + +func Test_GetModuleNameFromGoMod_NoFile(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create temp directory without go.mod + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Test extraction - should return empty + moduleName := GetModuleNameFromGoMod(tempDir) + t.Assert(moduleName, "") + }) +} + +func Test_GetModuleNameFromGoMod_SimpleModule(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create temp directory with simple go.mod + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Write simple go.mod file + goModContent := `module main + +go 1.21 +` + err = gfile.PutContents(filepath.Join(tempDir, "go.mod"), goModContent) + t.AssertNil(err) + + // Test extraction + moduleName := GetModuleNameFromGoMod(tempDir) + t.Assert(moduleName, "main") + }) +} + +func Test_ASTReplacer_ReplaceInFile(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create temp directory + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Create a Go file with imports + goFileContent := `package main + +import ( + "fmt" + + "github.com/old/module/internal/service" + "github.com/old/module/pkg/utils" + "github.com/other/package" +) + +func main() { + fmt.Println("Hello") +} +` + goFilePath := filepath.Join(tempDir, "main.go") + err = gfile.PutContents(goFilePath, goFileContent) + t.AssertNil(err) + + // Replace imports + replacer := NewASTReplacer("github.com/old/module", "github.com/new/project") + err = replacer.ReplaceInFile(context.Background(), goFilePath) + t.AssertNil(err) + + // Verify replacement + content := gfile.GetContents(goFilePath) + t.Assert(gfile.Exists(goFilePath), true) + + // Check that old imports are replaced + t.AssertNE(content, "") + t.Assert(contains(content, `"github.com/new/project/internal/service"`), true) + t.Assert(contains(content, `"github.com/new/project/pkg/utils"`), true) + + // Check that other imports are not affected + t.Assert(contains(content, `"github.com/other/package"`), true) + t.Assert(contains(content, `"fmt"`), true) + }) +} + +func Test_ASTReplacer_ReplaceInDir(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create temp directory structure + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Create subdirectory + subDir := filepath.Join(tempDir, "sub") + err = gfile.Mkdir(subDir) + t.AssertNil(err) + + // Create main.go + mainContent := `package main + +import "github.com/old/module/sub" + +func main() { + sub.Hello() +} +` + err = gfile.PutContents(filepath.Join(tempDir, "main.go"), mainContent) + t.AssertNil(err) + + // Create sub/sub.go + subContent := `package sub + +import "github.com/old/module/pkg" + +func Hello() { + pkg.Do() +} +` + err = gfile.PutContents(filepath.Join(subDir, "sub.go"), subContent) + t.AssertNil(err) + + // Replace imports in directory + replacer := NewASTReplacer("github.com/old/module", "github.com/new/project") + err = replacer.ReplaceInDir(context.Background(), tempDir) + t.AssertNil(err) + + // Verify main.go replacement + mainResult := gfile.GetContents(filepath.Join(tempDir, "main.go")) + t.Assert(contains(mainResult, `"github.com/new/project/sub"`), true) + + // Verify sub/sub.go replacement + subResult := gfile.GetContents(filepath.Join(subDir, "sub.go")) + t.Assert(contains(subResult, `"github.com/new/project/pkg"`), true) + }) +} + +func Test_findGoFiles(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create temp directory structure + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Create subdirectories + subDir := filepath.Join(tempDir, "sub") + err = gfile.Mkdir(subDir) + t.AssertNil(err) + + // Create various files + err = gfile.PutContents(filepath.Join(tempDir, "main.go"), "package main") + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(tempDir, "readme.md"), "# README") + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(subDir, "sub.go"), "package sub") + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(subDir, "data.json"), "{}") + t.AssertNil(err) + + // Find Go files + files, err := findGoFiles(tempDir) + t.AssertNil(err) + + // Should find exactly 2 Go files + t.Assert(len(files), 2) + + // Verify file names + hasMain := false + hasSub := false + for _, f := range files { + if filepath.Base(f) == "main.go" { + hasMain = true + } + if filepath.Base(f) == "sub.go" { + hasSub = true + } + } + t.Assert(hasMain, true) + t.Assert(hasSub, true) + }) +} + +func Test_findGoFiles_EmptyDir(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create empty temp directory + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Find Go files + files, err := findGoFiles(tempDir) + t.AssertNil(err) + t.Assert(len(files), 0) + }) +} + +// Helper function to check if string contains substring +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsAt(s, substr)) +} + +func containsAt(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} From 82d4d77e5628a8b65eb146d240cc864ea75721ae Mon Sep 17 00:00:00 2001 From: Jack Ling <34231795+lingcoder@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:09:38 +0800 Subject: [PATCH 02/11] test(cmd/gf): add unit tests for genenums package (#4641) ## Summary - Add comprehensive unit tests for the `genenums` package which handles enum parsing and JSON export - 13 new test cases covering core functionality ## Test Coverage | Function | Tests | Description | |----------|-------|-------------| | `NewEnumsParser` | 2 | Parser initialization | | `Export` | 7 | JSON export with various types | | `ParsePackages` | 2 | Integration with Go packages | | `EnumItem` | 1 | Data structure | | `getStandardPackages` | 1 | Standard library detection | ## Test plan - [x] All 13 tests pass locally - [x] No modifications to existing code - [x] New test file only: `genenums_z_unit_test.go` --- .../cmd/genenums/genenums_z_unit_test.go | 368 ++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 cmd/gf/internal/cmd/genenums/genenums_z_unit_test.go diff --git a/cmd/gf/internal/cmd/genenums/genenums_z_unit_test.go b/cmd/gf/internal/cmd/genenums/genenums_z_unit_test.go new file mode 100644 index 000000000..d3a3a58fc --- /dev/null +++ b/cmd/gf/internal/cmd/genenums/genenums_z_unit_test.go @@ -0,0 +1,368 @@ +// 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 genenums + +import ( + "go/constant" + "path/filepath" + "testing" + + "golang.org/x/tools/go/packages" + + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/guid" +) + +func Test_NewEnumsParser(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test creating parser without prefixes + p := NewEnumsParser(nil) + t.AssertNE(p, nil) + t.Assert(len(p.enums), 0) + t.Assert(len(p.prefixes), 0) + t.AssertNE(p.parsedPkg, nil) + t.AssertNE(p.standardPackages, nil) + }) +} + +func Test_NewEnumsParser_WithPrefixes(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test creating parser with prefixes + prefixes := []string{"github.com/gogf", "github.com/test"} + p := NewEnumsParser(prefixes) + t.AssertNE(p, nil) + t.Assert(len(p.prefixes), 2) + t.Assert(p.prefixes[0], "github.com/gogf") + t.Assert(p.prefixes[1], "github.com/test") + }) +} + +func Test_EnumsParser_Export_Empty(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test exporting empty enums + p := NewEnumsParser(nil) + result := p.Export() + t.Assert(result, "{}") + }) +} + +func Test_EnumsParser_Export_WithEnums(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test exporting with manually added enums + p := NewEnumsParser(nil) + + // Add some test enums + p.enums = []EnumItem{ + { + Name: "StatusActive", + Value: "1", + Type: "pkg.Status", + Kind: constant.Int, + }, + { + Name: "StatusInactive", + Value: "0", + Type: "pkg.Status", + Kind: constant.Int, + }, + { + Name: "TypeA", + Value: "type_a", + Type: "pkg.Type", + Kind: constant.String, + }, + } + + result := p.Export() + t.AssertNE(result, "") + + // Parse the result to verify - use raw map to avoid gjson path issues with "." + var resultMap map[string][]interface{} + err := gjson.DecodeTo(result, &resultMap) + t.AssertNil(err) + + // Verify Status type has 2 values + statusValues := resultMap["pkg.Status"] + t.Assert(len(statusValues), 2) + + // Verify Type type has 1 value + typeValues := resultMap["pkg.Type"] + t.Assert(len(typeValues), 1) + t.Assert(typeValues[0], "type_a") + }) +} + +func Test_EnumsParser_Export_IntValues(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + p := NewEnumsParser(nil) + p.enums = []EnumItem{ + {Name: "One", Value: "1", Type: "pkg.Int", Kind: constant.Int}, + {Name: "Two", Value: "2", Type: "pkg.Int", Kind: constant.Int}, + {Name: "Negative", Value: "-5", Type: "pkg.Int", Kind: constant.Int}, + } + + result := p.Export() + var resultMap map[string][]interface{} + err := gjson.DecodeTo(result, &resultMap) + t.AssertNil(err) + + values := resultMap["pkg.Int"] + t.Assert(len(values), 3) + // Int values should be exported as integers (stored as float64 in JSON) + t.Assert(values[0], float64(1)) + t.Assert(values[1], float64(2)) + t.Assert(values[2], float64(-5)) + }) +} + +func Test_EnumsParser_Export_FloatValues(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + p := NewEnumsParser(nil) + p.enums = []EnumItem{ + {Name: "Pi", Value: "3.14159", Type: "pkg.Float", Kind: constant.Float}, + {Name: "E", Value: "2.71828", Type: "pkg.Float", Kind: constant.Float}, + } + + result := p.Export() + var resultMap map[string][]interface{} + err := gjson.DecodeTo(result, &resultMap) + t.AssertNil(err) + + values := resultMap["pkg.Float"] + t.Assert(len(values), 2) + }) +} + +func Test_EnumsParser_Export_BoolValues(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + p := NewEnumsParser(nil) + p.enums = []EnumItem{ + {Name: "True", Value: "true", Type: "pkg.Bool", Kind: constant.Bool}, + {Name: "False", Value: "false", Type: "pkg.Bool", Kind: constant.Bool}, + } + + result := p.Export() + var resultMap map[string][]interface{} + err := gjson.DecodeTo(result, &resultMap) + t.AssertNil(err) + + values := resultMap["pkg.Bool"] + t.Assert(len(values), 2) + t.Assert(values[0], true) + t.Assert(values[1], false) + }) +} + +func Test_EnumsParser_Export_StringValues(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + p := NewEnumsParser(nil) + p.enums = []EnumItem{ + {Name: "Hello", Value: "hello", Type: "pkg.Str", Kind: constant.String}, + {Name: "World", Value: "world", Type: "pkg.Str", Kind: constant.String}, + } + + result := p.Export() + var resultMap map[string][]interface{} + err := gjson.DecodeTo(result, &resultMap) + t.AssertNil(err) + + values := resultMap["pkg.Str"] + t.Assert(len(values), 2) + t.Assert(values[0], "hello") + t.Assert(values[1], "world") + }) +} + +func Test_EnumsParser_Export_MixedTypes(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + p := NewEnumsParser(nil) + p.enums = []EnumItem{ + {Name: "IntVal", Value: "42", Type: "pkg.IntType", Kind: constant.Int}, + {Name: "StrVal", Value: "test", Type: "pkg.StrType", Kind: constant.String}, + {Name: "BoolVal", Value: "true", Type: "pkg.BoolType", Kind: constant.Bool}, + } + + result := p.Export() + var resultMap map[string][]interface{} + err := gjson.DecodeTo(result, &resultMap) + t.AssertNil(err) + + // Each type should have its own array + t.Assert(len(resultMap["pkg.IntType"]), 1) + t.Assert(len(resultMap["pkg.StrType"]), 1) + t.Assert(len(resultMap["pkg.BoolType"]), 1) + }) +} + +func Test_EnumItem_Structure(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test EnumItem structure + item := EnumItem{ + Name: "TestEnum", + Value: "test_value", + Type: "github.com/test/pkg.EnumType", + Kind: constant.String, + } + + t.Assert(item.Name, "TestEnum") + t.Assert(item.Value, "test_value") + t.Assert(item.Type, "github.com/test/pkg.EnumType") + t.Assert(item.Kind, constant.String) + }) +} + +func Test_EnumsParser_ParsePackages_Integration(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create a temporary directory with a Go package containing enums + // Note: The module path must contain "/" for enums to be parsed + // (the parser skips std types without "/" in the type name) + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Create go.mod with a path containing "/" + goModContent := `module github.com/test/enumtest + +go 1.21 +` + err = gfile.PutContents(filepath.Join(tempDir, "go.mod"), goModContent) + t.AssertNil(err) + + // Create a Go file with enum definitions + enumsContent := `package enumtest + +type Status int + +const ( + StatusActive Status = 1 + StatusInactive Status = 0 +) + +type Color string + +const ( + ColorRed Color = "red" + ColorGreen Color = "green" + ColorBlue Color = "blue" +) +` + err = gfile.PutContents(filepath.Join(tempDir, "enums.go"), enumsContent) + t.AssertNil(err) + + // Load the package + cfg := &packages.Config{ + Dir: tempDir, + Mode: pkgLoadMode, + Tests: false, + } + pkgs, err := packages.Load(cfg) + t.AssertNil(err) + t.Assert(len(pkgs) > 0, true) + + // Parse the packages + p := NewEnumsParser(nil) + p.ParsePackages(pkgs) + + // Export and verify - result should contain parsed enums + result := p.Export() + // Verify the export contains some data + t.Assert(len(result) > 2, true) // More than just "{}" + + // Parse result as raw map to handle keys with "/" + var resultMap map[string][]interface{} + err = gjson.DecodeTo(result, &resultMap) + t.AssertNil(err) + + // Verify Status enum was parsed (type will be "github.com/test/enumtest.Status") + statusKey := "github.com/test/enumtest.Status" + statusValues, hasStatus := resultMap[statusKey] + t.Assert(hasStatus, true) + t.Assert(len(statusValues), 2) + + // Verify Color enum was parsed + colorKey := "github.com/test/enumtest.Color" + colorValues, hasColor := resultMap[colorKey] + t.Assert(hasColor, true) + t.Assert(len(colorValues), 3) + }) +} + +func Test_EnumsParser_ParsePackages_WithPrefixes(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Create a temporary directory with a Go package + tempDir := gfile.Temp(guid.S()) + err := gfile.Mkdir(tempDir) + t.AssertNil(err) + defer gfile.Remove(tempDir) + + // Create go.mod with a specific module name + goModContent := `module github.com/allowed/pkg + +go 1.21 +` + err = gfile.PutContents(filepath.Join(tempDir, "go.mod"), goModContent) + t.AssertNil(err) + + // Create a Go file with enum definitions + enumsContent := `package pkg + +type Status int + +const ( + StatusOK Status = 1 +) +` + err = gfile.PutContents(filepath.Join(tempDir, "enums.go"), enumsContent) + t.AssertNil(err) + + // Load the package + cfg := &packages.Config{ + Dir: tempDir, + Mode: pkgLoadMode, + Tests: false, + } + pkgs, err := packages.Load(cfg) + t.AssertNil(err) + + // Parse with prefix filter that matches + p := NewEnumsParser([]string{"github.com/allowed"}) + p.ParsePackages(pkgs) + + result := p.Export() + // Should have enums because prefix matches + t.AssertNE(result, "{}") + + // Parse with prefix filter that doesn't match + p2 := NewEnumsParser([]string{"github.com/other"}) + p2.ParsePackages(pkgs) + + result2 := p2.Export() + // Should be empty because prefix doesn't match + t.Assert(result2, "{}") + }) +} + +func Test_getStandardPackages(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + stdPkgs := getStandardPackages() + t.AssertNE(stdPkgs, nil) + t.Assert(len(stdPkgs) > 0, true) + + // Verify some common standard packages are included + _, hasFmt := stdPkgs["fmt"] + t.Assert(hasFmt, true) + + _, hasOs := stdPkgs["os"] + t.Assert(hasOs, true) + + _, hasContext := stdPkgs["context"] + t.Assert(hasContext, true) + }) +} From bf2997e9cca64e3166d205572093f1f5620c51e1 Mon Sep 17 00:00:00 2001 From: Jack Ling <34231795+lingcoder@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:10:20 +0800 Subject: [PATCH 03/11] test(cmd/gf): add unit tests for pack command (#4642) ## Summary - Add comprehensive unit tests for the `pack` command which handles resource file packing - 8 new test cases covering core functionality ## Test Coverage | Test | Description | |------|-------------| | Test_Pack_ToGoFile | Pack files to .go file | | Test_Pack_ToBinaryFile | Pack files to binary file | | Test_Pack_MultipleSources | Pack multiple source directories | | Test_Pack_WithPrefix | Pack with prefix option | | Test_Pack_WithKeepPath | Pack with keepPath option | | Test_Pack_AutoPackageName | Auto-detect package name from directory | | Test_Pack_EmptySource | Handle empty source directory | | Test_Pack_NestedDirectories | Handle deeply nested directory structure | ## Test plan - [x] All 8 tests pass locally - [x] No modifications to existing code - [x] New test file only: `cmd_z_unit_pack_test.go` --- cmd/gf/internal/cmd/cmd_z_unit_pack_test.go | 346 ++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 cmd/gf/internal/cmd/cmd_z_unit_pack_test.go diff --git a/cmd/gf/internal/cmd/cmd_z_unit_pack_test.go b/cmd/gf/internal/cmd/cmd_z_unit_pack_test.go new file mode 100644 index 000000000..ac39e8da0 --- /dev/null +++ b/cmd/gf/internal/cmd/cmd_z_unit_pack_test.go @@ -0,0 +1,346 @@ +// 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 ( + "context" + "path/filepath" + "testing" + + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/guid" +) + +func Test_Pack_ToGoFile(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + srcPath = gfile.Temp(guid.S()) + dstPath = gfile.Temp(guid.S()) + dstFile = filepath.Join(dstPath, "packed", "data.go") + ) + // Create source directory with test files + err := gfile.Mkdir(srcPath) + t.AssertNil(err) + defer gfile.Remove(srcPath) + + err = gfile.Mkdir(dstPath) + t.AssertNil(err) + defer gfile.Remove(dstPath) + + // Create test files + err = gfile.PutContents(filepath.Join(srcPath, "test.txt"), "hello world") + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(srcPath, "test.json"), `{"key":"value"}`) + t.AssertNil(err) + + // Create packed directory + err = gfile.Mkdir(filepath.Join(dstPath, "packed")) + t.AssertNil(err) + + // Pack to go file + _, err = Pack.Index(context.Background(), cPackInput{ + Src: srcPath, + Dst: dstFile, + Name: "packed", + }) + t.AssertNil(err) + + // Verify output file exists + t.Assert(gfile.Exists(dstFile), true) + + // Verify it's a valid Go file + content := gfile.GetContents(dstFile) + t.Assert(gstr.Contains(content, "package packed"), true) + t.Assert(gstr.Contains(content, "func init()"), true) + }) +} + +func Test_Pack_ToBinaryFile(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + srcPath = gfile.Temp(guid.S()) + dstPath = gfile.Temp(guid.S()) + dstFile = filepath.Join(dstPath, "data.bin") + ) + // Create source directory with test files + err := gfile.Mkdir(srcPath) + t.AssertNil(err) + defer gfile.Remove(srcPath) + + err = gfile.Mkdir(dstPath) + t.AssertNil(err) + defer gfile.Remove(dstPath) + + // Create test file + err = gfile.PutContents(filepath.Join(srcPath, "test.txt"), "binary content") + t.AssertNil(err) + + // Pack to binary file (no Name specified) + _, err = Pack.Index(context.Background(), cPackInput{ + Src: srcPath, + Dst: dstFile, + }) + t.AssertNil(err) + + // Verify output file exists + t.Assert(gfile.Exists(dstFile), true) + + // Verify it's a binary file (not a Go file) + content := gfile.GetContents(dstFile) + t.Assert(gstr.Contains(content, "package"), false) + }) +} + +func Test_Pack_MultipleSources(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + srcPath1 = gfile.Temp(guid.S()) + srcPath2 = gfile.Temp(guid.S()) + dstPath = gfile.Temp(guid.S()) + dstFile = filepath.Join(dstPath, "packed", "multi.go") + ) + // Create source directories + err := gfile.Mkdir(srcPath1) + t.AssertNil(err) + defer gfile.Remove(srcPath1) + + err = gfile.Mkdir(srcPath2) + t.AssertNil(err) + defer gfile.Remove(srcPath2) + + err = gfile.Mkdir(dstPath) + t.AssertNil(err) + defer gfile.Remove(dstPath) + + // Create test files in each source + err = gfile.PutContents(filepath.Join(srcPath1, "file1.txt"), "content1") + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(srcPath2, "file2.txt"), "content2") + t.AssertNil(err) + + // Create packed directory + err = gfile.Mkdir(filepath.Join(dstPath, "packed")) + t.AssertNil(err) + + // Pack multiple sources (comma-separated) + _, err = Pack.Index(context.Background(), cPackInput{ + Src: srcPath1 + "," + srcPath2, + Dst: dstFile, + Name: "packed", + }) + t.AssertNil(err) + + // Verify output file exists + t.Assert(gfile.Exists(dstFile), true) + }) +} + +func Test_Pack_WithPrefix(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + srcPath = gfile.Temp(guid.S()) + dstPath = gfile.Temp(guid.S()) + dstFile = filepath.Join(dstPath, "packed", "prefix.go") + ) + // Create source directory + err := gfile.Mkdir(srcPath) + t.AssertNil(err) + defer gfile.Remove(srcPath) + + err = gfile.Mkdir(dstPath) + t.AssertNil(err) + defer gfile.Remove(dstPath) + + // Create test file + err = gfile.PutContents(filepath.Join(srcPath, "test.txt"), "with prefix") + t.AssertNil(err) + + // Create packed directory + err = gfile.Mkdir(filepath.Join(dstPath, "packed")) + t.AssertNil(err) + + // Pack with prefix + _, err = Pack.Index(context.Background(), cPackInput{ + Src: srcPath, + Dst: dstFile, + Name: "packed", + Prefix: "/static", + }) + t.AssertNil(err) + + // Verify output file exists + t.Assert(gfile.Exists(dstFile), true) + }) +} + +func Test_Pack_WithKeepPath(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + srcPath = gfile.Temp(guid.S()) + dstPath = gfile.Temp(guid.S()) + dstFile = filepath.Join(dstPath, "packed", "keeppath.go") + ) + // Create source directory with subdirectory + err := gfile.Mkdir(srcPath) + t.AssertNil(err) + defer gfile.Remove(srcPath) + + err = gfile.Mkdir(dstPath) + t.AssertNil(err) + defer gfile.Remove(dstPath) + + // Create subdirectory and file + subDir := filepath.Join(srcPath, "subdir") + err = gfile.Mkdir(subDir) + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(subDir, "test.txt"), "keeppath content") + t.AssertNil(err) + + // Create packed directory + err = gfile.Mkdir(filepath.Join(dstPath, "packed")) + t.AssertNil(err) + + // Pack with keepPath + _, err = Pack.Index(context.Background(), cPackInput{ + Src: srcPath, + Dst: dstFile, + Name: "packed", + KeepPath: true, + }) + t.AssertNil(err) + + // Verify output file exists + t.Assert(gfile.Exists(dstFile), true) + }) +} + +func Test_Pack_AutoPackageName(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + srcPath = gfile.Temp(guid.S()) + dstPath = gfile.Temp(guid.S()) + dstFile = filepath.Join(dstPath, "mypackage", "data.go") + ) + // Create source directory + err := gfile.Mkdir(srcPath) + t.AssertNil(err) + defer gfile.Remove(srcPath) + + err = gfile.Mkdir(dstPath) + t.AssertNil(err) + defer gfile.Remove(dstPath) + + // Create test file + err = gfile.PutContents(filepath.Join(srcPath, "test.txt"), "auto package name") + t.AssertNil(err) + + // Create mypackage directory + err = gfile.Mkdir(filepath.Join(dstPath, "mypackage")) + t.AssertNil(err) + + // Pack without Name - should use directory name "mypackage" + _, err = Pack.Index(context.Background(), cPackInput{ + Src: srcPath, + Dst: dstFile, + // Name not specified, should be auto-detected as "mypackage" + }) + t.AssertNil(err) + + // Verify output file exists and has correct package name + t.Assert(gfile.Exists(dstFile), true) + content := gfile.GetContents(dstFile) + t.Assert(gstr.Contains(content, "package mypackage"), true) + }) +} + +func Test_Pack_EmptySource(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + srcPath = gfile.Temp(guid.S()) + dstPath = gfile.Temp(guid.S()) + dstFile = filepath.Join(dstPath, "packed", "empty.go") + ) + // Create empty source directory + err := gfile.Mkdir(srcPath) + t.AssertNil(err) + defer gfile.Remove(srcPath) + + err = gfile.Mkdir(dstPath) + t.AssertNil(err) + defer gfile.Remove(dstPath) + + // Create packed directory + err = gfile.Mkdir(filepath.Join(dstPath, "packed")) + t.AssertNil(err) + + // Pack empty directory + _, err = Pack.Index(context.Background(), cPackInput{ + Src: srcPath, + Dst: dstFile, + Name: "packed", + }) + t.AssertNil(err) + + // Verify output file exists (even if source is empty) + t.Assert(gfile.Exists(dstFile), true) + }) +} + +func Test_Pack_NestedDirectories(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + srcPath = gfile.Temp(guid.S()) + dstPath = gfile.Temp(guid.S()) + dstFile = filepath.Join(dstPath, "packed", "nested.go") + ) + // Create source directory with nested structure + err := gfile.Mkdir(srcPath) + t.AssertNil(err) + defer gfile.Remove(srcPath) + + err = gfile.Mkdir(dstPath) + t.AssertNil(err) + defer gfile.Remove(dstPath) + + // Create nested directories and files + level1 := filepath.Join(srcPath, "level1") + level2 := filepath.Join(level1, "level2") + level3 := filepath.Join(level2, "level3") + err = gfile.Mkdir(level3) + t.AssertNil(err) + + err = gfile.PutContents(filepath.Join(srcPath, "root.txt"), "root") + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(level1, "l1.txt"), "level1") + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(level2, "l2.txt"), "level2") + t.AssertNil(err) + err = gfile.PutContents(filepath.Join(level3, "l3.txt"), "level3") + t.AssertNil(err) + + // Create packed directory + err = gfile.Mkdir(filepath.Join(dstPath, "packed")) + t.AssertNil(err) + + // Pack nested directories + _, err = Pack.Index(context.Background(), cPackInput{ + Src: srcPath, + Dst: dstFile, + Name: "packed", + }) + t.AssertNil(err) + + // Verify output file exists + t.Assert(gfile.Exists(dstFile), true) + + // Verify content includes all files + content := gfile.GetContents(dstFile) + t.Assert(gstr.Contains(content, "package packed"), true) + }) +} From 2d05fb426f3286a1818c8e39eb74fe575bd188a4 Mon Sep 17 00:00:00 2001 From: Jack Ling <34231795+lingcoder@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:10:56 +0800 Subject: [PATCH 04/11] test(cmd/gf): enhance unit tests for fix command (#4643) ## Summary - Enhance unit tests for the `fix` command's `doFixV25Content` function - 5 new test cases added (total: 6) ## New Test Cases | Test | Description | |------|-------------| | Test_Fix_doFixV25Content_WithReplacement | Verify actual replacement is made | | Test_Fix_doFixV25Content_NoMatch | Handle content without patterns | | Test_Fix_doFixV25Content_MultipleMatches | Handle multiple occurrences | | Test_Fix_doFixV25Content_EmptyContent | Handle empty content | | Test_Fix_doFixV25Content_ComplexPath | Handle complex URL paths | ## Test plan - [x] All 6 tests pass locally - [x] Only added new test cases to existing test file - [x] No modifications to non-test code --- cmd/gf/internal/cmd/cmd_z_unit_fix_test.go | 80 ++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/cmd/gf/internal/cmd/cmd_z_unit_fix_test.go b/cmd/gf/internal/cmd/cmd_z_unit_fix_test.go index ad53e4129..bd456f786 100644 --- a/cmd/gf/internal/cmd/cmd_z_unit_fix_test.go +++ b/cmd/gf/internal/cmd/cmd_z_unit_fix_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/text/gstr" ) func Test_Fix_doFixV25Content(t *testing.T) { @@ -22,3 +23,82 @@ func Test_Fix_doFixV25Content(t *testing.T) { t.AssertNil(err) }) } + +func Test_Fix_doFixV25Content_WithReplacement(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + f = cFix{} + content = `s.BindHookHandlerByMap("/path", map[string]ghttp.HandlerFunc{ + ghttp.HookBeforeServe: func(r *ghttp.Request) {}, + })` + ) + newContent, err := f.doFixV25Content(content) + t.AssertNil(err) + // Verify the replacement was made + t.Assert(gstr.Contains(newContent, "map[ghttp.HookName]ghttp.HandlerFunc"), true) + t.Assert(gstr.Contains(newContent, "map[string]ghttp.HandlerFunc"), false) + }) +} + +func Test_Fix_doFixV25Content_NoMatch(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + f = cFix{} + content = `package main + +func main() { + fmt.Println("Hello World") +} +` + ) + newContent, err := f.doFixV25Content(content) + t.AssertNil(err) + // Content should remain unchanged + t.Assert(newContent, content) + }) +} + +func Test_Fix_doFixV25Content_MultipleMatches(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + f = cFix{} + content = ` +s.BindHookHandlerByMap("/path1", map[string]ghttp.HandlerFunc{}) +s.BindHookHandlerByMap("/path2", map[string]ghttp.HandlerFunc{}) +` + ) + newContent, err := f.doFixV25Content(content) + t.AssertNil(err) + // Both should be replaced + count := gstr.Count(newContent, "map[ghttp.HookName]ghttp.HandlerFunc") + t.Assert(count, 2) + }) +} + +func Test_Fix_doFixV25Content_EmptyContent(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + f = cFix{} + content = "" + ) + newContent, err := f.doFixV25Content(content) + t.AssertNil(err) + t.Assert(newContent, "") + }) +} + +func Test_Fix_doFixV25Content_ComplexPath(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + f = cFix{} + content = `s.BindHookHandlerByMap("/api/v1/user/{id}/profile", map[string]ghttp.HandlerFunc{ + ghttp.HookBeforeServe: func(r *ghttp.Request) { + r.Response.Write("before") + }, + })` + ) + newContent, err := f.doFixV25Content(content) + t.AssertNil(err) + t.Assert(gstr.Contains(newContent, "map[ghttp.HookName]ghttp.HandlerFunc"), true) + }) +} From 626fc629efb6e330db98328134a55fd469b21cb0 Mon Sep 17 00:00:00 2001 From: Jack Ling <34231795+lingcoder@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:11:45 +0800 Subject: [PATCH 05/11] test(cmd/gf): enhance integration tests for gen pb command (#4644) ## Summary - Add 2 new integration test cases for `gf gen pb` command - `TestGenPb_MultipleTags`: tests multiple validation tags (v:required, v:#Id > 0, v:email) and dc tags - `TestGenPb_NestedMessage`: tests nested message structures with various tag types ## Test Data - Add `testdata/genpb/multiple_tags.proto` - proto file with multiple tag annotations - Add `testdata/genpb/nested_message.proto` - proto file with nested message structures ## Test Plan - [x] Run `go test -v -run "TestGenPb" ./...` - all 4 tests pass (2 existing + 2 new) --- cmd/gf/internal/cmd/cmd_z_unit_gen_pb_test.go | 73 +++++++++++++++++++ .../cmd/testdata/genpb/multiple_tags.proto | 22 ++++++ .../cmd/testdata/genpb/nested_message.proto | 21 ++++++ 3 files changed, 116 insertions(+) create mode 100644 cmd/gf/internal/cmd/testdata/genpb/multiple_tags.proto create mode 100644 cmd/gf/internal/cmd/testdata/genpb/nested_message.proto diff --git a/cmd/gf/internal/cmd/cmd_z_unit_gen_pb_test.go b/cmd/gf/internal/cmd/cmd_z_unit_gen_pb_test.go index 80cf1e814..6c3cff236 100644 --- a/cmd/gf/internal/cmd/cmd_z_unit_gen_pb_test.go +++ b/cmd/gf/internal/cmd/cmd_z_unit_gen_pb_test.go @@ -88,3 +88,76 @@ func TestGenPbIssue3953(t *testing.T) { t.Assert(gstr.Contains(genContent, notExceptText), false) }) } + +func TestGenPb_MultipleTags(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + outputPath = gfile.Temp(guid.S()) + outputApiPath = filepath.Join(outputPath, "api") + outputCtrlPath = filepath.Join(outputPath, "controller") + + protobufFolder = gtest.DataPath("genpb") + in = genpb.CGenPbInput{ + Path: protobufFolder, + OutputApi: outputApiPath, + OutputCtrl: outputCtrlPath, + } + err error + ) + err = gfile.Mkdir(outputApiPath) + t.AssertNil(err) + err = gfile.Mkdir(outputCtrlPath) + t.AssertNil(err) + defer gfile.Remove(outputPath) + + _, err = genpb.CGenPb{}.Pb(ctx, in) + t.AssertNil(err) + + // Test multiple_tags.proto output + genContent := gfile.GetContents(filepath.Join(outputApiPath, "multiple_tags.pb.go")) + // Id field should have combined validation tags: v:"required#Id > 0" + t.Assert(gstr.Contains(genContent, `v:"required#Id > 0"`), true) + // Name field should have dc tag from plain comment + t.Assert(gstr.Contains(genContent, `dc:"User name for login"`), true) + // Email field should have combined validation and dc tag + t.Assert(gstr.Contains(genContent, `v:"requiredemail"`), true) + t.Assert(gstr.Contains(genContent, `dc:"User email address"`), true) + }) +} + +func TestGenPb_NestedMessage(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + outputPath = gfile.Temp(guid.S()) + outputApiPath = filepath.Join(outputPath, "api") + outputCtrlPath = filepath.Join(outputPath, "controller") + + protobufFolder = gtest.DataPath("genpb") + in = genpb.CGenPbInput{ + Path: protobufFolder, + OutputApi: outputApiPath, + OutputCtrl: outputCtrlPath, + } + err error + ) + err = gfile.Mkdir(outputApiPath) + t.AssertNil(err) + err = gfile.Mkdir(outputCtrlPath) + t.AssertNil(err) + defer gfile.Remove(outputPath) + + _, err = genpb.CGenPb{}.Pb(ctx, in) + t.AssertNil(err) + + // Test nested_message.proto output + genContent := gfile.GetContents(filepath.Join(outputApiPath, "nested_message.pb.go")) + // Order.OrderId should have v:"required" + t.Assert(gstr.Contains(genContent, `v:"required"`), true) + // Order.Detail should have dc:"Order details" + t.Assert(gstr.Contains(genContent, `dc:"Order details"`), true) + // OrderDetail.Quantity should have v:"min:1" + t.Assert(gstr.Contains(genContent, `v:"min:1"`), true) + // OrderDetail.Price should have v:"min:0.01" + t.Assert(gstr.Contains(genContent, `v:"min:0.01"`), true) + }) +} diff --git a/cmd/gf/internal/cmd/testdata/genpb/multiple_tags.proto b/cmd/gf/internal/cmd/testdata/genpb/multiple_tags.proto new file mode 100644 index 000000000..0f15b4217 --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/genpb/multiple_tags.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package genpb; + +option go_package = "genpb/v1"; + +message UserReq { + // v:required + // v:#Id > 0 + int64 Id = 1; + // User name for login + string Name = 2; + // v:required + // v:email + string Email = 3; // User email address +} + +message UserResp { + int64 Id = 1; + string Name = 2; + string Email = 3; +} diff --git a/cmd/gf/internal/cmd/testdata/genpb/nested_message.proto b/cmd/gf/internal/cmd/testdata/genpb/nested_message.proto new file mode 100644 index 000000000..c7a832f5d --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/genpb/nested_message.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package genpb; + +option go_package = "genpb/v1"; + +message Order { + // v:required + int64 OrderId = 1; + // Order details + OrderDetail Detail = 2; +} + +message OrderDetail { + // v:required + string ProductName = 1; + // v:min:1 + int32 Quantity = 2; + // v:min:0.01 + double Price = 3; +} From dd02af1b2fc83caed4eb7844da93da2e2d4e7567 Mon Sep 17 00:00:00 2001 From: Jack Ling <34231795+lingcoder@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:12:37 +0800 Subject: [PATCH 06/11] test(cmd/gf): enhance integration tests for gen service command (#4645) ## Summary - Add 2 new integration test cases for `gf gen service` command - `Test_Gen_Service_CamelCase`: tests `DstFileNameCase: "Camel"` option to generate service files with CamelCase naming - `Test_Gen_Service_PackagesFilter`: tests `Packages` filter option to generate service files only for specified packages ## Test Plan - [x] Run `go test -v -run "Test_Gen_Service" ./...` - all 5 tests pass (3 existing + 2 new) --- .../cmd/cmd_z_unit_gen_service_test.go | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/cmd/gf/internal/cmd/cmd_z_unit_gen_service_test.go b/cmd/gf/internal/cmd/cmd_z_unit_gen_service_test.go index 40ee7158e..322667dbc 100644 --- a/cmd/gf/internal/cmd/cmd_z_unit_gen_service_test.go +++ b/cmd/gf/internal/cmd/cmd_z_unit_gen_service_test.go @@ -156,3 +156,85 @@ func Test_Issue3835(t *testing.T) { t.Assert(gfile.GetContents(genFile), gfile.GetContents(expectFile)) }) } + +func Test_Gen_Service_CamelCase(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + path = gfile.Temp(guid.S()) + dstFolder = path + filepath.FromSlash("/service") + srvFolder = gtest.DataPath("genservice", "logic") + in = genservice.CGenServiceInput{ + SrcFolder: srvFolder, + DstFolder: dstFolder, + DstFileNameCase: "Camel", + WatchFile: "", + StPattern: "", + Packages: nil, + ImportPrefix: "", + Clear: false, + } + ) + err := gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + defer gfile.Remove(path) + + // Clean up generated logic.go + genSrv := srvFolder + filepath.FromSlash("/logic.go") + defer gfile.Remove(genSrv) + + _, err = genservice.CGenService{}.Service(ctx, in) + t.AssertNil(err) + + // Files should be in CamelCase + files, err := gfile.ScanDir(dstFolder, "*.go", true) + t.AssertNil(err) + t.Assert(files, []string{ + dstFolder + filepath.FromSlash("/Article.go"), + dstFolder + filepath.FromSlash("/Base.go"), + dstFolder + filepath.FromSlash("/Delivery.go"), + dstFolder + filepath.FromSlash("/User.go"), + }) + }) +} + +func Test_Gen_Service_PackagesFilter(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + path = gfile.Temp(guid.S()) + dstFolder = path + filepath.FromSlash("/service") + srvFolder = gtest.DataPath("genservice", "logic") + in = genservice.CGenServiceInput{ + SrcFolder: srvFolder, + DstFolder: dstFolder, + DstFileNameCase: "Snake", + WatchFile: "", + StPattern: "", + Packages: []string{"user"}, + ImportPrefix: "", + Clear: false, + } + ) + err := gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + defer gfile.Remove(path) + + // Clean up generated logic.go + genSrv := srvFolder + filepath.FromSlash("/logic.go") + defer gfile.Remove(genSrv) + + _, err = genservice.CGenService{}.Service(ctx, in) + t.AssertNil(err) + + // Only user.go should be generated + files, err := gfile.ScanDir(dstFolder, "*.go", true) + t.AssertNil(err) + t.Assert(len(files), 1) + t.Assert(files[0], dstFolder+filepath.FromSlash("/user.go")) + }) +} From 9a7df9944ce20f464d18717150d28a63b314adcb Mon Sep 17 00:00:00 2001 From: Jack Ling <34231795+lingcoder@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:14:03 +0800 Subject: [PATCH 07/11] revert(os/gcfg): restore config file priority over env/cmd in GetWithEnv and GetWithCmd (#4647) ## Summary - Reverts the behavior change introduced in PR #4587 (commit caea7ea4b) - Restores v2.9.7 priority behavior: - `GetWithEnv`: config file > environment variable > default value - `GetWithCmd`: config file > command line option > default value ## Related Issue Closes #4074 ## Changes - `os/gcfg/gcfg.go`: Restore original logic that checks config file first, then falls back to env/cmd - `os/gcfg/gcfg_z_example_test.go`: Restore original example test expectations --- os/gcfg/gcfg.go | 34 ++++++++++++++++++++++++++++------ os/gcfg/gcfg_z_example_test.go | 13 ++++++++----- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/os/gcfg/gcfg.go b/os/gcfg/gcfg.go index e71d49bc0..d41f8a187 100644 --- a/os/gcfg/gcfg.go +++ b/os/gcfg/gcfg.go @@ -11,6 +11,8 @@ import ( "context" "github.com/gogf/gf/v2/container/gvar" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/internal/command" "github.com/gogf/gf/v2/internal/intlog" "github.com/gogf/gf/v2/internal/utils" @@ -117,10 +119,20 @@ func (c *Config) Get(ctx context.Context, pattern string, def ...any) (*gvar.Var // // Fetching Rules: Environment arguments are in uppercase format, eg: GF_PACKAGE_VARIABLE. func (c *Config) GetWithEnv(ctx context.Context, pattern string, def ...any) (*gvar.Var, error) { - if v := genv.Get(utils.FormatEnvKey(pattern)); v != nil { - return v, nil + value, err := c.Get(ctx, pattern) + if err != nil && gerror.Code(err) != gcode.CodeNotFound { + return nil, err } - return c.Get(ctx, pattern, def...) + if value == nil { + if v := genv.Get(utils.FormatEnvKey(pattern)); v != nil { + return v, nil + } + if len(def) > 0 { + return gvar.New(def[0]), nil + } + return nil, nil + } + return value, nil } // GetWithCmd returns the configuration value specified by pattern `pattern`. @@ -129,10 +141,20 @@ func (c *Config) GetWithEnv(ctx context.Context, pattern string, def ...any) (*g // // Fetching Rules: Command line arguments are in lowercase format, eg: gf.package.variable. func (c *Config) GetWithCmd(ctx context.Context, pattern string, def ...any) (*gvar.Var, error) { - if v := command.GetOpt(utils.FormatCmdKey(pattern)); v != "" { - return gvar.New(v), nil + value, err := c.Get(ctx, pattern) + if err != nil && gerror.Code(err) != gcode.CodeNotFound { + return nil, err } - return c.Get(ctx, pattern, def...) + if value == nil { + if v := command.GetOpt(utils.FormatCmdKey(pattern)); v != "" { + return gvar.New(v), nil + } + if len(def) > 0 { + return gvar.New(def[0]), nil + } + return nil, nil + } + return value, nil } // Data retrieves and returns all configuration data as map type. diff --git a/os/gcfg/gcfg_z_example_test.go b/os/gcfg/gcfg_z_example_test.go index 3ce4ebe1c..78ea671ba 100644 --- a/os/gcfg/gcfg_z_example_test.go +++ b/os/gcfg/gcfg_z_example_test.go @@ -10,7 +10,6 @@ import ( "fmt" "os" - "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gcfg" "github.com/gogf/gf/v2/os/gcmd" @@ -24,9 +23,10 @@ func ExampleConfig_GetWithEnv() { ctx = gctx.New() ) v, err := g.Cfg().GetWithEnv(ctx, key) - if err == nil { - panic(gerror.New("environment variable is not defined")) + if err != nil { + panic(err) } + fmt.Printf("env:%s\n", v) if err = genv.Set(key, "gf"); err != nil { panic(err) } @@ -37,6 +37,7 @@ func ExampleConfig_GetWithEnv() { fmt.Printf("env:%s", v) // Output: + // env: // env:gf } @@ -46,9 +47,10 @@ func ExampleConfig_GetWithCmd() { ctx = gctx.New() ) v, err := g.Cfg().GetWithCmd(ctx, key) - if err == nil { - panic(gerror.New("command option is not defined")) + if err != nil { + panic(err) } + fmt.Printf("cmd:%s\n", v) // Re-Initialize custom command arguments. os.Args = append(os.Args, fmt.Sprintf(`--%s=yes`, key)) gcmd.Init(os.Args...) @@ -60,6 +62,7 @@ func ExampleConfig_GetWithCmd() { fmt.Printf("cmd:%s", v) // Output: + // cmd: // cmd:yes } From 73560cfe317b3ac69a7794e16cb9990f86255c5a Mon Sep 17 00:00:00 2001 From: Jack Ling <34231795+lingcoder@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:15:06 +0800 Subject: [PATCH 08/11] fix(cmd/gendao): fix overlapping shardingPattern matching issue (#4631) ## Summary - Fix overlapping shardingPattern matching issue where shorter patterns incorrectly match tables meant for longer patterns - Sort shardingPattern by length descending so longer (more specific) patterns are matched first - Add break after successful pattern match to prevent tables from matching multiple patterns ## Problem When `shardingPattern` contains overlapping prefixes like `["a_?", "a_b_?", "a_c_?"]`: - Tables `a_b_1`, `a_b_2` should match `a_b_?` and generate `a_b.go` - Tables `a_c_1`, `a_c_2` should match `a_c_?` and generate `a_c.go` - Tables `a_1`, `a_2` should match `a_?` and generate `a.go` But without this fix, `a_?` (converted to regex `a_(.+)`) would match `a_b_1` first, causing `a_b_?` and `a_c_?` patterns to fail to generate their respective dao files. ## Solution 1. Sort `shardingPattern` by length descending before matching 2. Add `break` after a table matches a pattern to prevent multiple matches ## Test plan - [x] Added integration test `Test_Gen_Dao_Sharding_Overlapping` with overlapping patterns - [x] Added SQL test data file `sharding_overlapping.sql` - [x] Verified 3 separate dao files are generated: `a.go`, `a_b.go`, `a_c.go` Fixes #4603 --- .../cmd/cmd_z_unit_gen_dao_sharding_test.go | 86 +++++++++++++++++++ cmd/gf/internal/cmd/gendao/gendao.go | 17 +++- .../gendao/sharding/sharding_overlapping.sql | 47 ++++++++++ 3 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 cmd/gf/internal/cmd/testdata/gendao/sharding/sharding_overlapping.sql diff --git a/cmd/gf/internal/cmd/cmd_z_unit_gen_dao_sharding_test.go b/cmd/gf/internal/cmd/cmd_z_unit_gen_dao_sharding_test.go index 5a3989667..df2a7a45d 100644 --- a/cmd/gf/internal/cmd/cmd_z_unit_gen_dao_sharding_test.go +++ b/cmd/gf/internal/cmd/cmd_z_unit_gen_dao_sharding_test.go @@ -18,6 +18,92 @@ import ( "github.com/gogf/gf/cmd/gf/v2/internal/cmd/gendao" ) +// Test_Gen_Dao_Sharding_Overlapping tests the fix for issue #4603. +// When sharding patterns have overlapping prefixes (like "a_?", "a_b_?", "a_c_?"), +// longer (more specific) patterns should be matched first. +// https://github.com/gogf/gf/issues/4603 +func Test_Gen_Dao_Sharding_Overlapping(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + err error + db = testDB + tableA1 = "a_1" + tableA2 = "a_2" + tableAB1 = "a_b_1" + tableAB2 = "a_b_2" + tableAC1 = "a_c_1" + tableAC2 = "a_c_2" + sqlFilePath = gtest.DataPath(`gendao`, `sharding`, `sharding_overlapping.sql`) + ) + dropTableWithDb(db, tableA1) + dropTableWithDb(db, tableA2) + dropTableWithDb(db, tableAB1) + dropTableWithDb(db, tableAB2) + dropTableWithDb(db, tableAC1) + dropTableWithDb(db, tableAC2) + t.AssertNil(execSqlFile(db, sqlFilePath)) + defer dropTableWithDb(db, tableA1) + defer dropTableWithDb(db, tableA2) + defer dropTableWithDb(db, tableAB1) + defer dropTableWithDb(db, tableAB2) + defer dropTableWithDb(db, tableAC1) + defer dropTableWithDb(db, tableAC2) + + var ( + path = gfile.Temp(guid.S()) + group = "test" + in = gendao.CGenDaoInput{ + Path: path, + Link: link, + Group: group, + Prefix: "", + // Patterns with overlapping prefixes - order should not matter due to sorting fix + ShardingPattern: []string{ + `a_?`, // shortest, matches a_1, a_2 but also a_b_1, a_c_1 without fix + `a_b_?`, // longer, should match a_b_1, a_b_2 + `a_c_?`, // longer, should match a_c_1, a_c_2 + }, + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + + pwd := gfile.Pwd() + err = gfile.Chdir(path) + t.AssertNil(err) + defer gfile.Chdir(pwd) + defer gfile.RemoveAll(path) + + _, err = gendao.CGenDao{}.Dao(ctx, in) + t.AssertNil(err) + + // Should generate 3 dao files: a.go, a_b.go, a_c.go (plus internal versions) + generatedFiles, err := gfile.ScanDir(path, "*.go", true) + t.AssertNil(err) + // 3 sharding groups * 4 files each (dao, internal, do, entity) = 12 files + t.Assert(len(generatedFiles), 12) + + var ( + daoAContent = gfile.GetContents(gfile.Join(path, "dao", "a.go")) + daoABContent = gfile.GetContents(gfile.Join(path, "dao", "a_b.go")) + daoACContent = gfile.GetContents(gfile.Join(path, "dao", "a_c.go")) + ) + + // Verify each sharding group has correct dao file generated + t.Assert(gstr.Contains(daoAContent, "aShardingHandler"), true) + t.Assert(gstr.Contains(daoAContent, "m.Sharding(gdb.ShardingConfig{"), true) + + t.Assert(gstr.Contains(daoABContent, "aBShardingHandler"), true) + t.Assert(gstr.Contains(daoABContent, "m.Sharding(gdb.ShardingConfig{"), true) + + t.Assert(gstr.Contains(daoACContent, "aCShardingHandler"), true) + t.Assert(gstr.Contains(daoACContent, "m.Sharding(gdb.ShardingConfig{"), true) + }) +} + func Test_Gen_Dao_Sharding(t *testing.T) { gtest.C(t, func(t *gtest.T) { var ( diff --git a/cmd/gf/internal/cmd/gendao/gendao.go b/cmd/gf/internal/cmd/gendao/gendao.go index bc6ad943a..777b24ea1 100644 --- a/cmd/gf/internal/cmd/gendao/gendao.go +++ b/cmd/gf/internal/cmd/gendao/gendao.go @@ -9,6 +9,7 @@ package gendao import ( "context" "fmt" + "sort" "strings" "github.com/olekukonko/tablewriter" @@ -240,13 +241,22 @@ func doGenDaoForArray(ctx context.Context, index int, in CGenDaoInput) { newTableNames = make([]string, len(tableNames)) shardingNewTableSet = gset.NewStrSet() ) + // Sort sharding patterns by length descending, so that longer (more specific) patterns + // are matched first. This prevents shorter patterns like "a_?" from incorrectly matching + // tables that should match longer patterns like "a_b_?" or "a_c_?". + // https://github.com/gogf/gf/issues/4603 + sortedShardingPatterns := make([]string, len(in.ShardingPattern)) + copy(sortedShardingPatterns, in.ShardingPattern) + sort.Slice(sortedShardingPatterns, func(i, j int) bool { + return len(sortedShardingPatterns[i]) > len(sortedShardingPatterns[j]) + }) for i, tableName := range tableNames { newTableName := tableName for _, v := range removePrefixArray { newTableName = gstr.TrimLeftStr(newTableName, v, 1) } - if len(in.ShardingPattern) > 0 { - for _, pattern := range in.ShardingPattern { + if len(sortedShardingPatterns) > 0 { + for _, pattern := range sortedShardingPatterns { var ( match []string regPattern = gstr.Replace(pattern, "?", `(.+)`) @@ -262,10 +272,11 @@ func doGenDaoForArray(ctx context.Context, index int, in CGenDaoInput) { newTableName = gstr.Trim(newTableName, `_.-`) if shardingNewTableSet.Contains(newTableName) { tableNames[i] = "" - continue + break } // Add prefix to sharding table name, if not, the isSharding check would not match. shardingNewTableSet.Add(in.Prefix + newTableName) + break } } newTableName = in.Prefix + newTableName diff --git a/cmd/gf/internal/cmd/testdata/gendao/sharding/sharding_overlapping.sql b/cmd/gf/internal/cmd/testdata/gendao/sharding/sharding_overlapping.sql new file mode 100644 index 000000000..6e6a8e928 --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/gendao/sharding/sharding_overlapping.sql @@ -0,0 +1,47 @@ +-- Test case for issue #4603: overlapping sharding patterns +-- https://github.com/gogf/gf/issues/4603 +-- +-- Patterns: "a_?", "a_b_?", "a_c_?" +-- Expected: a_1/a_2 -> "a", a_b_1/a_b_2 -> "a_b", a_c_1/a_c_2 -> "a_c" + +CREATE TABLE `a_1` +( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(45) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `a_2` +( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(45) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `a_b_1` +( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(45) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `a_b_2` +( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(45) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `a_c_1` +( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(45) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `a_c_2` +( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(45) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; From cee6f499fc71cb7e4266e14547f1102ef14fcc79 Mon Sep 17 00:00:00 2001 From: Jack Ling <34231795+lingcoder@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:15:42 +0800 Subject: [PATCH 09/11] fix(cmd/gf): fix gf gen enums output path error when using relative path (#4636) ## Summary - Fix `gf gen enums` output file created at wrong location when using relative path - Output was incorrectly relative to source directory instead of current working directory - Add `defer gfile.Chdir(originPwd)` to restore original working directory ## Root Cause The code calls `gfile.Chdir(realPath)` to change to source directory before `gfile.PutContents(in.Path, ...)`, causing relative output path to be resolved relative to source directory. ## Solution - Convert output path to absolute using `gfile.Abs()` before `Chdir` - Restore original working directory with `defer` (following `genpb.go` pattern) ## Test Cases - `Test_Gen_Enums_Issue4387_RelativePath` - standard project with relative path - `Test_Gen_Enums_AbsolutePath` - absolute path (should work as before) - `Test_Gen_Enums_Issue4387_Monorepo` - monorepo mode (`cd app/xxx && gf gen enums`) Closes #4387 --- .../internal/cmd/cmd_z_unit_gen_enums_test.go | 158 ++++++++++++++++++ cmd/gf/internal/cmd/genenums/genenums.go | 13 +- .../cmd/testdata/issue/4387/api/types.go | 16 ++ .../internal/cmd/testdata/issue/4387/go.mod | 3 + 4 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 cmd/gf/internal/cmd/cmd_z_unit_gen_enums_test.go create mode 100644 cmd/gf/internal/cmd/testdata/issue/4387/api/types.go create mode 100644 cmd/gf/internal/cmd/testdata/issue/4387/go.mod diff --git a/cmd/gf/internal/cmd/cmd_z_unit_gen_enums_test.go b/cmd/gf/internal/cmd/cmd_z_unit_gen_enums_test.go new file mode 100644 index 000000000..1cee880a7 --- /dev/null +++ b/cmd/gf/internal/cmd/cmd_z_unit_gen_enums_test.go @@ -0,0 +1,158 @@ +// 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 ( + "path/filepath" + "testing" + + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/guid" + "github.com/gogf/gf/v2/util/gutil" + + "github.com/gogf/gf/cmd/gf/v2/internal/cmd/genenums" +) + +// https://github.com/gogf/gf/issues/4387 +// Test that the output path is relative to the original working directory, +// not the source directory after Chdir. +func Test_Gen_Enums_Issue4387_RelativePath(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + // Create temp directory to simulate user's project + tempPath = gfile.Temp(guid.S()) + // Copy testdata to temp directory + srcTestData = gtest.DataPath("issue", "4387") + ) + + // Setup: create temp project structure + err := gfile.CopyDir(srcTestData, tempPath) + t.AssertNil(err) + defer gfile.Remove(tempPath) + + // Save original working directory + originalWd := gfile.Pwd() + + // Change to temp directory (simulate user being in project root) + err = gfile.Chdir(tempPath) + t.AssertNil(err) + defer gfile.Chdir(originalWd) // Restore original working directory + + // Run gen enums with relative paths + var ( + srcFolder = "api" + outputPath = filepath.FromSlash("internal/packed/packed_enums.go") + in = genenums.CGenEnumsInput{ + Src: srcFolder, + Path: outputPath, + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + _, err = genenums.CGenEnums{}.Enums(ctx, in) + t.AssertNil(err) + + // Expected: file should be created at tempPath/internal/packed/packed_enums.go + expectedPath := filepath.Join(tempPath, "internal", "packed", "packed_enums.go") + // Bug: file is created at tempPath/api/internal/packed/packed_enums.go + wrongPath := filepath.Join(tempPath, "api", "internal", "packed", "packed_enums.go") + + // Assert the file is at the expected location + t.Assert(gfile.Exists(expectedPath), true) + // Assert the file is NOT at the wrong location + t.Assert(gfile.Exists(wrongPath), false) + }) +} + +// Test gen enums with absolute output path (should work correctly) +func Test_Gen_Enums_AbsolutePath(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + tempPath = gfile.Temp(guid.S()) + srcTestData = gtest.DataPath("issue", "4387") + ) + + err := gfile.CopyDir(srcTestData, tempPath) + t.AssertNil(err) + defer gfile.Remove(tempPath) + + originalWd := gfile.Pwd() + err = gfile.Chdir(tempPath) + t.AssertNil(err) + defer gfile.Chdir(originalWd) + + // Use absolute path for output + var ( + srcFolder = "api" + outputPath = filepath.Join(tempPath, "internal", "packed", "packed_enums.go") + in = genenums.CGenEnumsInput{ + Src: srcFolder, + Path: outputPath, + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + _, err = genenums.CGenEnums{}.Enums(ctx, in) + t.AssertNil(err) + + // Assert the file exists at absolute path + t.Assert(gfile.Exists(outputPath), true) + }) +} + +// Test gen enums in monorepo mode (cd app/xxx/ then run command) +func Test_Gen_Enums_Issue4387_Monorepo(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + // Simulate monorepo structure + tempPath = gfile.Temp(guid.S()) + srcTestData = gtest.DataPath("issue", "4387") + // app/myapp is the subdirectory in monorepo + appPath = filepath.Join(tempPath, "app", "myapp") + ) + + // Create monorepo structure: tempPath/app/myapp/api/... + err := gfile.Mkdir(appPath) + t.AssertNil(err) + // Copy testdata into app/myapp + err = gfile.CopyDir(srcTestData, appPath) + t.AssertNil(err) + defer gfile.Remove(tempPath) + + originalWd := gfile.Pwd() + + // cd app/myapp (simulate user in monorepo subdirectory) + err = gfile.Chdir(appPath) + t.AssertNil(err) + defer gfile.Chdir(originalWd) + + var ( + srcFolder = "api" + outputPath = filepath.FromSlash("internal/packed/packed_enums.go") + in = genenums.CGenEnumsInput{ + Src: srcFolder, + Path: outputPath, + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + _, err = genenums.CGenEnums{}.Enums(ctx, in) + t.AssertNil(err) + + // Expected: file at app/myapp/internal/packed/packed_enums.go + expectedPath := filepath.Join(appPath, "internal", "packed", "packed_enums.go") + // Bug: file at app/myapp/api/internal/packed/packed_enums.go + wrongPath := filepath.Join(appPath, "api", "internal", "packed", "packed_enums.go") + + t.Assert(gfile.Exists(expectedPath), true) + t.Assert(gfile.Exists(wrongPath), false) + }) +} diff --git a/cmd/gf/internal/cmd/genenums/genenums.go b/cmd/gf/internal/cmd/genenums/genenums.go index a214072c2..27ff362e8 100644 --- a/cmd/gf/internal/cmd/genenums/genenums.go +++ b/cmd/gf/internal/cmd/genenums/genenums.go @@ -55,6 +55,13 @@ func (c CGenEnums) Enums(ctx context.Context, in CGenEnumsInput) (out *CGenEnums if realPath == "" { mlog.Fatalf(`source folder path "%s" does not exist`, in.Src) } + // Convert output path to absolute before Chdir, so it remains correct after directory change. + // See: https://github.com/gogf/gf/issues/4387 + outputPath := gfile.Abs(in.Path) + + originPwd := gfile.Pwd() + defer gfile.Chdir(originPwd) + err = gfile.Chdir(realPath) if err != nil { mlog.Fatal(err) @@ -72,14 +79,14 @@ func (c CGenEnums) Enums(ctx context.Context, in CGenEnumsInput) (out *CGenEnums p := NewEnumsParser(in.Prefixes) p.ParsePackages(pkgs) var enumsContent = gstr.ReplaceByMap(consts.TemplateGenEnums, g.MapStrStr{ - "{PackageName}": gfile.Basename(gfile.Dir(in.Path)), + "{PackageName}": gfile.Basename(gfile.Dir(outputPath)), "{EnumsJson}": "`" + p.Export() + "`", }) enumsContent = gstr.Trim(enumsContent) - if err = gfile.PutContents(in.Path, enumsContent); err != nil { + if err = gfile.PutContents(outputPath, enumsContent); err != nil { return } - mlog.Printf(`generated enums go file: %s`, in.Path) + mlog.Printf(`generated enums go file: %s`, outputPath) mlog.Print("done!") return } diff --git a/cmd/gf/internal/cmd/testdata/issue/4387/api/types.go b/cmd/gf/internal/cmd/testdata/issue/4387/api/types.go new file mode 100644 index 000000000..9af24e033 --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/issue/4387/api/types.go @@ -0,0 +1,16 @@ +// 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 api + +// Status is a sample enum type for testing. +type Status int + +const ( + StatusPending Status = iota + StatusActive + StatusDone +) diff --git a/cmd/gf/internal/cmd/testdata/issue/4387/go.mod b/cmd/gf/internal/cmd/testdata/issue/4387/go.mod new file mode 100644 index 000000000..f2ecd4c8d --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/issue/4387/go.mod @@ -0,0 +1,3 @@ +module github.com/gogf/gf/cmd/gf/v2/internal/cmd/testdata/issue/4387 + +go 1.20 From 095c69c42420e047c9d88847a39f6005861083e4 Mon Sep 17 00:00:00 2001 From: Jack Ling <34231795+lingcoder@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:15:57 +0800 Subject: [PATCH 10/11] fix(cmd/gf): fix gf env and gf build --dumpEnv command error (#4635) ## Summary - Fix `gf env` and `gf build --dumpEnv` command failing when `go env` outputs warning messages - When `go env` outputs warnings (e.g., invalid characters in environment variables), it returns non-zero exit code but still provides valid output - The original code would fail in this case ## Changes - Only fail when `go env` returns empty output, allow non-zero exit code with valid output - Skip lines that don't match `key=value` format instead of failing with Fatal error - Add debug log for skipped lines to help troubleshooting - Add unit tests for env command ## Related Issue Fixes #4469 ## Test plan - [x] `gf env` command works correctly even when `go env` outputs warnings - [x] `gf build --dumpEnv` works correctly - [x] Added unit tests pass --- .gitignore | 3 +- cmd/gf/internal/cmd/cmd_env.go | 14 ++-- cmd/gf/internal/cmd/cmd_z_unit_env_test.go | 84 ++++++++++++++++++++++ 3 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 cmd/gf/internal/cmd/cmd_z_unit_env_test.go diff --git a/.gitignore b/.gitignore index 964d5fdd0..be381afce 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ node_modules .docusaurus output .example/ -.golangci.bck.yml \ No newline at end of file +.golangci.bck.yml +*.exe \ No newline at end of file diff --git a/cmd/gf/internal/cmd/cmd_env.go b/cmd/gf/internal/cmd/cmd_env.go index 8c48be62e..52153bade 100644 --- a/cmd/gf/internal/cmd/cmd_env.go +++ b/cmd/gf/internal/cmd/cmd_env.go @@ -37,11 +37,13 @@ type cEnvInput struct { type cEnvOutput struct{} func (c cEnv) Index(ctx context.Context, in cEnvInput) (out *cEnvOutput, err error) { - result, err := gproc.ShellExec(ctx, "go env") - if err != nil { - mlog.Fatal(err) - } + result, execErr := gproc.ShellExec(ctx, "go env") + // Note: go env may return non-zero exit code when there are warnings (e.g., invalid characters in env vars), + // but it still outputs valid environment variables. So we only fail if result is empty. if result == "" { + if execErr != nil { + mlog.Fatal(execErr) + } mlog.Fatal(`retrieving Golang environment variables failed, did you install Golang?`) } var ( @@ -59,7 +61,9 @@ func (c cEnv) Index(ctx context.Context, in cEnvInput) (out *cEnvOutput, err err } match, _ := gregex.MatchString(`(.+?)=(.*)`, line) if len(match) < 3 { - mlog.Fatalf(`invalid Golang environment variable: "%s"`, line) + // Skip lines that don't match key=value format (e.g., warning messages from go env) + mlog.Debugf(`invalid Golang environment variable: "%s"`, line) + continue } array = append(array, []string{gstr.Trim(match[1]), gstr.Trim(match[2])}) } diff --git a/cmd/gf/internal/cmd/cmd_z_unit_env_test.go b/cmd/gf/internal/cmd/cmd_z_unit_env_test.go new file mode 100644 index 000000000..cecb67edc --- /dev/null +++ b/cmd/gf/internal/cmd/cmd_z_unit_env_test.go @@ -0,0 +1,84 @@ +// 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 ( + "testing" + + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" +) + +func Test_Env_Index(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test that env command runs without error + _, err := Env.Index(ctx, cEnvInput{}) + t.AssertNil(err) + }) +} + +func Test_Env_ParseGoEnvOutput(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test parsing normal go env output + lines := []string{ + "set GOPATH=C:\\Users\\test\\go", + "set GOROOT=C:\\Go", + "set GOOS=windows", + "GOARCH=amd64", // Unix format without "set " prefix + "CGO_ENABLED=0", + } + + for _, line := range lines { + line = gstr.Trim(line) + if gstr.Pos(line, "set ") == 0 { + line = line[4:] + } + match, _ := gregex.MatchString(`(.+?)=(.*)`, line) + t.Assert(len(match) >= 3, true) + } + }) +} + +func Test_Env_ParseGoEnvOutput_WithWarnings(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // Test parsing go env output that contains warning messages + // These lines should be skipped without causing errors + lines := []string{ + "go: stripping unprintable or unescapable characters from %\"GOPROXY\"%", + "go: warning: some warning message", + "# this is a comment", + "", + "set GOPATH=C:\\Users\\test\\go", + "set GOOS=windows", + } + + array := make([][]string, 0) + for _, line := range lines { + line = gstr.Trim(line) + if line == "" { + continue + } + if gstr.Pos(line, "set ") == 0 { + line = line[4:] + } + match, _ := gregex.MatchString(`(.+?)=(.*)`, line) + if len(match) < 3 { + // Skip lines that don't match key=value format (e.g., warning messages) + continue + } + array = append(array, []string{gstr.Trim(match[1]), gstr.Trim(match[2])}) + } + + // Should have parsed 2 valid environment variables + t.Assert(len(array), 2) + t.Assert(array[0][0], "GOPATH") + t.Assert(array[0][1], "C:\\Users\\test\\go") + t.Assert(array[1][0], "GOOS") + t.Assert(array[1][1], "windows") + }) +} From 110e3fbf16bfe3475dc711b2592a21e2617d548c Mon Sep 17 00:00:00 2001 From: Jack Ling <34231795+lingcoder@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:16:12 +0800 Subject: [PATCH 11/11] feat(cmd/gendao): add wildcard pattern support for tables configuration (#4632) ## Summary - Add wildcard pattern support (`*` and `?`) for `tables` configuration - Fix `tablesEx` wildcard to use exact match (`^$`) for consistency - Add warning when exact table name does not exist - Add unit tests and integration tests for MySQL and PostgreSQL ## Changes | Configuration | Before | After | |---------------|--------|-------| | `tables: "user_*"` | Not supported | Matches tables starting with "user_" | | `tables: "*"` | Not supported | Matches all tables | | `tablesEx: "user_*"` | Partial match | Exact match (consistent with tables) | ## Features - `*` matches any characters (e.g., `user_*` matches `user_info`, `user_log`) - `?` matches single character (e.g., `user_???` matches `user_log` but not `user_info`) - Mixed patterns and exact names supported (e.g., `tables: "user_*,config"`) - Non-existent exact table names are skipped with warning message ## Test plan - [x] Unit tests for `containsWildcard`, `patternToRegex`, `filterTablesByPatterns` (11 cases) - [x] Integration tests for MySQL (5 cases) - [x] Integration tests for PostgreSQL (1 case with tables + tablesEx) - [x] Standard SQL syntax for cross-database compatibility Closes #4629 --------- Co-authored-by: github-actions[bot] --- cmd/gf/internal/cmd/cmd_z_init_test.go | 20 +- .../cmd/cmd_z_unit_gen_dao_issue_test.go | 395 ++++++++++++++++++ cmd/gf/internal/cmd/gendao/gendao.go | 99 ++++- cmd/gf/internal/cmd/gendao/gendao_test.go | 182 ++++++++ .../cmd/testdata/gendao/tables_pattern.sql | 30 ++ 5 files changed, 707 insertions(+), 19 deletions(-) create mode 100644 cmd/gf/internal/cmd/gendao/gendao_test.go create mode 100644 cmd/gf/internal/cmd/testdata/gendao/tables_pattern.sql diff --git a/cmd/gf/internal/cmd/cmd_z_init_test.go b/cmd/gf/internal/cmd/cmd_z_init_test.go index 1e71cbd8b..d066a2599 100644 --- a/cmd/gf/internal/cmd/cmd_z_init_test.go +++ b/cmd/gf/internal/cmd/cmd_z_init_test.go @@ -15,9 +15,11 @@ import ( ) var ( - ctx = context.Background() - testDB gdb.DB - link = "mysql:root:12345678@tcp(127.0.0.1:3306)/test?loc=Local&parseTime=true" + ctx = context.Background() + testDB gdb.DB + testPgDB gdb.DB + link = "mysql:root:12345678@tcp(127.0.0.1:3306)/test?loc=Local&parseTime=true" + linkPg = "pgsql:postgres:12345678@tcp(127.0.0.1:5432)/test" ) func init() { @@ -28,6 +30,10 @@ func init() { if err != nil { panic(err) } + // PostgreSQL connection (optional, may not be available in all environments) + testPgDB, _ = gdb.New(gdb.ConfigNode{ + Link: linkPg, + }) } func dropTableWithDb(db gdb.DB, table string) { @@ -36,3 +42,11 @@ func dropTableWithDb(db gdb.DB, table string) { gtest.Error(err) } } + +// dropTableStd uses standard SQL syntax compatible with MySQL and PostgreSQL. +func dropTableStd(db gdb.DB, table string) { + dropTableStmt := fmt.Sprintf("DROP TABLE IF EXISTS %s", table) + if _, err := db.Exec(ctx, dropTableStmt); err != nil { + gtest.Error(err) + } +} diff --git a/cmd/gf/internal/cmd/cmd_z_unit_gen_dao_issue_test.go b/cmd/gf/internal/cmd/cmd_z_unit_gen_dao_issue_test.go index 02d5f5936..3a8421fda 100644 --- a/cmd/gf/internal/cmd/cmd_z_unit_gen_dao_issue_test.go +++ b/cmd/gf/internal/cmd/cmd_z_unit_gen_dao_issue_test.go @@ -460,3 +460,398 @@ func Test_Gen_Dao_Issue3749(t *testing.T) { } }) } + +// https://github.com/gogf/gf/issues/4629 +// Test tables pattern matching with * wildcard. +func Test_Gen_Dao_Issue4629_TablesPattern_Star(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + err error + db = testDB + table1 = "trade_order" + table2 = "trade_item" + table3 = "user_info" + table4 = "user_log" + table5 = "config" + sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`) + ) + dropTableStd(db, table1) + dropTableStd(db, table2) + dropTableStd(db, table3) + dropTableStd(db, table4) + dropTableStd(db, table5) + t.AssertNil(execSqlFile(db, sqlFilePath)) + defer dropTableStd(db, table1) + defer dropTableStd(db, table2) + defer dropTableStd(db, table3) + defer dropTableStd(db, table4) + defer dropTableStd(db, table5) + + var ( + path = gfile.Temp(guid.S()) + group = "test" + in = gendao.CGenDaoInput{ + Path: path, + Link: link, + Group: group, + Tables: "trade_*", // Should match trade_order, trade_item + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + + pwd := gfile.Pwd() + err = gfile.Chdir(path) + t.AssertNil(err) + defer gfile.Chdir(pwd) + defer gfile.RemoveAll(path) + + _, err = gendao.CGenDao{}.Dao(ctx, in) + t.AssertNil(err) + + // Should generate 2 dao files: trade_order.go, trade_item.go + generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false) + t.AssertNil(err) + t.Assert(len(generatedFiles), 2) + + // Verify the correct files are generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_order.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_item.go")), true) + // user_* and config should NOT be generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_info.go")), false) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_log.go")), false) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "config.go")), false) + }) +} + +// https://github.com/gogf/gf/issues/4629 +// Test tables pattern matching with multiple patterns. +func Test_Gen_Dao_Issue4629_TablesPattern_Multiple(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + err error + db = testDB + table1 = "trade_order" + table2 = "trade_item" + table3 = "user_info" + table4 = "user_log" + table5 = "config" + sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`) + ) + dropTableStd(db, table1) + dropTableStd(db, table2) + dropTableStd(db, table3) + dropTableStd(db, table4) + dropTableStd(db, table5) + t.AssertNil(execSqlFile(db, sqlFilePath)) + defer dropTableStd(db, table1) + defer dropTableStd(db, table2) + defer dropTableStd(db, table3) + defer dropTableStd(db, table4) + defer dropTableStd(db, table5) + + var ( + path = gfile.Temp(guid.S()) + group = "test" + in = gendao.CGenDaoInput{ + Path: path, + Link: link, + Group: group, + Tables: "trade_*,user_*", // Should match trade_order, trade_item, user_info, user_log + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + + pwd := gfile.Pwd() + err = gfile.Chdir(path) + t.AssertNil(err) + defer gfile.Chdir(pwd) + defer gfile.RemoveAll(path) + + _, err = gendao.CGenDao{}.Dao(ctx, in) + t.AssertNil(err) + + // Should generate 4 dao files + generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false) + t.AssertNil(err) + t.Assert(len(generatedFiles), 4) + + // Verify the correct files are generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_order.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_item.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_info.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_log.go")), true) + // config should NOT be generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "config.go")), false) + }) +} + +// https://github.com/gogf/gf/issues/4629 +// Test tables pattern mixed with exact table name. +func Test_Gen_Dao_Issue4629_TablesPattern_Mixed(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + err error + db = testDB + table1 = "trade_order" + table2 = "trade_item" + table3 = "user_info" + table4 = "user_log" + table5 = "config" + sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`) + ) + dropTableStd(db, table1) + dropTableStd(db, table2) + dropTableStd(db, table3) + dropTableStd(db, table4) + dropTableStd(db, table5) + t.AssertNil(execSqlFile(db, sqlFilePath)) + defer dropTableStd(db, table1) + defer dropTableStd(db, table2) + defer dropTableStd(db, table3) + defer dropTableStd(db, table4) + defer dropTableStd(db, table5) + + var ( + path = gfile.Temp(guid.S()) + group = "test" + in = gendao.CGenDaoInput{ + Path: path, + Link: link, + Group: group, + Tables: "trade_*,config", // Pattern + exact name + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + + pwd := gfile.Pwd() + err = gfile.Chdir(path) + t.AssertNil(err) + defer gfile.Chdir(pwd) + defer gfile.RemoveAll(path) + + _, err = gendao.CGenDao{}.Dao(ctx, in) + t.AssertNil(err) + + // Should generate 3 dao files: trade_order.go, trade_item.go, config.go + generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false) + t.AssertNil(err) + t.Assert(len(generatedFiles), 3) + + // Verify the correct files are generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_order.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_item.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "config.go")), true) + // user_* should NOT be generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_info.go")), false) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_log.go")), false) + }) +} + +// https://github.com/gogf/gf/issues/4629 +// Test tables pattern with ? wildcard (single character match). +func Test_Gen_Dao_Issue4629_TablesPattern_Question(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + err error + db = testDB + table1 = "trade_order" + table2 = "trade_item" + table3 = "user_info" + table4 = "user_log" + table5 = "config" + sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`) + ) + dropTableStd(db, table1) + dropTableStd(db, table2) + dropTableStd(db, table3) + dropTableStd(db, table4) + dropTableStd(db, table5) + t.AssertNil(execSqlFile(db, sqlFilePath)) + defer dropTableStd(db, table1) + defer dropTableStd(db, table2) + defer dropTableStd(db, table3) + defer dropTableStd(db, table4) + defer dropTableStd(db, table5) + + var ( + path = gfile.Temp(guid.S()) + group = "test" + in = gendao.CGenDaoInput{ + Path: path, + Link: link, + Group: group, + Tables: "user_???", // ? matches single char: user_log (3 chars) but not user_info (4 chars) + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + + pwd := gfile.Pwd() + err = gfile.Chdir(path) + t.AssertNil(err) + defer gfile.Chdir(pwd) + defer gfile.RemoveAll(path) + + _, err = gendao.CGenDao{}.Dao(ctx, in) + t.AssertNil(err) + + // Should generate 1 dao file: user_log.go (3 chars after user_) + generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false) + t.AssertNil(err) + t.Assert(len(generatedFiles), 1) + + // Verify only user_log is generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_log.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_info.go")), false) // 4 chars, doesn't match + }) +} + +// https://github.com/gogf/gf/issues/4629 +// Test that exact table names still work (backward compatibility). +func Test_Gen_Dao_Issue4629_TablesPattern_ExactNames(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + err error + db = testDB + table1 = "trade_order" + table2 = "trade_item" + table3 = "user_info" + table4 = "user_log" + table5 = "config" + sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`) + ) + dropTableStd(db, table1) + dropTableStd(db, table2) + dropTableStd(db, table3) + dropTableStd(db, table4) + dropTableStd(db, table5) + t.AssertNil(execSqlFile(db, sqlFilePath)) + defer dropTableStd(db, table1) + defer dropTableStd(db, table2) + defer dropTableStd(db, table3) + defer dropTableStd(db, table4) + defer dropTableStd(db, table5) + + var ( + path = gfile.Temp(guid.S()) + group = "test" + in = gendao.CGenDaoInput{ + Path: path, + Link: link, + Group: group, + Tables: "trade_order,config", // Exact names, no patterns + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + + pwd := gfile.Pwd() + err = gfile.Chdir(path) + t.AssertNil(err) + defer gfile.Chdir(pwd) + defer gfile.RemoveAll(path) + + _, err = gendao.CGenDao{}.Dao(ctx, in) + t.AssertNil(err) + + // Should generate 2 dao files + generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false) + t.AssertNil(err) + t.Assert(len(generatedFiles), 2) + + // Verify exactly the specified tables are generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_order.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "config.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_item.go")), false) + }) +} + +// https://github.com/gogf/gf/issues/4629 +// Test tables pattern matching with PostgreSQL. +func Test_Gen_Dao_Issue4629_TablesPattern_PgSql(t *testing.T) { + if testPgDB == nil { + t.Skip("PostgreSQL database not available, skipping test") + return + } + gtest.C(t, func(t *gtest.T) { + var ( + err error + db = testPgDB + table1 = "trade_order" + table2 = "trade_item" + table3 = "user_info" + table4 = "user_log" + table5 = "config" + sqlFilePath = gtest.DataPath(`gendao`, `tables_pattern.sql`) + ) + dropTableStd(db, table1) + dropTableStd(db, table2) + dropTableStd(db, table3) + dropTableStd(db, table4) + dropTableStd(db, table5) + t.AssertNil(execSqlFile(db, sqlFilePath)) + defer dropTableStd(db, table1) + defer dropTableStd(db, table2) + defer dropTableStd(db, table3) + defer dropTableStd(db, table4) + defer dropTableStd(db, table5) + + // Test tables pattern with tablesEx pattern + var ( + path = gfile.Temp(guid.S()) + group = "test" + in = gendao.CGenDaoInput{ + Path: path, + Link: linkPg, + Group: group, + Tables: "trade_*,user_*,config", // Match only our test tables + TablesEx: "user_*", // Exclude user_* tables + } + ) + err = gutil.FillStructWithDefault(&in) + t.AssertNil(err) + + err = gfile.Mkdir(path) + t.AssertNil(err) + + pwd := gfile.Pwd() + err = gfile.Chdir(path) + t.AssertNil(err) + defer gfile.Chdir(pwd) + defer gfile.RemoveAll(path) + + _, err = gendao.CGenDao{}.Dao(ctx, in) + t.AssertNil(err) + + // Should generate 3 dao files: trade_order, trade_item, config (user_* excluded by tablesEx) + generatedFiles, err := gfile.ScanDir(gfile.Join(path, "dao"), "*.go", false) + t.AssertNil(err) + t.Assert(len(generatedFiles), 3) + + // Verify the correct files are generated + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_order.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "trade_item.go")), true) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "config.go")), true) + // user_* should NOT be generated (excluded by tablesEx) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_info.go")), false) + t.Assert(gfile.Exists(gfile.Join(path, "dao", "user_log.go")), false) + }) +} diff --git a/cmd/gf/internal/cmd/gendao/gendao.go b/cmd/gf/internal/cmd/gendao/gendao.go index 777b24ea1..056d91dcc 100644 --- a/cmd/gf/internal/cmd/gendao/gendao.go +++ b/cmd/gf/internal/cmd/gendao/gendao.go @@ -188,7 +188,27 @@ func doGenDaoForArray(ctx context.Context, index int, in CGenDaoInput) { var tableNames []string if in.Tables != "" { - tableNames = gstr.SplitAndTrim(in.Tables, ",") + inputTables := gstr.SplitAndTrim(in.Tables, ",") + // Check if any table pattern contains wildcard characters. + // https://github.com/gogf/gf/issues/4629 + var hasPattern bool + for _, t := range inputTables { + if containsWildcard(t) { + hasPattern = true + break + } + } + if hasPattern { + // Fetch all tables first, then filter by patterns. + allTables, err := db.Tables(context.TODO()) + if err != nil { + mlog.Fatalf("fetching tables failed: %+v", err) + } + tableNames = filterTablesByPatterns(allTables, inputTables) + } else { + // Use exact table names as before. + tableNames = inputTables + } } else { tableNames, err = db.Tables(context.TODO()) if err != nil { @@ -199,22 +219,11 @@ func doGenDaoForArray(ctx context.Context, index int, in CGenDaoInput) { if in.TablesEx != "" { array := garray.NewStrArrayFrom(tableNames) for _, p := range gstr.SplitAndTrim(in.TablesEx, ",") { - if gstr.Contains(p, "*") || gstr.Contains(p, "?") { - p = gstr.ReplaceByMap(p, map[string]string{ - "\r": "", - "\n": "", - }) - p = gstr.ReplaceByMap(p, map[string]string{ - "*": "\r", - "?": "\n", - }) - p = gregex.Quote(p) - p = gstr.ReplaceByMap(p, map[string]string{ - "\r": ".*", - "\n": ".", - }) + if containsWildcard(p) { + // Use exact match with ^ and $ anchors for consistency with tables pattern. + regPattern := "^" + patternToRegex(p) + "$" for _, v := range array.Clone().Slice() { - if gregex.IsMatchString(p, v) { + if gregex.IsMatchString(regPattern, v) { array.RemoveValue(v) } } @@ -422,3 +431,61 @@ func getTemplateFromPathOrDefault(filePath string, def string) string { } return def } + +// containsWildcard checks if the pattern contains wildcard characters (* or ?). +func containsWildcard(pattern string) bool { + return gstr.Contains(pattern, "*") || gstr.Contains(pattern, "?") +} + +// patternToRegex converts a wildcard pattern to a regex pattern. +// Wildcard characters: * matches any characters, ? matches single character. +func patternToRegex(pattern string) string { + pattern = gstr.ReplaceByMap(pattern, map[string]string{ + "\r": "", + "\n": "", + }) + pattern = gstr.ReplaceByMap(pattern, map[string]string{ + "*": "\r", + "?": "\n", + }) + pattern = gregex.Quote(pattern) + pattern = gstr.ReplaceByMap(pattern, map[string]string{ + "\r": ".*", + "\n": ".", + }) + return pattern +} + +// filterTablesByPatterns filters tables by given patterns. +// Patterns support wildcard characters: * matches any characters, ? matches single character. +// https://github.com/gogf/gf/issues/4629 +func filterTablesByPatterns(allTables []string, patterns []string) []string { + var result []string + matched := make(map[string]bool) + allTablesSet := make(map[string]bool) + for _, t := range allTables { + allTablesSet[t] = true + } + for _, p := range patterns { + if containsWildcard(p) { + regPattern := "^" + patternToRegex(p) + "$" + for _, table := range allTables { + if !matched[table] && gregex.IsMatchString(regPattern, table) { + result = append(result, table) + matched[table] = true + } + } + } else { + // Exact table name, use direct string comparison. + if !allTablesSet[p] { + mlog.Printf(`table "%s" does not exist, skipped`, p) + continue + } + if !matched[p] { + result = append(result, p) + matched[p] = true + } + } + } + return result +} diff --git a/cmd/gf/internal/cmd/gendao/gendao_test.go b/cmd/gf/internal/cmd/gendao/gendao_test.go new file mode 100644 index 000000000..80b67d907 --- /dev/null +++ b/cmd/gf/internal/cmd/gendao/gendao_test.go @@ -0,0 +1,182 @@ +// 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 gendao + +import ( + "testing" + + "github.com/gogf/gf/v2/test/gtest" +) + +// Test containsWildcard function. +func Test_containsWildcard(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + t.Assert(containsWildcard("trade_*"), true) + t.Assert(containsWildcard("user_?"), true) + t.Assert(containsWildcard("*"), true) + t.Assert(containsWildcard("?"), true) + t.Assert(containsWildcard("trade_order"), false) + t.Assert(containsWildcard(""), false) + }) +} + +// Test patternToRegex function. +func Test_patternToRegex(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + // * should become .* + t.Assert(patternToRegex("trade_*"), "trade_.*") + // ? should become . + t.Assert(patternToRegex("user_???"), "user_...") + // Mixed + t.Assert(patternToRegex("*_order_?"), ".*_order_.") + // No wildcards - should escape special regex chars + t.Assert(patternToRegex("trade_order"), "trade_order") + // Just * + t.Assert(patternToRegex("*"), ".*") + }) +} + +// Test filterTablesByPatterns with * wildcard. +func Test_filterTablesByPatterns_Star(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item", "user_info", "user_log", "config"} + + // Single pattern with * + result := filterTablesByPatterns(allTables, []string{"trade_*"}) + t.Assert(len(result), 2) + t.AssertIN("trade_order", result) + t.AssertIN("trade_item", result) + + // Multiple patterns with * + result = filterTablesByPatterns(allTables, []string{"trade_*", "user_*"}) + t.Assert(len(result), 4) + t.AssertIN("trade_order", result) + t.AssertIN("trade_item", result) + t.AssertIN("user_info", result) + t.AssertIN("user_log", result) + }) +} + +// Test filterTablesByPatterns with ? wildcard. +func Test_filterTablesByPatterns_Question(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item", "user_info", "user_log", "config"} + + // ? matches single character: user_log (3 chars) but not user_info (4 chars) + result := filterTablesByPatterns(allTables, []string{"user_???"}) + t.Assert(len(result), 1) + t.AssertIN("user_log", result) + t.AssertNI("user_info", result) + + // user_???? should match user_info (4 chars) + result = filterTablesByPatterns(allTables, []string{"user_????"}) + t.Assert(len(result), 1) + t.AssertIN("user_info", result) + t.AssertNI("user_log", result) + }) +} + +// Test filterTablesByPatterns with mixed patterns and exact names. +func Test_filterTablesByPatterns_Mixed(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item", "user_info", "user_log", "config"} + + // Pattern + exact name + result := filterTablesByPatterns(allTables, []string{"trade_*", "config"}) + t.Assert(len(result), 3) + t.AssertIN("trade_order", result) + t.AssertIN("trade_item", result) + t.AssertIN("config", result) + t.AssertNI("user_info", result) + t.AssertNI("user_log", result) + }) +} + +// Test filterTablesByPatterns with exact names only (backward compatibility). +func Test_filterTablesByPatterns_ExactNames(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item", "user_info", "user_log", "config"} + + // Exact names only + result := filterTablesByPatterns(allTables, []string{"trade_order", "config"}) + t.Assert(len(result), 2) + t.AssertIN("trade_order", result) + t.AssertIN("config", result) + t.AssertNI("trade_item", result) + }) +} + +// Test filterTablesByPatterns - no duplicates when table matches multiple patterns. +func Test_filterTablesByPatterns_NoDuplicates(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item", "user_info"} + + // trade_order matches both patterns, should only appear once + result := filterTablesByPatterns(allTables, []string{"trade_*", "trade_order"}) + t.Assert(len(result), 2) // trade_order, trade_item + + // Count occurrences of trade_order + count := 0 + for _, v := range result { + if v == "trade_order" { + count++ + } + } + t.Assert(count, 1) // No duplicates + }) +} + +// Test filterTablesByPatterns - pattern matches nothing. +func Test_filterTablesByPatterns_NoMatch(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item", "user_info"} + + // Pattern that matches nothing + result := filterTablesByPatterns(allTables, []string{"nonexistent_*"}) + t.Assert(len(result), 0) + }) +} + +// Test filterTablesByPatterns - empty input. +func Test_filterTablesByPatterns_Empty(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item"} + + // Empty patterns + result := filterTablesByPatterns(allTables, []string{}) + t.Assert(len(result), 0) + + // Empty tables + result = filterTablesByPatterns([]string{}, []string{"trade_*"}) + t.Assert(len(result), 0) + }) +} + +// Test filterTablesByPatterns - "*" matches all tables. +func Test_filterTablesByPatterns_MatchAll(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item", "user_info", "user_log", "config"} + + // "*" should match all tables + result := filterTablesByPatterns(allTables, []string{"*"}) + t.Assert(len(result), 5) + }) +} + +// Test filterTablesByPatterns - non-existent exact table name should be skipped. +func Test_filterTablesByPatterns_NonExistent(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + allTables := []string{"trade_order", "trade_item", "user_info"} + + // Mix of existing and non-existing tables + result := filterTablesByPatterns(allTables, []string{"trade_order", "nonexistent", "user_info"}) + t.Assert(len(result), 2) + t.AssertIN("trade_order", result) + t.AssertIN("user_info", result) + t.AssertNI("nonexistent", result) + }) +} diff --git a/cmd/gf/internal/cmd/testdata/gendao/tables_pattern.sql b/cmd/gf/internal/cmd/testdata/gendao/tables_pattern.sql new file mode 100644 index 000000000..0d122de3a --- /dev/null +++ b/cmd/gf/internal/cmd/testdata/gendao/tables_pattern.sql @@ -0,0 +1,30 @@ +-- Test case for issue #4629: tables pattern matching +-- https://github.com/gogf/gf/issues/4629 +-- Standard SQL syntax compatible with MySQL and PostgreSQL +-- +-- Tables: trade_order, trade_item, user_info, user_log, config + +CREATE TABLE trade_order ( + id INTEGER PRIMARY KEY, + name VARCHAR(45) NOT NULL +); + +CREATE TABLE trade_item ( + id INTEGER PRIMARY KEY, + name VARCHAR(45) NOT NULL +); + +CREATE TABLE user_info ( + id INTEGER PRIMARY KEY, + name VARCHAR(45) NOT NULL +); + +CREATE TABLE user_log ( + id INTEGER PRIMARY KEY, + name VARCHAR(45) NOT NULL +); + +CREATE TABLE config ( + id INTEGER PRIMARY KEY, + name VARCHAR(45) NOT NULL +);