diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml new file mode 100644 index 00000000..8d495ba6 --- /dev/null +++ b/.github/workflows/go-test.yml @@ -0,0 +1,46 @@ +name: Go CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + name: Run go test + runs-on: ubuntu-latest + env: + # TestPatcher: + # absolute path in localsession.json + # TestCases: + # hardcoded obselete json + # Test_goParser_ParseNode: + # test tries to retrieve non-existent node + # TestParser_NodeFieldsConsistency: + # Vars.Content only includes name, not whole body + SKIPPED_TESTS: >- + TestPatcher|TestCases|Test_goParser_ParseNode|TestParser_NodeFieldsConsistency + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + components: rust-analyzer + + - name: Install gopls + run: go install golang.org/x/tools/gopls@latest + + - name: Run all tests + run: go test ./lang/... -skip '${{ env.SKIPPED_TESTS }}' diff --git a/.gitignore b/.gitignore index 29c1e7cb..2ea9b811 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,7 @@ __pycache__ rust-analyzer-x86_64-unknown-linux-gnu testdata/test +testdata/repos testdata/jsons src/lang/testdata @@ -77,4 +78,4 @@ src/lang/testdata tools abcoder -!testdata/asts/*.json \ No newline at end of file +!testdata/asts/*.json diff --git a/README.md b/README.md index a1859afa..d8390d30 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ see [UniAST Specification](docs/uniast-zh.md) abcoder parse go localsession -o /abcoder-asts/localsession.json ``` - To parse repositories in other languages, [install the corresponding langauge server first](./docs/lsp-installation-en.md). + To parse repositories in other languages, [install the corresponding language server first](./docs/lsp-installation-en.md). 3. Integrate ABCoder's MCP tools into your AI agent. @@ -80,7 +80,7 @@ see [UniAST Specification](docs/uniast-zh.md) - You can add more repo ASTs into the AST directory without restarting abcoder MCP server. -- Try to use [the recommaned prompt](llm/prompt/analyzer.md) and combine planning/memory tools like [sequential-thinking](https://github.com/modelcontextprotocol/servers/tree/main/src/sequentialthinking) in your AI agent. +- Try to use [the recommened prompt](llm/prompt/analyzer.md) and combine planning/memory tools like [sequential-thinking](https://github.com/modelcontextprotocol/servers/tree/main/src/sequentialthinking) in your AI agent. ## Use ABCoder as an Agent (WIP) diff --git a/docs/lsp-installation-cn.md b/docs/lsp-installation-zh.md similarity index 100% rename from docs/lsp-installation-cn.md rename to docs/lsp-installation-zh.md diff --git a/lang/collect/collect_test.go b/lang/collect/collect_test.go index d2662fb5..0f3ddeea 100644 --- a/lang/collect/collect_test.go +++ b/lang/collect/collect_test.go @@ -17,103 +17,50 @@ package collect import ( "context" "encoding/json" - "fmt" "os" - "path/filepath" "testing" - "time" "github.com/cloudwego/abcoder/lang/log" "github.com/cloudwego/abcoder/lang/lsp" + "github.com/cloudwego/abcoder/lang/testutils" "github.com/cloudwego/abcoder/lang/uniast" ) -var testroot = "../../../testdata" - func TestCollector_Collect(t *testing.T) { - root := testroot + "/rust2" - - root, _ = filepath.Abs(root) log.SetLogLevel(log.DebugLevel) - rustLSP, err := lsp.NewLSPClient(root, root+"/src/main.rs", time.Second*5, lsp.ClientOptions{ - Server: "rust-analyzer", - Language: "rust", - Verbose: true, - }) + rustLSP, rustTestCase, err := lsp.InitLSPForFirstTest(uniast.Rust, "rust-analyzer") if err != nil { - fmt.Printf("Failed to initialize rust LSP client: %v", err) + t.Fatalf("Failed to initialize rust LSP client: %v", err) } defer rustLSP.Close() - tests := []struct { - name string - want *uniast.Repository - wantErr bool - }{ - { - name: "rust", - want: &uniast.Repository{}, - wantErr: false, - }, - } - dir := testroot - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := NewCollector(root, rustLSP) - c.LoadExternalSymbol = true - err := c.Collect(context.Background()) - if (err != nil) != tt.wantErr { - t.Errorf("Collector.Collect() error = %v, wantErr %v", err, tt.wantErr) - return - } - js1, err := json.Marshal(c.syms) - if err != nil { - t.Fatalf("Marshal symbols failed: %v", err) - } - if err := os.WriteFile(dir+"/symbols.json", js1, 0644); err != nil { - t.Fatalf("Write json failed: %v", err) - } - // if !reflect.DeepEqual(got, tt.want) { - // t.Errorf("Collector.Collect() = %#v, want %#v", got, tt.want) - // } - // for sym, content := range c.symbols { - // if sym.Name == "add" { - // t.Logf("symbol: %#v, content:%s", sym, content) - // } - // } - js3, err := json.Marshal(c.deps) - if err != nil { - t.Fatalf("Marshal deps failed: %v", err) - } - if err := os.WriteFile(dir+"/deps.json", js3, 0644); err != nil { - t.Fatalf("Write json failed: %v", err) - } - js4, err := json.Marshal(c.funcs) - if err != nil { - t.Fatalf("Marshal methods failed: %v", err) - } - if err := os.WriteFile(dir+"/funcs.json", js4, 0644); err != nil { - t.Fatalf("Write json failed: %v", err) - } - js5, err := json.Marshal(c.vars) - if err != nil { - t.Fatalf("Marshal methods failed: %v", err) - } - if err := os.WriteFile(dir+"/vars.json", js5, 0644); err != nil { - t.Fatalf("Write json failed: %v", err) - } + t.Run("rustCollect", func(t *testing.T) { + c := NewCollector(rustTestCase, rustLSP) + c.LoadExternalSymbol = true + err := c.Collect(context.Background()) + if err != nil { + t.Fatalf("Collector.Collect() failed = %v\n", err) + } - repo, err := c.Export(context.Background()) - if err != nil { - t.Fatalf("export repo failed: %v", err) - } - js6, err := json.Marshal(repo) + outdir := testutils.MakeTmpTestdir(true) + marshals := []struct { + val any + name string + }{ + {&c.syms, "symbols"}, + {&c.deps, "deps"}, + {&c.funcs, "funcs"}, + {&c.vars, "vars"}, + {&c.repo, "repo"}, + } + for _, m := range marshals { + js, err := json.Marshal(m.val) if err != nil { - t.Fatalf("Marshal methods failed: %v", err) + t.Fatalf("Marshal %s failed: %v", m.name, err) } - if err := os.WriteFile(dir+"/repo.json", js6, 0644); err != nil { - t.Fatalf("Write json failed: %v", err) + if err := os.WriteFile(outdir+"/"+m.name+".json", js, 0644); err != nil { + t.Fatalf("Write json %s failed: %v", m.name, err) } - }) - } + } + }) } diff --git a/lang/golang/parser/pkg_test.go b/lang/golang/parser/pkg_test.go index 763b5421..81807dc3 100644 --- a/lang/golang/parser/pkg_test.go +++ b/lang/golang/parser/pkg_test.go @@ -16,21 +16,23 @@ package parser import ( "encoding/json" - "fmt" "go/ast" "go/parser" "go/token" "os" - "path/filepath" "testing" + "github.com/cloudwego/abcoder/lang/testutils" . "github.com/cloudwego/abcoder/lang/uniast" ) +const localSessURL = "github.com/cloudwego/localsession" + func Test_goParser_ParseRepo(t *testing.T) { type fields struct { modName string homePageDir string + id Identity } tests := []struct { name string @@ -39,40 +41,31 @@ func Test_goParser_ParseRepo(t *testing.T) { { name: "test", fields: fields{ - modName: "github.com/cloudwego/localsession", - homePageDir: "../../../tmp/localsession", + modName: localSessURL, + homePageDir: "localsession", + id: NewIdentity(localSessURL, localSessURL+"/backup", "RecoverCtxOnDemands"), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - abs, _ := filepath.Abs(tt.fields.homePageDir) - println(abs) - p := newGoParser(tt.fields.modName, tt.fields.homePageDir, Options{ + repoDir, err := testutils.GitCloneFast(tt.fields.modName, tt.fields.homePageDir, "main") + if err != nil { + t.Fatalf("failed to clone repo %s", err) + } + p := newGoParser(tt.fields.modName, repoDir, Options{ ReferCodeDepth: 1, NeedTest: true, }) r, err := p.ParseRepo() if err != nil { - t.Fatal(err) + t.Fatalf("failed to parse repo %s", err) } r.BuildGraph() - // spew.Dump(p) - pj, err := json.MarshalIndent(r, "", " ") + _, err = p.getNode(tt.fields.id) if err != nil { - t.Fatal(err) + t.Fatalf("failed to get node %s", err) } - _ = pj - _ = os.WriteFile("ast.json", pj, 0644) - n, err := p.getNode(NewIdentity("github.com/cloudwego/localsession", "github.com/cloudwego/localsession/backup", "RecoverCtxOnDemands")) - if err != nil { - t.Fatal(err) - } - jf, err := json.MarshalIndent(n, "", " ") - if err != nil { - t.Fatalf("json.Marshal() error = %v", err) - } - os.WriteFile("node.json", jf, 0644) }) } } @@ -93,7 +86,7 @@ func Test_goParser_ParseDirs(t *testing.T) { { name: "test", args: args{ - homePageDir: "../../../testdata/golang", + homePageDir: testutils.FirstTest("go"), modName: "a.b/c", pkg: "a.b/c/cmd", opts: Options{ @@ -137,7 +130,6 @@ type Struct struct { t.Fatal(err) } ast.Inspect(node, func(n ast.Node) bool { - fmt.Printf("%#v\n", n) if sel, ok := n.(*ast.SelectorExpr); ok { println("selector:", string(GetRawContent(fset, []byte(src), sel, false))) } @@ -157,6 +149,10 @@ func Test_goParser_ParseNode(t *testing.T) { pkgPath string name string } + localSessionDir, err := testutils.GitCloneFast(localSessURL, "localsession", "main") + if err != nil { + t.Fatalf("failed to clone repo %s", err) + } tests := []struct { name string fields fields @@ -167,7 +163,7 @@ func Test_goParser_ParseNode(t *testing.T) { name: "test", fields: fields{ modName: "github.com/cloudwego/localsession", - homePageDir: "../../../tmp/localsession", + homePageDir: localSessionDir, }, args: args{ pkgPath: "github.com/modern-go/gls", diff --git a/lang/golang/writer/write_test.go b/lang/golang/writer/write_test.go index 860d5934..fcca496b 100644 --- a/lang/golang/writer/write_test.go +++ b/lang/golang/writer/write_test.go @@ -22,11 +22,13 @@ import ( "reflect" "testing" + "github.com/cloudwego/abcoder/lang/testutils" "github.com/cloudwego/abcoder/lang/uniast" ) func TestWriter_WriteRepo(t *testing.T) { - repo, err := uniast.LoadRepo("../../../../tmp_compress/localsession.json") + astFile := testutils.GetTestAstFile("localsession") + repo, err := uniast.LoadRepo(astFile) if err != nil { t.Fatal(err) } @@ -46,17 +48,18 @@ func TestWriter_WriteRepo(t *testing.T) { name: "test", fields: fields{ Options: Options{ - CompilerPath: "1.18", + CompilerPath: "true", // DO NOT RUN go mod tidy }, }, args: args{repo: repo}, wantErr: false, }, } + tmproot := testutils.MakeTmpTestdir(true) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { w := NewWriter(tt.fields.Options) - if err := w.WriteRepo(tt.args.repo, "../../../../tmp/localsession2"); (err != nil) != tt.wantErr { + if err := w.WriteRepo(tt.args.repo, tmproot); (err != nil) != tt.wantErr { t.Errorf("Writer.WriteRepo() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -64,9 +67,14 @@ func TestWriter_WriteRepo(t *testing.T) { } func TestPatcher_PatchImports(t *testing.T) { - data, err := os.ReadFile("../../../../tmp/localsession/gls.go") + repoDir, err := testutils.GitCloneFast("github.com/cloudwego/localsession", "localsession", "main") if err != nil { - t.Errorf("fail read file %v", err) + t.Errorf("fail to clone repo %v", err) + } + glsFile := repoDir + "/gls.go" + data, err := os.ReadFile(glsFile) + if err != nil { + t.Errorf("fail read file %v file: %s", err, glsFile) return } alias1 := string("_") @@ -87,15 +95,6 @@ func TestPatcher_PatchImports(t *testing.T) { "time" _ "runtime" ) -`), 1) - data2, err := os.ReadFile("../../../../tmp/localsession/backup/xx_test.go") - if err != nil { - t.Errorf("fail read file %v", err) - return - } - data2 = bytes.Replace(data2, []byte(`package backup -`), []byte(`package backup -import "fmt" `), 1) type args struct { @@ -107,35 +106,17 @@ import "fmt" want []byte wantErr bool }{ - // { - // name: "empty new", - // args: args{ - // file: &uniast.File{ - // Name: "gls.go", - // Imports: []uniast.Import{}, - // Path: "gls.go", - // }, - // }, - // want: data, - // wantErr: false, - // }, - // { - // name: "empty old", - // args: args{ - // file: &uniast.File{ - // Name: "backup/xx_test.go", - // Imports: []uniast.Import{ - // { - // Path: `"fmt"`, - // Alias: nil, - // }, - // }, - // Path: "backup/xx_test.go", - // }, - // }, - // want: data2, - // wantErr: false, - // }, + { + name: "empty new", + args: args{ + file: &uniast.File{ + Imports: []uniast.Import{}, + Path: glsFile, + }, + }, + want: data, + wantErr: false, + }, { name: "add", args: args{ @@ -146,7 +127,7 @@ import "fmt" Alias: &alias1, }, }, - Path: "gls.go", + Path: glsFile, }, }, want: data1, @@ -158,7 +139,8 @@ import "fmt" t.Run(tt.name, func(t *testing.T) { old, err := os.ReadFile(tt.args.file.Path) if err != nil { - t.Errorf("fail read file %v", err) + println("wtf", tt.args.file.Path) + t.Errorf("fail read file %v file: %s", err, tt.args.file.Path) return } got, err := p.PatchImports(tt.args.file.Imports, old) diff --git a/lang/lsp/client_test.go b/lang/lsp/client_test.go deleted file mode 100644 index 22b229c2..00000000 --- a/lang/lsp/client_test.go +++ /dev/null @@ -1,266 +0,0 @@ -// Copyright 2025 CloudWeGo Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package lsp - -import ( - "context" - "encoding/json" - "fmt" - "os" - "path/filepath" - "sync" - "testing" - "time" - - "github.com/cloudwego/abcoder/lang/log" -) - -var golangLSP *LSPClient -var rustLSP *LSPClient -var rootDir = "../../../testdata" - -func testClientInit(t *testing.T) { - var err error - rootDir, err = filepath.Abs(rootDir) - if err != nil { - t.Fatalf("Failed to get absolute path of testdata: %v", err) - } - sync.OnceFunc(func() { - - log.SetLogLevel(log.DebugLevel) - var err error - golangLSP, err = NewLSPClient(rootDir+"/golang", "", 0, ClientOptions{ - Server: "gopls", - Language: "go", - Verbose: true, - }) - if err != nil { - fmt.Printf("Failed to initialize golang LSP client: %v", err) - os.Exit(1) - } - - wg := sync.WaitGroup{} - wg.Add(1) - // go func() { - // defer wg.Done() - // rustLSP, err = NewLSPClient("/root/codes/abcoder/tmp/lust-example-item", "/root/codes/abcoder/tmp/lust-example-item/src/lib.rs", time.Second*30, ClientOptions{ - // Server: "rust-analyzer", - // Language: "rust", - // Verbose: true, - // }) - // if err != nil { - // fmt.Printf("Failed to initialize rust: %v", err) - // os.Exit(1) - // } - // }() - go func() { - defer wg.Done() - rustLSP, err = NewLSPClient(rootDir+"/rust2", rootDir+"/rust2/Cargo.toml", time.Second*15, ClientOptions{ - Server: "rust-analyzer", - Language: "rust", - Verbose: true, - }) - if err != nil { - fmt.Printf("Failed to initialize rust LSP client: %v", err) - } - }() - wg.Wait() - - // c := m.Run() - - // golangLSP.Close() - // rustLSP.Close() - // os.Exit(c) - - })() -} - -func TestGolang(t *testing.T) { - testClientInit(t) - - uri := NewURI(rootDir + "/golang/pkg/entity/entity.go") - - // documentSymbol - t.Run("documentSymbol", func(t *testing.T) { - symbols, err := golangLSP.DocumentSymbols(context.Background(), uri) - if err != nil { - t.Fatalf("Document Symbol failed: %v", err) - } - fmt.Printf("Document Symbol: %#v\n", symbols) - js, err := json.Marshal(symbols) - if err != nil { - t.Fatalf("Marshal Document Symbol failed: %v", err) - } - os.WriteFile("./symbol_golang.json", js, 0644) - }) - - // references - t.Run("references", func(t *testing.T) { - // reference to Function - id := Location{ - URI: uri, - Range: Range{ - Start: Position{ - Line: 8, - Character: 8, - }, - }, - } - references, err := golangLSP.References(context.Background(), id) - if err != nil { - t.Fatalf("Find Reference failed: %v", err) - } - fmt.Printf("Find Reference: %#v\n", references) - }) - - // semanticTokens - t.Run("semanticTokens", func(t *testing.T) { - id := Location{ - URI: uri, - Range: Range{ - Start: Position{ - Line: 0, - Character: 0, - }, - End: Position{ - Line: 40, - Character: 0, - }, - }, - } - tokens, err := golangLSP.SemanticTokens(context.Background(), id) - if err != nil { - t.Fatalf("Semantic Tokens failed: %v", err) - } - fmt.Printf("Semantic Tokens: %#v\n", tokens) - // js, err := json.Marshal(tokens) - // if err != nil { - // t.Fatalf("Marshal Semantic Tokens failed: %v", err) - // } - // os.WriteFile("./sytax-golang.json", js, 0644) - }) -} - -func TestRust(t *testing.T) { - testClientInit(t) - - // url encode - uri := NewURI(rootDir + "/rust2/src/entity/mod.rs") - - // documentSymbol - t.Run("documentSymbol", func(t *testing.T) { - symbols, err := rustLSP.DocumentSymbols(context.Background(), uri) - if err != nil { - t.Fatalf("Document Symbol failed: %v", err) - } - t.Logf("Document Symbol: %#v", symbols) - js, err := json.Marshal(symbols) - if err != nil { - t.Fatalf("Marshal Document Symbol failed: %v", err) - } - println(string(js)) - }) - - // references - t.Run("references", func(t *testing.T) { - // reference to Function - id := Location{ - URI: uri, - Range: Range{ - Start: Position{ - Line: 13, - Character: 13, - }, - }, - } - references, err := rustLSP.References(context.Background(), id) - if err != nil { - t.Fatalf("Find Reference failed: %v", err) - } - t.Logf("Find Reference: %#v", references) - }) - - // semanticTokens - t.Run("semanticTokens", func(t *testing.T) { - id := Location{ - URI: uri, - Range: Range{ - Start: Position{ - Line: 0, - Character: 0, - }, - End: Position{ - Line: 66, - Character: 0, - }, - }, - } - tokens, err := rustLSP.SemanticTokens(context.Background(), id) - if err != nil { - t.Fatalf("Semantic Tokens failed: %v", err) - } - js, err := json.Marshal(tokens) - if err != nil { - t.Fatalf("Marshal Semantic Tokens failed: %v", err) - } - println(string(js)) - }) - - // definition - t.Run("definition", func(t *testing.T) { - uri := NewURI(rootDir + "/rust2/src/main.rs") - definition, err := rustLSP.Definition(context.Background(), uri, Position{9, 27}) - if err != nil { - t.Fatalf("Find Definition failed: %v", err) - } - if len(definition) != 1 { - t.Fatalf("Find Definition failed: %v", definition) - } - t.Logf("Find Definition: %#v", definition) - }) - - t.Run("workspaceSymbol", func(t *testing.T) { - symbols, err := rustLSP.WorkspaceSymbols(context.Background(), "add") - if err != nil { - t.Fatalf("Workspace Symbol failed: %v", err) - } - t.Logf("Workspace Symbol: %#v", symbols) - }) -} - -func TestSearchSymbol(t *testing.T) { - testClientInit(t) - - syms, err := rustLSP.DocumentSymbols(context.Background(), NewURI("/root/codes/abcoder/tmp/lust-example-item/src/lib.rs")) - if err != nil { - t.Fatalf("Document Symbol failed: %v", err) - } - t.Logf("Document Symbol: %#v", syms) - symbols, err := rustLSP.WorkspaceSymbols(context.Background(), "Request") - if err != nil { - t.Fatalf("Workspace Symbol failed: %v", err) - } - t.Logf("Workspace Symbol: %#v", symbols) -} - -func TestFileStructure(t *testing.T) { - testClientInit(t) - - symbols, err := rustLSP.FileStructure(context.Background(), NewURI("/root/codes/abcoder/tmp/lust-example-item/target/debug/build/lust-gen-e0683cdee43abe70/out/lust_gen.rs")) - if err != nil { - t.Fatalf("File Structure failed: %v", err) - } - t.Logf("File Structure: %#v", symbols) -} diff --git a/lang/lsp/clients_test.go b/lang/lsp/clients_test.go new file mode 100644 index 00000000..3b5e7296 --- /dev/null +++ b/lang/lsp/clients_test.go @@ -0,0 +1,258 @@ +// Copyright 2025 CloudWeGo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lsp + +import ( + "context" + "encoding/json" + "fmt" + "slices" + "strings" + "testing" + + "github.com/cloudwego/abcoder/lang/uniast" +) + +func checkSymNames(t *testing.T, symbols map[Range]*DocumentSymbol, expectedNames []string) { + t.Helper() + var symNames []string + for _, sym := range symbols { + symNames = append(symNames, sym.Name) + } + slices.Sort(symNames) + slices.Sort(expectedNames) + failMsg := "" + if len(symNames) != len(expectedNames) { + failMsg = fmt.Sprintf("Symbol count mismatch: expected %d, got %d", len(expectedNames), len(symNames)) + } + for i := range symNames { + if symNames[i] != expectedNames[i] { + failMsg = fmt.Sprintf("Symbol name mismatch at index %d: expected %s, got %s", i, expectedNames[i], symNames[i]) + break + } + } + if failMsg != "" { + t.Fatal(failMsg) + for i := range symNames { + t.Logf("Symbol[%d]: %s", i, symNames[i]) + } + for i := range expectedNames { + t.Logf("Expected[%d]: %s", i, expectedNames[i]) + } + } +} + +func TestGolangLSP(t *testing.T) { + golangLSP, goTestCase, err := InitLSPForFirstTest(uniast.Golang, "gopls") + if err != nil { + t.Fatalf("Failed to initialize Golang LSP client: %v", err) + } + defer golangLSP.Close() + + uri := NewURI(goTestCase + "/pkg/entity/entity.go") + // documentSymbol + expectedSymNames := `(MyStruct).String +(MyStructC).String +(MyStructD).String +A +G1 +Integer +InterfaceB +MyStruct +MyStructC +MyStructD +V1` + // references + refRange := Range{ // MyStructC + Start: Position{ + Line: 16, + Character: 5, + }, + } + + // documentSymbol + t.Run("documentSymbol", func(t *testing.T) { + symbols, err := golangLSP.DocumentSymbols(context.Background(), uri) + if err != nil { + t.Fatalf("Document Symbol failed: %v", err) + } + checkSymNames(t, symbols, strings.Split(expectedSymNames, "\n")) + if _, err := json.Marshal(symbols); err != nil { + t.Fatalf("Marshal Document Symbols failed: %v", err) + } + }) + + // references + t.Run("references", func(t *testing.T) { + id := Location{ + URI: uri, + Range: refRange, + } + references, err := golangLSP.References(context.Background(), id) + if err != nil { + t.Fatalf("Reference failed: %v", err) + } + if len(references) != 3 { + t.Fatalf("Expected 3 references, got %d", len(references)) + } + if _, err := json.Marshal(references); err != nil { + t.Fatalf("Marshal References failed: %v", err) + } + }) +} + +func TestRustLSP(t *testing.T) { + rustLSP, rustTestCase, err := InitLSPForFirstTest(uniast.Rust, "rust-analyzer") + if err != nil { + t.Fatalf("Failed to initialize rust LSP client: %v", err) + } + defer rustLSP.Close() + + // documentSymbol + entity_mod_uri := NewURI(rustTestCase + "/src/entity/mod.rs") + expectedSymNames := `a +A +add +add +add +b +B +func +impl MyStruct +impl MyTrait for MyStruct +impl std::ops::Add for MyInt2 +inter +MyEnum +MyInt +MY_INT +MyInt2 +my_macro +MY_STATIC +MyStruct +my_trait +my_trait +MyTrait +new +Output` + t.Run("documentSymbol", func(t *testing.T) { + symbols, err := rustLSP.DocumentSymbols(context.Background(), entity_mod_uri) + if err != nil { + t.Fatalf("Document Symbol failed: %v", err) + } + checkSymNames(t, symbols, strings.Split(expectedSymNames, "\n")) + if _, err := json.Marshal(symbols); err != nil { + t.Fatalf("Marshal Document Symbols failed: %v", err) + } + }) + + // references + refRange := Range{ + Start: Position{ + Line: 13, + Character: 13, + }, + } + t.Run("references", func(t *testing.T) { + id := Location{ + URI: entity_mod_uri, + Range: refRange, + } + references, err := rustLSP.References(context.Background(), id) + if err != nil { + t.Fatalf("Find Reference failed: %v", err) + } + if _, err := json.Marshal(references); err != nil { + t.Fatalf("Marshal Reference failed: %v", err) + } + }) + + // semanticTokens + semtoksRange := Range{ + Start: Position{ + Line: 0, + Character: 0, + }, + End: Position{ + Line: 66, + Character: 0, + }, + } + t.Run("semanticTokens", func(t *testing.T) { + id := Location{ + URI: entity_mod_uri, + Range: semtoksRange, + } + tokens, err := rustLSP.SemanticTokens(context.Background(), id) + if err != nil { + t.Fatalf("Semantic Tokens failed: %v", err) + } + if _, err := json.Marshal(tokens); err != nil { + t.Fatalf("Marshal Semantic Tokens failed: %v", err) + } + }) + + // definition + main_uri := NewURI(rustTestCase + "/src/main.rs") + t.Run("definition", func(t *testing.T) { + for _, pos := range []Position{ + {Line: 37, Character: 23}, + {Line: 20, Character: 4}, + {Line: 21, Character: 4}, + {Line: 27, Character: 16}, + {Line: 23, Character: 24}, + {Line: 24, Character: 11}, + {Line: 18, Character: 4}, + {Line: 17, Character: 20}, + {Line: 33, Character: 23}, + {Line: 38, Character: 18}, + {Line: 18, Character: 31}, + {Line: 19, Character: 35}, + {Line: 37, Character: 12}, + {Line: 20, Character: 27}, + {Line: 16, Character: 3}, + } { + definition, err := rustLSP.Definition(context.Background(), main_uri, pos) + if err != nil { + t.Fatalf("Find Definition failed: %v", err) + } + if len(definition) != 1 { + t.Fatalf("Find Definition should have found entry, but got none at %#v", pos) + } + // t.Logf("Find Definition %#v ->\n%#v", pos, definition) + } + }) + + // workspaceSymbol + t.Run("workspaceSymbol", func(t *testing.T) { + symbols, err := rustLSP.WorkspaceSymbols(context.Background(), "add") + if err != nil { + t.Fatalf("Workspace Symbol failed: %v", err) + } + if _, err := json.Marshal(symbols); err != nil { + t.Fatalf("Marshal Workspace Symbols failed: %v", err) + } + }) + + // fileStructure + t.Run("FileStructure", func(t *testing.T) { + symbols, err := rustLSP.FileStructure(context.Background(), main_uri) + if err != nil { + t.Fatalf("File Structure failed: %v", err) + } + if _, err := json.Marshal(symbols); err != nil { + t.Fatalf("Marshal File Structure failed: %v", err) + } + }) +} diff --git a/lang/lsp/lsp_test.go b/lang/lsp/lsp_test.go deleted file mode 100644 index 93351957..00000000 --- a/lang/lsp/lsp_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2025 CloudWeGo Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package lsp - -import ( - "encoding/json" - "reflect" - "testing" -) - -func TestDocumentSymbol_MarshalJSON(t *testing.T) { - tests := []struct { - name string - fields DocumentSymbol - want []byte - wantErr bool - }{ - { - name: "rust2", - fields: DocumentSymbol{}, - want: []byte(`{"name":"","kind":0,"tags":null,"location":":1:1-1:1","children":null,"text":"","tokens":null}`), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := json.Marshal(&tt.fields) - if (err != nil) != tt.wantErr { - t.Errorf("DocumentSymbol.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("DocumentSymbol.MarshalJSON() = %v, want %v", string(got), string(tt.want)) - } - }) - } -} diff --git a/lang/lsp/testutils.go b/lang/lsp/testutils.go new file mode 100644 index 00000000..2cd00ac8 --- /dev/null +++ b/lang/lsp/testutils.go @@ -0,0 +1,45 @@ +// Copyright 2025 CloudWeGo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lsp + +import ( + "log" + "time" + + "github.com/cloudwego/abcoder/lang/testutils" + "github.com/cloudwego/abcoder/lang/uniast" +) + +var clients = make(map[uniast.Language]*LSPClient) + +func InitLSPForFirstTest(lang uniast.Language, server string) (*LSPClient, string, error) { + testdata := testutils.FirstTest(string(lang)) + if client, exists := clients[lang]; exists { + return client, testdata, nil + } + + client, err := NewLSPClient(testdata, "", 0, ClientOptions{ + Server: server, + Language: uniast.Language(lang), + Verbose: true, + }) + if err != nil { + log.Fatalf("Failed to initialize %s LSP client: %v", lang, err) + return nil, "", err + } + clients[lang] = client + time.Sleep(3 * time.Second) // wait for LSP server to be ready + return client, testdata, nil +} diff --git a/lang/lsp/utils.go b/lang/lsp/utils.go index 7ccaaa60..4dab55c3 100644 --- a/lang/lsp/utils.go +++ b/lang/lsp/utils.go @@ -54,11 +54,10 @@ func ChunkHead(text string, textPos Position, pos Position) string { } // calculate the relative index of a position to a text -func RelativePostionWithLines(lines []int, textPos Position, pos Position) int { +func RelativePostionWithLines(lines []int, basePos Position, pos Position) int { // find the line of the position - l := pos.Line - textPos.Line - - return lines[l] + pos.Character - textPos.Character + l := pos.Line - basePos.Line + return lines[l] + pos.Character - basePos.Character } func PositionOffset(file_uri string, text string, pos Position) int { @@ -67,10 +66,6 @@ func PositionOffset(file_uri string, text string, pos Position) int { return -1 } lines := utils.CountLinesCached(file_uri, text) - - // lines := utils.CountLinesPooled(text) - // defer utils.PutCount(lines) - return RelativePostionWithLines(*lines, Position{Line: 0, Character: 0}, pos) } diff --git a/lang/parse_test.go b/lang/parse_test.go new file mode 100644 index 00000000..cfff7353 --- /dev/null +++ b/lang/parse_test.go @@ -0,0 +1,157 @@ +/** + * Copyright 2025 ByteDance Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package lang + +import ( + "context" + "encoding/json" + "io" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/cloudwego/abcoder/lang/collect" + "github.com/cloudwego/abcoder/lang/testutils" + "github.com/cloudwego/abcoder/lang/uniast" + "github.com/pkg/errors" +) + +func defaultOptions(lang string) ParseOptions { + lsp := map[string]string{ + "rust": "rust-analyzer", + } + return ParseOptions{ + LSP: lsp[lang], + Verbose: false, + CollectOption: collect.CollectOption{ + Language: uniast.Language(lang), + LoadExternalSymbol: false, + NeedStdSymbol: false, + NoNeedComment: true, + NotNeedTest: true, + Excludes: []string{}, + }, + RepoID: "", + } +} + +func readFile(path string) ([]byte, error) { + file, err := os.Open(path) + if err != nil { + return nil, errors.Wrap(err, "failed to open file") + } + defer file.Close() + return io.ReadAll(file) +} + +// Compare func.Content with file[startOffset:endOffset]. +func matchWithImplhead(lang, expected string, fn *uniast.Function) bool { + actual := fn.Content + // compare by verbatim for non-methods + if !fn.IsMethod { + return actual == expected + } + // compare with implhead for methods + ptnFrags := map[string][]string{ + "rust": { + `impl[^{]*\{`, + `(type[^;]*;)*`, + regexp.QuoteMeta(expected), + `\}`, + }, + }[lang] + ptn := strings.Join(ptnFrags, `\s*`) + reptn := regexp.MustCompile(ptn) + return reptn.MatchString(actual) +} + +func checkFunctionConsistency(t *testing.T, lang string, fn *uniast.Function, workspace string) { + println("checking fn ", fn.Name, "in", workspace) + file := filepath.Join(workspace, fn.File) + fileContents, err := readFile(file) + if err != nil { + t.Fatalf("Failed to read file %s: %v", file, err) + } + expectedContent := string(fileContents[fn.StartOffset:fn.EndOffset]) + if !matchWithImplhead(lang, expectedContent, fn) { + t.Fatalf("Function %s content mismatch: expected\n%q\ngot\n%q\nfn: %#v\n", fn.Name, expectedContent, fn.Content, fn) + } +} + +func checkTypeConsistency(t *testing.T, lang string, ty *uniast.Type, workspace string) { + println("checking ty ", ty.Name, "in", workspace) + file := filepath.Join(workspace, ty.File) + fileContents, err := readFile(file) + if err != nil { + t.Fatalf("Failed to read file %s: %v", file, err) + } + expectedContent := string(fileContents[ty.StartOffset:ty.EndOffset]) + if expectedContent != ty.Content { + t.Fatalf("Type %s content mismatch: expected\n%q\ngot\n%q\nty: %#v\n", ty.Name, expectedContent, ty.Content, ty) + } +} + +func checkVarConsistency(t *testing.T, lang string, va *uniast.Var, workspace string) { + println("checking var ", va.Name, "in", workspace) + file := filepath.Join(workspace, va.File) + fileContents, err := readFile(file) + if err != nil { + t.Fatalf("Failed to read file %s: %v", file, err) + } + expectedContent := string(fileContents[va.StartOffset:va.EndOffset]) + if expectedContent != va.Content { + t.Fatalf("Var %s content mismatch: expected\n%q\ngot\n%q\nva: %#v\n", va.Name, expectedContent, va.Content, va) + } +} + +// Checks for all Functions/Types/Vars: +// +// their Content, Start/EndOffset, Line fields are consistent +func checkRepoConsistency(t *testing.T, lang string, repo *uniast.Repository, workspace string) { + t.Helper() + for _, mod := range repo.Modules { + for _, pkg := range mod.Packages { + for _, fn := range pkg.Functions { + checkFunctionConsistency(t, lang, fn, workspace) + } + for _, ty := range pkg.Types { + checkTypeConsistency(t, lang, ty, workspace) + } + for _, va := range pkg.Vars { + checkVarConsistency(t, lang, va, workspace) + } + } + } +} + +func TestParser_NodeFieldsConsistency(t *testing.T) { + checked_languages := []string{"rust", "go"} + for _, lang := range checked_languages { + testCase := testutils.FirstTest(lang) + repobytes, err := Parse(context.Background(), testCase, defaultOptions(lang)) + if err != nil { + t.Fatalf("Parse() failed: %v", err) + } + var repo uniast.Repository + if err := json.Unmarshal(repobytes, &repo); err != nil { + t.Fatalf("Unmarshal() failed: %v", err) + } + checkRepoConsistency(t, lang, &repo, testCase) + } +} diff --git a/lang/patch/lib.go b/lang/patch/lib.go index 36a6a5f3..563eda07 100644 --- a/lang/patch/lib.go +++ b/lang/patch/lib.go @@ -60,9 +60,9 @@ func (p *Patcher) SetPatchNodes(ps Patches) { } type Options struct { - RepoDir string - OutDir string - DefaultLanuage uniast.Language + RepoDir string + OutDir string + DefaultLanguage uniast.Language } func NewPatcher(repo *uniast.Repository, opts Options) *Patcher { @@ -97,14 +97,14 @@ next_dep: mod := p.repo.GetModule(patch.Id.ModPath) if mod == nil { - mod = uniast.NewModule(patch.Id.ModPath, "", p.DefaultLanuage) + mod = uniast.NewModule(patch.Id.ModPath, "", p.DefaultLanguage) p.repo.SetModule(patch.Id.ModPath, mod) } f := mod.GetFile(patch.File) if f == nil { f = uniast.NewFile(patch.File) - mod.SetFile(patch.File, f) + mod.CreateFile(patch.File, f) } fl := node.FileLine() @@ -231,7 +231,7 @@ func (p *Patcher) Flush() error { func (p *Patcher) getLangWriter(lang uniast.Language) uniast.Writer { if lang == "" || lang == uniast.Unknown { - lang = p.DefaultLanuage + lang = p.DefaultLanguage } switch lang { case uniast.Golang: diff --git a/lang/patch/lib_test.go b/lang/patch/lib_test.go index 920f56be..ce9415b5 100644 --- a/lang/patch/lib_test.go +++ b/lang/patch/lib_test.go @@ -19,29 +19,39 @@ package patch import ( "testing" + "github.com/cloudwego/abcoder/lang/testutils" "github.com/cloudwego/abcoder/lang/uniast" ) -var root = "../../../tmp" - +// Expected to fail because the AST file contains local paths. func TestPatcher(t *testing.T) { - // Load repository - repo, err := uniast.LoadRepo(root + "/localsession.json") + // Load AST + t.Logf("Loading AST file for localsession...") + astFile := testutils.GetTestAstFile("localsession") + repo, err := uniast.LoadRepo(astFile) if err != nil { - t.Errorf("failed to load repo: %v", err) + t.Fatalf("failed to load repo: %v", err) + } + + // Load repo from git + repoURL := "github.com/cloudwego/localsession" + repoDir, err := testutils.GitCloneFast(repoURL, "localsession", "main") + if err != nil { + t.Fatalf("failed to clone repo: %v", err) } // Create patcher with options + tmpRoot := testutils.MakeTmpTestdir(true) patcher := NewPatcher(repo, Options{ - RepoDir: root + "/localsession", - OutDir: root + "/localsession2", - DefaultLanuage: uniast.Golang, + RepoDir: repoDir, + OutDir: tmpRoot + "/localsession2", + DefaultLanguage: uniast.Golang, }) // Create a test patch testPatches := []Patch{ { - Id: uniast.Identity{ModPath: "github.com/cloudwego/localsession", PkgPath: "github.com/cloudwego/localsession/backup", Name: "DefaultOptions"}, + Id: uniast.Identity{ModPath: repoURL, PkgPath: repoURL + "/backup", Name: "DefaultOptions"}, Codes: `func DefaultOptions() Options { ret := Options{ Enable: false, @@ -57,7 +67,7 @@ func TestPatcher(t *testing.T) { }, }, { - Id: uniast.Identity{ModPath: "github.com/cloudwego/localsession", PkgPath: "github.com/cloudwego/localsession/backup", Name: "DefaultOptions2"}, + Id: uniast.Identity{ModPath: repoURL, PkgPath: repoURL + "/backup", Name: "DefaultOptions2"}, Codes: `func DefaultOptions2() Options { ret := Options{ Enable: false, @@ -69,7 +79,7 @@ func TestPatcher(t *testing.T) { Type: uniast.FUNC, }, { - Id: uniast.Identity{ModPath: "github.com/cloudwego/localsession", PkgPath: "github.com/cloudwego/localsession/backup", Name: "TestCase"}, + Id: uniast.Identity{ModPath: repoURL, PkgPath: repoURL + "/backup", Name: "TestCase"}, Codes: `type TestCase struct { Enable bool }`, @@ -77,7 +87,7 @@ func TestPatcher(t *testing.T) { Type: uniast.TYPE, }, { - Id: uniast.Identity{ModPath: "github.com/cloudwego/localsession", PkgPath: "github.com/cloudwego/localsession/backup", Name: "TestFunc"}, + Id: uniast.Identity{ModPath: repoURL, PkgPath: repoURL + "/backup", Name: "TestFunc"}, Codes: ` func TestFunc(t *testing.T) {}`, File: "backup/abcoder_test.go", @@ -91,12 +101,14 @@ func TestPatcher(t *testing.T) { // Apply the patches for _, testPatch := range testPatches { if err := patcher.Patch(testPatch); err != nil { - t.Errorf("failed to patch: %v", err) + t.Fatalf("failed to patch: %v", err) } } // Flush changes if err := patcher.Flush(); err != nil { - t.Errorf("failed to flush: %v", err) + t.Fatalf("failed to flush: %v", err) } + + // TODO: check patching work as expected } diff --git a/lang/rust/ast_test.go b/lang/rust/ast_test.go index 7f1ba8c3..225ad2b2 100644 --- a/lang/rust/ast_test.go +++ b/lang/rust/ast_test.go @@ -19,7 +19,7 @@ import ( "testing" ) -func Test_Rust(t *testing.T) { +func TestRustDependencyTree(t *testing.T) { useStatements, err := ParseUseStatements(`use http::Method as HttpMethod; use http::{Server, Request as HttpRequest, Response::{IntoResponse, StatusCode as HttpStatusCode}}; `) @@ -28,11 +28,27 @@ use http::{Server, Request as HttpRequest, Response::{IntoResponse, StatusCode a return } dependencyTree := BuildDependencyTree(useStatements) - //PrintTree(dependencyTree, "") - for _, r := range dependencyTree.Children { - uses := ConvertTreeToUse(r, "") - for _, u := range uses { - fmt.Println(u) + if len(dependencyTree.Children) != 1 { + t.Fatalf("Expected 1 child, got %d", len(dependencyTree.Children)) + } + uses := ConvertTreeToUse(dependencyTree.Children[0], "") + usePaths := make([]string, len(uses)) + for i, u := range uses { + usePaths[i] = u.Path + } + expectedPaths := []string{ + "use http::Method as HttpMethod;", + "use http::Server;", + "use http::Request as HttpRequest;", + "use http::Response::IntoResponse;", + "use http::Response::StatusCode as HttpStatusCode;", + } + if len(usePaths) != len(expectedPaths) { + t.Fatalf("Expected %d paths, got %d", len(expectedPaths), len(usePaths)) + } + for i := range usePaths { + if usePaths[i] != expectedPaths[i] { + t.Fatalf("Index %d, Expected path\n%s\nbut got\n%s", i, expectedPaths[i], usePaths[i]) } } } diff --git a/lang/rust/repo_test.go b/lang/rust/repo_test.go index 3b6dc336..2b1a60f6 100644 --- a/lang/rust/repo_test.go +++ b/lang/rust/repo_test.go @@ -19,20 +19,24 @@ import ( "time" "github.com/cloudwego/abcoder/lang/log" + "github.com/cloudwego/abcoder/lang/testutils" ) func TestCheckRepo(t *testing.T) { type args struct { repo string } + rust2Path := testutils.TestPath("rust2", "rust") tests := []struct { name string args args want string want1 time.Duration }{ - {"rust2", args{"/home/duanyi.aster/Rust/ABCoder/testdata/rust2"}, "/home/duanyi.aster/Rust/ABCoder/testdata/rust2/src/main.rs", time.Second * 15}, - {"live", args{"/home/duanyi.aster/Rust/ABCoder/tmp/live"}, "/home/duanyi.aster/Rust/ABCoder/tmp/live/src/env.rs", time.Minute * 1}, + {"rust2", + args{rust2Path}, + rust2Path + "/src/main.rs", + time.Second * 15}, } log.SetLogLevel(log.DebugLevel) diff --git a/lang/rust/rust_test.go b/lang/rust/rust_test.go index b6861187..12548141 100644 --- a/lang/rust/rust_test.go +++ b/lang/rust/rust_test.go @@ -15,182 +15,71 @@ package rust import ( - "encoding/json" - "fmt" - "os" "reflect" - "strings" "testing" - lsp "github.com/cloudwego/abcoder/lang/lsp" + "github.com/cloudwego/abcoder/lang/testutils" ) -func TestRustSpec_NameSpace(t *testing.T) { +func TestRustSpec_NameSpaceInternal(t *testing.T) { type args struct { root string } type nameSpace struct { - path string + relPath string wantMod string wantPkg string } + rustTestRoot := testutils.FirstTest("rust") tests := []struct { name string args args - want map[string]string nameSpace []nameSpace + want map[string]string wantErr bool }{ {name: "", - args: args{"/root/codes/abcoder"}, + args: args{rustTestRoot}, nameSpace: []nameSpace{ - {"/root/codes/abcoder/src/lib.rs", "ABCoder", "ABCoder"}, - {"/root/codes/abcoder/src/repo.rs", "ABCoder", "ABCoder::repo"}, - {"/root/codes/abcoder/src/config/mod.rs", "ABCoder", "ABCoder::config"}, - {"/root/codes/abcoder/src/utils/cmd.rs", "ABCoder", "ABCoder::utils::cmd"}, - {"/root/codes/abcoder/testdata/rust2/src/main.rs", "rust2", "rust2"}, - {"/root/codes/abcoder/testdata/rust2/src/entity/mod.rs", "rust2", "rust2::entity"}, - {"/root/.cargo/registry/src/xxx/byted-env-0.2.8/src/lib.rs", "byted-env@0.2.8", "byted-env"}, - {"/root/.cargo/registry/src/xxx/byted-env-0.2.8/src/idc/mod.rs", "byted-env@0.2.8", "byted-env::idc"}, - {"/root/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs", "", "alloc::alloc"}, - {"/root/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/f32.rs", "std", "std::f32"}, + {"/src/main.rs", "rust2", "rust2"}, + {"/src/entity/mod.rs", "rust2", "rust2::entity"}, + {"/src/entity/func.rs", "rust2", "rust2::entity::func"}, + {"/src/entity/inter.rs", "rust2", "rust2::entity::inter"}, + // {"/root/.cargo/registry/src/xxx/byted-env-0.2.8/src/lib.rs", "byted-env@0.2.8", "byted-envs"}, + // {"/root/.cargo/registry/src/xxx/byted-env-0.2.8/src/idc/mod.rs", "byted-env@0.2.8", "byted-env::idc"}, + // {"/root/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/alloc.rs", "", "alloc::alloc"}, + // {"/root/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/f32.rs", "std", "std::f32"}, }, - want: map[string]string{"ABCoder": "/root/codes/abcoder/src", "rust2": "/root/codes/abcoder/testdata/rust2/src"}, + want: map[string]string{ + "rust2": rustTestRoot + "/src"}, wantErr: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := NewRustSpec() + // Workspace got, err := c.WorkSpace(tt.args.root) if (err != nil) != tt.wantErr { - t.Errorf("RustSpec.CollectModules() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("RustSpec.WorkSpace() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("RustSpec.CollectModules() = %v, want %v", got, tt.want) + t.Errorf("RustSpec.WorkSpace() got = %v, want %v", got, tt.want) } - // test namespace + // Namespace for _, ns := range tt.nameSpace { - fmt.Printf("test: %#v\n", ns) - gotMod, gotPkg, err := c.NameSpace(ns.path) + gotMod, gotPkg, err := c.NameSpace(tt.args.root + ns.relPath) if err != nil { t.Errorf("RustSpec.NameSpace() error = %v", err) return } if gotMod != ns.wantMod { - t.Errorf("RustSpec.NameSpace() crate get %v, want %v", gotMod, ns.wantMod) + t.Errorf("RustSpec.NameSpace() crate got %v, want %v", gotMod, ns.wantMod) } if gotPkg != ns.wantPkg { - t.Errorf("RustSpec.NameSpace() mod get %v, want %v", gotPkg, ns.wantPkg) + t.Errorf("RustSpec.NameSpace() mod got %v, want %v", gotPkg, ns.wantPkg) } } }) } } - -func getData(path string, key string) *lsp.DocumentSymbol { - f, err := os.ReadFile(path) - if err != nil { - panic(err) - } - var obj map[string]json.RawMessage - if err := json.Unmarshal(f, &obj); err != nil { - return nil - } - - var js string - for k, v := range obj { - if strings.HasPrefix(k, key) { - js = string(v) - break - } - } - var ret lsp.DocumentSymbol - if err := json.Unmarshal([]byte(js), &ret); err != nil { - return nil - } - return &ret -} - -func TestRustSpec_FunctionSymbol(t *testing.T) { - var file = "../testdata/symbols_rust2-save.json" - tests := []struct { - name string - args lsp.DocumentSymbol - want int - want1 int - want2 int - }{ - { - name: "rust2-write_to_output", - args: *getData(file, "write_to_output Function"), - want: 2, - want1: 1, - want2: 3, - }, - { - name: "rust2-add", - args: *getData(file, "add Function file:///root/codes/abcoder/testdata/rust2/src/entity/mod.rs:16:1-18:2"), - want: 0, - want1: 2, - want2: 1, - }, - { - name: "rust2-my_trait", - args: *getData(file, "my_trait Method file:///root/codes/abcoder/testdata/rust2/src/entity/mod.rs:36:5-36:33"), - want: 0, - want1: 0, - want2: 0, - }, - { - name: "main Function", - args: *getData(file, "main Function"), - want: 0, - want1: 0, - want2: 0, - }, - { - name: "apply Function", - args: *getData(file, "apply_closure Function"), - want: 3, - want1: 2, - want2: 1, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := NewRustSpec() - _, err := c.WorkSpace("/root/codes/abcoder") - if err != nil { - t.Errorf("RustSpec.WorkSpace() error = %v", err) - } - rec, got, got1, got2 := c.FunctionSymbol(tt.args) - if rec != -1 { - t.Logf("FunctionSymbol: %#v", rec) - } - if len(got) != tt.want { - t.Errorf("RustSpec.FunctionSymbol() got = %v, want %v", got, tt.want) - } - if len(got1) != tt.want1 { - t.Errorf("RustSpec.FunctionSymbol() got1 = %v, want %v", got1, tt.want1) - } - if len(got2) != tt.want2 { - t.Errorf("RustSpec.FunctionSymbol() got2 = %v, want %v", got2, tt.want2) - } - }) - } -} - -func BenchmarkRustSpec_FunctionSymbol(b *testing.B) { - var file = "../../testdata/symbols_rust2-save.json" - c := NewRustSpec() - _, err := c.WorkSpace("/root/codes/abcoder") - if err != nil { - b.Errorf("RustSpec.WorkSpace() error = %v", err) - } - ds := *getData(file, "write_to_output Function") - b.ResetTimer() - for i := 0; i < b.N; i++ { - c.FunctionSymbol(ds) - } -} diff --git a/lang/rust/spec.go b/lang/rust/spec.go index e6b1aa20..36daae38 100644 --- a/lang/rust/spec.go +++ b/lang/rust/spec.go @@ -448,6 +448,7 @@ func (c *RustSpec) collect(i *int, lines []string, path string, rets map[string] // if err != nil { // rel = dir // } + // TODO: duplicate append c.crates = append(c.crates, Module{ Name: m[1], Path: dir, diff --git a/lang/rust/utils/lsp.go b/lang/rust/utils/lsp.go index 22fd711f..2fb28922 100644 --- a/lang/rust/utils/lsp.go +++ b/lang/rust/utils/lsp.go @@ -17,10 +17,12 @@ package utils import ( "context" "path/filepath" + "regexp" "strings" "sync" "time" + "github.com/cloudwego/abcoder/lang/log" lsp "github.com/cloudwego/abcoder/lang/lsp" ) @@ -113,7 +115,13 @@ func hasIdent(text string, token string) bool { // continue // } // } - return strings.Contains(text, token) + // match regex: \ in text + if token == "" { + log.Error("token cannot be empty") + return false + } + ptn := regexp.MustCompile(`\b` + regexp.QuoteMeta(token) + `\b`) + return ptn.MatchString(text) } // func isAlpha(r byte) bool { diff --git a/lang/rust/utils/lsp_test.go b/lang/rust/utils/lsp_test.go index e019b46e..d95f2eb4 100644 --- a/lang/rust/utils/lsp_test.go +++ b/lang/rust/utils/lsp_test.go @@ -1,11 +1,11 @@ // Copyright 2025 CloudWeGo Authors -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// +// // https://www.apache.org/licenses/LICENSE-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -29,8 +29,9 @@ func Test_hasIdent(t *testing.T) { args args want bool }{ - {"", args{"ExampleService", "ExampleService"}, true}, - {"", args{strings.ToLower(`impl< + {"simple", args{"ExampleService", "ExampleService"}, true}, + {"simple nonword", args{"ExampleService", "ExampleServicex"}, false}, + {"realworld", args{strings.ToLower(`impl< S: ::volo::service::Service< ::volo_thrift::context::ClientContext, ExampleServiceRequestSend, diff --git a/lang/testutils/testutils.go b/lang/testutils/testutils.go new file mode 100644 index 00000000..d7ca0513 --- /dev/null +++ b/lang/testutils/testutils.go @@ -0,0 +1,138 @@ +// Copyright 2025 CloudWeGo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testutils + +import ( + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "sort" +) + +// Hack to get the project root directory from go tests. +// Go tests start from the directory where the test file is located, +// causing the relative path to testdata files to be unstable. +func GetTestDataRoot() string { + _, currentFilePath, _, ok := runtime.Caller(0) + if !ok { + panic("failed to get caller information") + } + projectRoot := filepath.Dir(currentFilePath) + for { + goModPath := filepath.Join(projectRoot, "go.mod") + if _, err := os.Stat(goModPath); err == nil { + break + } + parentDir := filepath.Dir(projectRoot) + if parentDir == projectRoot { + panic("could not find project root (go.mod not found)") + } + projectRoot = parentDir + } + rootDir, err := filepath.Abs(filepath.Join(projectRoot, "testdata")) + if err != nil { + panic("Failed to get absolute path of testdata: " + err.Error()) + } + if _, err := os.Stat(rootDir); os.IsNotExist(err) { + log.Fatalf("Test data directory does not exist: %s", rootDir) + } + return rootDir +} + +func MakeTmpTestdir(reset bool) string { + rootDir := GetTestDataRoot() + tmpDir := filepath.Join(rootDir, "tmp") + if reset { + if err := os.RemoveAll(tmpDir); err != nil { + panic("Failed to remove old tmp directory: " + err.Error()) + } + } + if _, err := os.Stat(tmpDir); os.IsNotExist(err) { + if err := os.Mkdir(tmpDir, 0755); err != nil { + panic("Failed to create tmp directory: " + err.Error()) + } + } + return tmpDir +} + +func GitCloneFast(repoURL, dir, branch string) (string, error) { + rootDir := GetTestDataRoot() + repoDir := filepath.Join(rootDir, "repos", dir) + if _, err := os.Stat(repoDir); !os.IsNotExist(err) { + cmd := exec.Command("git", "-C", repoDir, "status") + if err := cmd.Run(); err == nil { + return repoDir, nil + } else { + return "", fmt.Errorf("bad existing repo %s: %w", repoDir, err) + } + } + if err := os.MkdirAll(repoDir, 0755); err != nil { + return "", fmt.Errorf("failed to create repo directory: %w", err) + } + cmd := exec.Command("git", "clone", "--depth", "1", "--branch", branch, "https://"+repoURL, repoDir) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("git clone failed: %w", err) + } + return repoDir, nil +} + +func GetTestAstFile(name string) string { + rootDir := GetTestDataRoot() + astFile := filepath.Join(rootDir, "asts", name+".json") + if _, err := os.Stat(astFile); os.IsNotExist(err) { + panic(fmt.Sprintf("AST file does not exist: %s", astFile)) + } + return astFile +} + +func ListTests(lang string) []string { + var testcases []string + test_root := filepath.Join(GetTestDataRoot(), lang) + entries, err := os.ReadDir(test_root) + if err != nil || len(entries) == 0 { + panic(fmt.Sprintf("Failed to read test directory %s: %v", test_root, err)) + } + for _, entry := range entries { + if entry.IsDir() { + testcases = append(testcases, filepath.Join(test_root, entry.Name())) + } + } + sort.Slice(testcases, func(i, j int) bool { + return filepath.Base(testcases[i]) < filepath.Base(testcases[j]) + }) + return testcases +} + +func TestPath(name, lang string) string { + testcases := ListTests(lang) + for _, test := range testcases { + ptn := fmt.Sprintf("^\\d+_%s$", regexp.QuoteMeta(name)) + matched, _ := regexp.MatchString(ptn, filepath.Base(test)) + if matched { + return test + } + } + panic(fmt.Sprintf("Test case %s not found in language %s, available: %v", name, lang, testcases)) +} + +func FirstTest(lang string) string { + return ListTests(lang)[0] +} diff --git a/lang/uniast/ast.go b/lang/uniast/ast.go index dde92030..394acea3 100644 --- a/lang/uniast/ast.go +++ b/lang/uniast/ast.go @@ -159,14 +159,15 @@ func (i Import) Equals(other Import) bool { } func NewFile(path string) *File { - // abs, _ := filepath.Abs(path) ret := File{ Path: path, } return &ret } -func (m Module) SetFile(path string, file *File) { +// Create a NEW entry in m.Files for file. +// If entry already exists, do nothing. +func (m Module) CreateFile(path string, file *File) { if m.Files == nil { m.Files = map[string]*File{} } diff --git a/lang/uniast/ast_test.go b/lang/uniast/ast_test.go index 324fcba7..62af3afd 100644 --- a/lang/uniast/ast_test.go +++ b/lang/uniast/ast_test.go @@ -20,27 +20,25 @@ import ( "encoding/json" "os" "testing" -) -var testdata = "../../testdata" + "github.com/cloudwego/abcoder/lang/testutils" +) func TestRepository_BuildGraph(t *testing.T) { - var r Repository - data, err := os.ReadFile(testdata + "/ast/localsession.json") + astFile := testutils.GetTestAstFile("localsession") + r, err := LoadRepo(astFile) if err != nil { - t.Fatal(err) - } - if err := json.Unmarshal(data, &r); err != nil { - t.Fatal(err) + t.Fatalf("failed to load repo: %v", err) } if err := r.BuildGraph(); err != nil { - t.Fatal(err) + t.Fatalf("failed to build graph: %v", err) } if js, err := json.Marshal(r); err != nil { - t.Fatal(err) + t.Fatalf("failed to marshal repo: %v", err) } else { - if err := os.WriteFile(testdata+"/ast/localsession_g.json", js, os.FileMode(0644)); err != nil { - t.Fatal(err) + astFileWithGraph := testutils.GetTestDataRoot() + "/asts/localsession_g.json" + if err := os.WriteFile(astFileWithGraph, js, 0644); err != nil { + t.Fatalf("failed to write repo with graph: %v", err) } } } diff --git a/script/check_all_linenos.sh b/script/check_all_linenos.sh deleted file mode 100755 index 5d397943..00000000 --- a/script/check_all_linenos.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -# Copyright 2025 CloudWeGo Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -root=$(dirname $(realpath $(dirname $0))) -cd $root - -mkdir -p testdata/jsons - -do_test() { - name="ast" - lang=$1 - srcpath=$2 - flags=$4 - - echo "go run . parse $lang $srcpath -verbose --no-need-comment > testdata/jsons/$name.json" - go run . parse $lang $srcpath -verbose --no-need-comment > testdata/jsons/$name.json - python3 script/check_lineno.py --json testdata/jsons/$name.json --base $srcpath $flags > testdata/jsons/$name.check - - if grep -q "All functions verified successfully!" testdata/jsons/$name.check; then - echo " [PASS]" - else - echo " [FAIL]" - exit 1 - fi -} -do_test go testdata/golang "--zero_linebase" -do_test rust testdata/rust2 "--zero_linebase --implheads" diff --git a/script/check_lineno.py b/script/check_lineno.py deleted file mode 100644 index 919226a6..00000000 --- a/script/check_lineno.py +++ /dev/null @@ -1,188 +0,0 @@ -"""检查 json 中诸符号的 StartOffset, EndOffset, Line, Content 的一致性 -(假设本文件在 src/lang 中) - -例如检查本项目: - - $ ./lang -d -v --no-need-comment collect go . > lang.json - # 应当成功,尤其应当是 --zero_linebase(行号从 0 开始) - $ python3 check.py --json lang.json --base . --zero_linebase - -检查 rust 项目 - - $ ./lang -d -v --no-need-comment collect rust ../../testdata/rust2 > rust2.json - $ python3 check.py --json lang.json --base . --zero_linebase --implheads -""" -import json -import os -import argparse -import sys -from collections import defaultdict - - -def trim_multiline(s, max_lines=5): - lines = s.splitlines() - if len(lines) > max_lines: - return "\n".join(lines[:max_lines]) + "\n..." - return s - - -def safe_decode(b): - try: - return b.decode("utf-8") - except UnicodeDecodeError: - return b.decode("utf-8", errors="replace") - - -def verify_function_content( - json_path, - base_dir=".", - bail_on_error=False, - filter_files=None, - filter_funcs=None, - zero_linebase=False, - implheads=False, -): - with open(json_path, "r", encoding="utf-8") as f: - data = json.load(f) - - modules = data.get("Modules", {}) - errors = defaultdict(list) - - for module_name, module in modules.items(): - packages = module.get("Packages", {}) - for package_name, package in packages.items(): - functions = package.get("Functions", {}) - for func_name, func in functions.items(): - file_name = func.get("File") - if not file_name: - continue - if filter_files and file_name not in filter_files: - continue - if filter_funcs and func_name not in filter_funcs: - continue - - file_path = os.path.join(base_dir, file_name) - try: - with open(file_path, "rb") as src: - content_bytes = src.read() - except FileNotFoundError: - print(f"[ERROR] File not found: {file_path}") - errors[file_name].append(func_name) - if bail_on_error: - sys.exit(1) - continue - - start = func["StartOffset"] - end = func["EndOffset"] - expected_content = func["Content"] - actual_bytes = content_bytes[start:end] - actual_content = safe_decode(actual_bytes) - - line_number = func["Line"] - content_str = safe_decode(content_bytes) - file_lines = content_str.splitlines() - - try: - if zero_linebase: - actual_line_content = file_lines[line_number].strip() - else: - actual_line_content = file_lines[line_number - 1].strip() - except IndexError: - actual_line_content = "" - - if implheads: - offset_match = actual_content in expected_content - line_match = any( - line.strip() == actual_line_content.strip() - for line in expected_content.splitlines() - ) - else: - offset_match = actual_content == expected_content - expected_line_start = ( - expected_content.splitlines()[0].strip() - if expected_content - else "" - ) - line_match = actual_line_content == expected_line_start - - print(f"[{module_name}/{package_name}] Checking function: {func_name}") - if not offset_match: - print(" [Mismatch] Offset content does not match.") - print(" Expected:\n" + trim_multiline(expected_content)) - print(" Actual:\n" + trim_multiline(actual_content)) - if not line_match: - display_line_number = line_number if zero_linebase else line_number - print(f" [Mismatch] Line {display_line_number} mismatch:") - print(f" Expected line (from JSON content):") - if implheads: - print(f" Any line in expected content matching actual line:") - else: - print(f" {expected_line_start}") - print(f" Actual line:") - print(f" {actual_line_content}") - if not offset_match or not line_match: - errors[file_name].append(func_name) - if bail_on_error: - sys.exit(1) - if offset_match and line_match: - print(" [OK] Function content and line verified.") - print() - - if errors: - print("===== MISMATCH SUMMARY =====") - for file, funcs in errors.items(): - print(f"File: {file}") - for func in funcs: - print(f" - {func}") - print("============================") - else: - print("✅ All functions verified successfully!") - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description="Verify function content from JSON and source files." - ) - parser.add_argument( - "--json", type=str, default="input.json", help="Path to the JSON file" - ) - parser.add_argument( - "--base", type=str, default=".", help="Base directory for source files" - ) - parser.add_argument( - "--bail_on_error", action="store_true", help="Stop at first error" - ) - parser.add_argument( - "--filter_file", - type=str, - help="Comma-separated list of files to check (e.g. 'main.go,util.go')", - ) - parser.add_argument( - "--filter_func", - type=str, - help="Comma-separated list of function names to check", - ) - parser.add_argument( - "--zero_linebase", - action="store_true", - help="Line numbers in JSON are 0-based instead of 1-based", - ) - parser.add_argument( - "--implheads", - action="store_true", - help="Allow actual content to be a substring of expected content and lines to match any line", - ) - - args = parser.parse_args() - filter_files = set(args.filter_file.split(",")) if args.filter_file else None - filter_funcs = set(args.filter_func.split(",")) if args.filter_func else None - - verify_function_content( - json_path=args.json, - base_dir=args.base, - bail_on_error=args.bail_on_error, - filter_files=filter_files, - filter_funcs=filter_funcs, - zero_linebase=args.zero_linebase, - implheads=args.implheads, - ) diff --git a/testdata/README_en.md b/testdata/README_en.md new file mode 100644 index 00000000..36cfdb76 --- /dev/null +++ b/testdata/README_en.md @@ -0,0 +1,5 @@ +# Naming of tests +Each test case is a self-contained project/directory, located at `testdata/{language}/index_{name}`. +The `index` is a 0-based number, which `go test` uses to determine the test execution order. + +Note that `go test` occasionally only runs test cases prefixed with `0_xx`. diff --git a/testdata/README_zh.md b/testdata/README_zh.md new file mode 100644 index 00000000..b7e5ad82 --- /dev/null +++ b/testdata/README_zh.md @@ -0,0 +1,5 @@ +# 测例规范 +每个测例是对应语言的一个单独项目(目录),放在 `testdata/{lang}/index_{name}` 里。 +其中 index 是 0 开始的数字,go test 会按照这个顺序测试。 + +go test 中有时只会测试 0_xx 的测例。 diff --git a/testdata/cxxsimple/main.c b/testdata/cxx/0_simple/main.c similarity index 100% rename from testdata/cxxsimple/main.c rename to testdata/cxx/0_simple/main.c diff --git a/testdata/cxxsimple/pair.c b/testdata/cxx/0_simple/pair.c similarity index 100% rename from testdata/cxxsimple/pair.c rename to testdata/cxx/0_simple/pair.c diff --git a/testdata/cxxsimple/pair.h b/testdata/cxx/0_simple/pair.h similarity index 100% rename from testdata/cxxsimple/pair.h rename to testdata/cxx/0_simple/pair.h diff --git a/testdata/cduplicate/CMakeLists.txt b/testdata/cxx/1_duplicate_fn/CMakeLists.txt similarity index 100% rename from testdata/cduplicate/CMakeLists.txt rename to testdata/cxx/1_duplicate_fn/CMakeLists.txt diff --git a/testdata/cduplicate/d1/CMakeLists.txt b/testdata/cxx/1_duplicate_fn/d1/CMakeLists.txt similarity index 100% rename from testdata/cduplicate/d1/CMakeLists.txt rename to testdata/cxx/1_duplicate_fn/d1/CMakeLists.txt diff --git a/testdata/cduplicate/d1/add.c b/testdata/cxx/1_duplicate_fn/d1/add.c similarity index 100% rename from testdata/cduplicate/d1/add.c rename to testdata/cxx/1_duplicate_fn/d1/add.c diff --git a/testdata/cduplicate/d2/CMakeLists.txt b/testdata/cxx/1_duplicate_fn/d2/CMakeLists.txt similarity index 100% rename from testdata/cduplicate/d2/CMakeLists.txt rename to testdata/cxx/1_duplicate_fn/d2/CMakeLists.txt diff --git a/testdata/cduplicate/d2/add.c b/testdata/cxx/1_duplicate_fn/d2/add.c similarity index 100% rename from testdata/cduplicate/d2/add.c rename to testdata/cxx/1_duplicate_fn/d2/add.c diff --git a/testdata/cduplicate/main.c b/testdata/cxx/1_duplicate_fn/main.c similarity index 100% rename from testdata/cduplicate/main.c rename to testdata/cxx/1_duplicate_fn/main.c diff --git a/testdata/golang/cmd/go.mod b/testdata/go/0_golang/cmd/go.mod similarity index 100% rename from testdata/golang/cmd/go.mod rename to testdata/go/0_golang/cmd/go.mod diff --git a/testdata/golang/cmd/go.sum b/testdata/go/0_golang/cmd/go.sum similarity index 100% rename from testdata/golang/cmd/go.sum rename to testdata/go/0_golang/cmd/go.sum diff --git a/testdata/golang/cmd/main.go b/testdata/go/0_golang/cmd/main.go similarity index 100% rename from testdata/golang/cmd/main.go rename to testdata/go/0_golang/cmd/main.go diff --git a/testdata/golang/cmd/serdes.go b/testdata/go/0_golang/cmd/serdes.go similarity index 100% rename from testdata/golang/cmd/serdes.go rename to testdata/go/0_golang/cmd/serdes.go diff --git a/testdata/golang/go.mod b/testdata/go/0_golang/go.mod similarity index 100% rename from testdata/golang/go.mod rename to testdata/go/0_golang/go.mod diff --git a/testdata/golang/go.sum b/testdata/go/0_golang/go.sum similarity index 100% rename from testdata/golang/go.sum rename to testdata/go/0_golang/go.sum diff --git a/testdata/golang/pkg/entity/entity.go b/testdata/go/0_golang/pkg/entity/entity.go similarity index 100% rename from testdata/golang/pkg/entity/entity.go rename to testdata/go/0_golang/pkg/entity/entity.go diff --git a/testdata/golang/pkg/refer.go b/testdata/go/0_golang/pkg/refer.go similarity index 100% rename from testdata/golang/pkg/refer.go rename to testdata/go/0_golang/pkg/refer.go diff --git a/testdata/golang/pkg/util.go b/testdata/go/0_golang/pkg/util.go similarity index 100% rename from testdata/golang/pkg/util.go rename to testdata/go/0_golang/pkg/util.go diff --git a/testdata/pythonsimple/test.py b/testdata/python/0_simple/test.py similarity index 100% rename from testdata/pythonsimple/test.py rename to testdata/python/0_simple/test.py diff --git a/testdata/pythonsimple/test2.py b/testdata/python/0_simple/test2.py similarity index 100% rename from testdata/pythonsimple/test2.py rename to testdata/python/0_simple/test2.py diff --git a/testdata/pythonsimple/test3.py b/testdata/python/0_simple/test3.py similarity index 100% rename from testdata/pythonsimple/test3.py rename to testdata/python/0_simple/test3.py diff --git a/testdata/pythonsingle/main.py b/testdata/python/1_single/main.py similarity index 100% rename from testdata/pythonsingle/main.py rename to testdata/python/1_single/main.py diff --git a/testdata/pysimpleobj/main.py b/testdata/python/2_class/main.py similarity index 100% rename from testdata/pysimpleobj/main.py rename to testdata/python/2_class/main.py diff --git a/testdata/pyfileimports/main.py b/testdata/python/3_complex_imports/main.py similarity index 100% rename from testdata/pyfileimports/main.py rename to testdata/python/3_complex_imports/main.py diff --git a/testdata/pyglobvar/main.py b/testdata/python/4_globvar/main.py similarity index 100% rename from testdata/pyglobvar/main.py rename to testdata/python/4_globvar/main.py diff --git a/testdata/python/5_modules/__init__.py b/testdata/python/5_modules/__init__.py new file mode 100644 index 00000000..3f66584e --- /dev/null +++ b/testdata/python/5_modules/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2025 CloudWeGo Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from top import * diff --git a/testdata/python/5_modules/a/__init__.py b/testdata/python/5_modules/a/__init__.py new file mode 100644 index 00000000..09e11456 --- /dev/null +++ b/testdata/python/5_modules/a/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2025 CloudWeGo Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .impl_fa import fa diff --git a/testdata/python/5_modules/a/impl_fa.py b/testdata/python/5_modules/a/impl_fa.py new file mode 100644 index 00000000..7c87d2e2 --- /dev/null +++ b/testdata/python/5_modules/a/impl_fa.py @@ -0,0 +1,17 @@ +# Copyright 2025 CloudWeGo Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def fa(): + return 3 diff --git a/testdata/python/5_modules/top.py b/testdata/python/5_modules/top.py new file mode 100644 index 00000000..ac719b12 --- /dev/null +++ b/testdata/python/5_modules/top.py @@ -0,0 +1,23 @@ +# Copyright 2025 CloudWeGo Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from a import fa +from b import fb + + +def main(): + return fa() + fb() + + +print(main()) diff --git a/testdata/pythonoperator/main.py b/testdata/python/6_operator/main.py similarity index 100% rename from testdata/pythonoperator/main.py rename to testdata/python/6_operator/main.py diff --git a/testdata/pyimexport/main.py b/testdata/python/7_reexport/main.py similarity index 100% rename from testdata/pyimexport/main.py rename to testdata/python/7_reexport/main.py diff --git a/testdata/rust2/Cargo.toml b/testdata/rust/0_rust2/Cargo.toml similarity index 100% rename from testdata/rust2/Cargo.toml rename to testdata/rust/0_rust2/Cargo.toml diff --git a/testdata/rust2/src/entity/func.rs b/testdata/rust/0_rust2/src/entity/func.rs similarity index 100% rename from testdata/rust2/src/entity/func.rs rename to testdata/rust/0_rust2/src/entity/func.rs diff --git a/testdata/rust2/src/entity/inter.rs b/testdata/rust/0_rust2/src/entity/inter.rs similarity index 100% rename from testdata/rust2/src/entity/inter.rs rename to testdata/rust/0_rust2/src/entity/inter.rs diff --git a/testdata/rust2/src/entity/mod.rs b/testdata/rust/0_rust2/src/entity/mod.rs similarity index 100% rename from testdata/rust2/src/entity/mod.rs rename to testdata/rust/0_rust2/src/entity/mod.rs diff --git a/testdata/rust2/src/main.rs b/testdata/rust/0_rust2/src/main.rs similarity index 100% rename from testdata/rust2/src/main.rs rename to testdata/rust/0_rust2/src/main.rs diff --git a/testdata/rustsimpleobj/Cargo.toml b/testdata/rust/1_simpleobj/Cargo.toml similarity index 100% rename from testdata/rustsimpleobj/Cargo.toml rename to testdata/rust/1_simpleobj/Cargo.toml diff --git a/testdata/rustsimpleobj/src/main.rs b/testdata/rust/1_simpleobj/src/main.rs similarity index 100% rename from testdata/rustsimpleobj/src/main.rs rename to testdata/rust/1_simpleobj/src/main.rs