Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use -toolexec to avoid manual preprocessing #81

Merged
merged 5 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: setup go
uses: actions/setup-go@v2
with:
go-version: 1.13
go-version: 1.22
- name: validation
run: make build test
- name: codecov
Expand Down
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ LDFLAGS += -X "github.com/pingcap/failpoint/failpoint-ctl/version.gitBranch=$(sh
LDFLAGS += -X "github.com/pingcap/failpoint/failpoint-ctl/version.goVersion=$(shell go version)"

FAILPOINT_CTL_BIN := bin/failpoint-ctl
FAILPOINT_TOOLEXEC_BIN := bin/failpoint-toolexec

path_to_add := $(addsuffix /bin,$(subst :,/bin:,$(GOPATH)))
export PATH := $(path_to_add):$(PATH):$(shell pwd)/tools/bin
Expand All @@ -31,12 +32,17 @@ default: build checksuccess

build:
$(GOBUILD) $(RACE_FLAG) -ldflags '$(LDFLAGS)' -o $(FAILPOINT_CTL_BIN) failpoint-ctl/main.go
$(GOBUILD) $(RACE_FLAG) -ldflags '$(LDFLAGS)' -o $(FAILPOINT_TOOLEXEC_BIN) failpoint-toolexec/main.go

checksuccess:
@if [ -f $(FAILPOINT_CTL_BIN) ]; \
then \
echo "failpoint-ctl build successfully :-) !" ; \
fi
@if [ -f $(FAILPOINT_TOOLEXEC_BIN) ]; \
then \
echo "failpoint-toolexec build successfully :-) !" ; \
fi

test: gotest check-static

Expand Down
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ An implementation of [failpoints][failpoint] for Golang. Fail points are used to

[failpoint]: http://www.freebsd.org/cgi/man.cgi?query=fail

## Quick Start
## Quick Start (use `failpoint-ctl`)

1. Build `failpoint-ctl` from source

Expand Down Expand Up @@ -51,6 +51,47 @@ An implementation of [failpoints][failpoint] for Golang. Fail points are used to
GO_FAILPOINTS="main/testPanic=return(true)" go run your-program.go binding__failpoint_binding__.go
```

## Quick Start (use `failpoint-toolexec`)

1. Build `failpoint-toolexec` from source

``` bash
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-toolexec
```

2. Inject failpoints to your program, eg:

``` go
package main

import "github.com/pingcap/failpoint"

func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
```

3. Use a separate build cache to avoid mixing caches without `failpoint-toolexec`, and build

`GOCACHE=/tmp/failpoint-cache go build -toolexec path/to/failpoint-toolexec`

4. Enable failpoints with `GO_FAILPOINTS` environment variable

``` bash
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
```

5. You can also use `go run` or `go test`, like:

```bash
GOCACHE=/tmp/failpoint-cache GO_FAILPOINTS="main/testPanic=return(true)" go run -toolexec path/to/failpoint-toolexec your-program.go
```

## Design principles

- Define failpoint in valid Golang code, not comments or anything else
Expand Down
4 changes: 2 additions & 2 deletions code/expr_rewriter.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func (r *Rewriter) rewriteInject(call *ast.CallExpr) (bool, ast.Stmt, error) {
}

fpnameExtendCall := &ast.CallExpr{
Fun: ast.NewIdent(extendPkgName),
Fun: ast.NewIdent(ExtendPkgName),
Args: []ast.Expr{fpname},
}

Expand Down Expand Up @@ -163,7 +163,7 @@ func (r *Rewriter) rewriteInjectContext(call *ast.CallExpr) (bool, ast.Stmt, err
}

fpnameExtendCall := &ast.CallExpr{
Fun: ast.NewIdent(extendPkgName),
Fun: ast.NewIdent(ExtendPkgName),
Args: []ast.Expr{fpname},
}

Expand Down
4 changes: 3 additions & 1 deletion code/restorer.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (

// Restorer represents a manager to restore currentFile tree which has been modified by
// `failpoint-ctl enable`, e.g:
/*
// ├── foo
// │ ├── foo.go
// │   └── foo.go__failpoint_stash__
Expand All @@ -48,6 +49,7 @@ const (
// │   └── bar.go <- bar.go__failpoint_stash__
// └── foobar
//    └── foobar.go <- foobar.go__failpoint_stash__
*/
type Restorer struct {
path string
}
Expand Down Expand Up @@ -154,6 +156,6 @@ func init() {
func %s(name string) string {
return __failpointBindingCache.pkgpath + "/" + name
}
`, pak, extendPkgName)
`, pak, ExtendPkgName)
return ioutil.WriteFile(bindingFile, []byte(bindingContent), 0644)
}
33 changes: 26 additions & 7 deletions code/rewriter.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
packageName = "failpoint"
evalFunction = "Eval"
evalCtxFunction = "EvalContext"
extendPkgName = "_curpkg_"
ExtendPkgName = "_curpkg_"
// It is an indicator to indicate the label is converted from `failpoint.Label("...")`
// We use an illegal suffix to avoid conflict with the user's code
// So `failpoint.Label("label1")` will be converted to `label1-tmp-marker:` in expression
Expand All @@ -44,12 +44,13 @@
// corresponding statements in Golang. It will traverse the specified path and filter
// out files which do not have failpoint injection sites, and rewrite the remain files.
type Rewriter struct {
rewriteDir string
currentPath string
currentFile *ast.File
currsetFset *token.FileSet
failpointName string
rewritten bool
rewriteDir string
currentPath string
currentFile *ast.File
currsetFset *token.FileSet
failpointName string
allowNotChecked bool
rewritten bool

output io.Writer
}
Expand All @@ -66,6 +67,21 @@
r.output = out
}

// SetAllowNotChecked sets whether the rewriter allows the file which does not import failpoint package.
func (r *Rewriter) SetAllowNotChecked(b bool) {
r.allowNotChecked = b

Check warning on line 72 in code/rewriter.go

View check run for this annotation

Codecov / codecov/patch

code/rewriter.go#L72

Added line #L72 was not covered by tests
}

// GetRewritten returns whether the rewriter has rewritten the file in a RewriteFile call.
func (r *Rewriter) GetRewritten() bool {
return r.rewritten

Check warning on line 77 in code/rewriter.go

View check run for this annotation

Codecov / codecov/patch

code/rewriter.go#L77

Added line #L77 was not covered by tests
}

// GetCurrentFile returns the current file which is being rewritten
func (r *Rewriter) GetCurrentFile() *ast.File {
return r.currentFile

Check warning on line 82 in code/rewriter.go

View check run for this annotation

Codecov / codecov/patch

code/rewriter.go#L82

Added line #L82 was not covered by tests
}

func (r *Rewriter) pos(pos token.Pos) string {
p := r.currsetFset.Position(pos)
return fmt.Sprintf("%s:%d", p.Filename, p.Line)
Expand Down Expand Up @@ -603,6 +619,9 @@
}
}
if failpointImport == nil {
if r.allowNotChecked {
return nil

Check warning on line 623 in code/rewriter.go

View check run for this annotation

Codecov / codecov/patch

code/rewriter.go#L622-L623

Added lines #L622 - L623 were not covered by tests
}
panic("import path should be check before rewrite")
}
if failpointImport.Name != nil {
Expand Down
190 changes: 190 additions & 0 deletions failpoint-toolexec/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Copyright 2024 PingCAP, 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
//
// http://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 main

import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"

"github.com/pingcap/errors"
"github.com/pingcap/failpoint/code"
"golang.org/x/mod/modfile"
)

var logger = log.New(os.Stderr, "[failpoint-toolexec]", log.LstdFlags)

func main() {
if len(os.Args) < 2 {
return

Check warning on line 35 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L34-L35

Added lines #L34 - L35 were not covered by tests
}
goCmd, buildArgs := os.Args[1], os.Args[2:]
goCmdBase := filepath.Base(goCmd)
if runtime.GOOS == "windows" {
goCmdBase = strings.TrimSuffix(goCmd, ".exe")

Check warning on line 40 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L37-L40

Added lines #L37 - L40 were not covered by tests
}

if strings.ToLower(goCmdBase) == "compile" {
if err := injectFailpoint(&buildArgs); err != nil {
logger.Println("failed to inject failpoint", err)

Check warning on line 45 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L43-L45

Added lines #L43 - L45 were not covered by tests
}
}

cmd := exec.Command(goCmd, buildArgs...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

Check warning on line 51 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L49-L51

Added lines #L49 - L51 were not covered by tests

if err := cmd.Run(); err != nil {
logger.Println("failed to run command", err)

Check warning on line 54 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L53-L54

Added lines #L53 - L54 were not covered by tests
}
}

func injectFailpoint(argsP *[]string) error {
callersModule, err := findCallersModule()
if err != nil {
return err

Check warning on line 61 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L59-L61

Added lines #L59 - L61 were not covered by tests
}

// ref https://pkg.go.dev/cmd/compile#hdr-Command_Line
var module string
args := *argsP

Check warning on line 66 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L65-L66

Added lines #L65 - L66 were not covered by tests
for i, arg := range args {
if arg == "-p" {
if i+1 < len(args) {
module = args[i+1]

Check warning on line 70 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L68-L70

Added lines #L68 - L70 were not covered by tests
}
break

Check warning on line 72 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L72

Added line #L72 was not covered by tests
}
}
if !strings.HasPrefix(module, callersModule) && module != "main" {
return nil

Check warning on line 76 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L75-L76

Added lines #L75 - L76 were not covered by tests
}

fileIndices := make([]int, 0, len(args))

Check warning on line 79 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L79

Added line #L79 was not covered by tests
for i, arg := range args {
// find the golang source files of the caller's package
if strings.HasSuffix(arg, ".go") && !inSDKOrMod(arg) {
fileIndices = append(fileIndices, i)

Check warning on line 83 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L82-L83

Added lines #L82 - L83 were not covered by tests
}
}

needExtraFile := false
writer := &code.Rewriter{}
writer.SetAllowNotChecked(true)

Check warning on line 89 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L87-L89

Added lines #L87 - L89 were not covered by tests
for _, idx := range fileIndices {
needExtraFile = needExtraFile || injectFailpointForFile(writer, &args[idx], module)

Check warning on line 91 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L91

Added line #L91 was not covered by tests
}
if needExtraFile {
newFile := filepath.Join(tmpFolder, module, "failpoint_toolexec_extra.go")
if err := writeExtraFile(newFile, writer.GetCurrentFile().Name.Name, module); err != nil {
return err

Check warning on line 96 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L93-L96

Added lines #L93 - L96 were not covered by tests
}
*argsP = append(args, newFile)

Check warning on line 98 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L98

Added line #L98 was not covered by tests
}
return nil

Check warning on line 100 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L100

Added line #L100 was not covered by tests
}

// ref https://github.com/golang/go/blob/bdd27c4debfb51fe42df0c0532c1c747777b7a32/src/cmd/go/internal/modload/init.go#L1511
func findCallersModule() (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", err

Check warning on line 107 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L105-L107

Added lines #L105 - L107 were not covered by tests
}
dir := filepath.Clean(cwd)

Check warning on line 109 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L109

Added line #L109 was not covered by tests

// Look for enclosing go.mod.
for {
goModPath := filepath.Join(dir, "go.mod")
if fi, err := os.Stat(goModPath); err == nil && !fi.IsDir() {
data, err := os.ReadFile(goModPath)
if err != nil {
return "", err

Check warning on line 117 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L113-L117

Added lines #L113 - L117 were not covered by tests
}
f, err := modfile.ParseLax(goModPath, data, nil)
if err != nil {
return "", err

Check warning on line 121 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L119-L121

Added lines #L119 - L121 were not covered by tests
}
return f.Module.Mod.Path, err

Check warning on line 123 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L123

Added line #L123 was not covered by tests
}
d := filepath.Dir(dir)
if d == dir {
break

Check warning on line 127 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L125-L127

Added lines #L125 - L127 were not covered by tests
}
dir = d

Check warning on line 129 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L129

Added line #L129 was not covered by tests
}
return "", errors.New("go.mod file not found")

Check warning on line 131 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L131

Added line #L131 was not covered by tests
}

var goModCache = os.Getenv("GOMODCACHE")
var goRoot = runtime.GOROOT()

func inSDKOrMod(path string) bool {
absPath, err := filepath.Abs(path)
if err != nil {
logger.Println("failed to get absolute path", err)
return false

Check warning on line 141 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L138-L141

Added lines #L138 - L141 were not covered by tests
}

if goModCache != "" && strings.HasPrefix(absPath, goModCache) {
return true

Check warning on line 145 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L144-L145

Added lines #L144 - L145 were not covered by tests
}
if strings.HasPrefix(absPath, goRoot) {
return true

Check warning on line 148 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L147-L148

Added lines #L147 - L148 were not covered by tests
}
return false

Check warning on line 150 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L150

Added line #L150 was not covered by tests
}

var tmpFolder = filepath.Join(os.TempDir(), "failpoint-toolexec")

func injectFailpointForFile(w *code.Rewriter, file *string, module string) bool {
newFile := filepath.Join(tmpFolder, module, filepath.Base(*file))
newFileDir := filepath.Dir(newFile)
if err := os.MkdirAll(newFileDir, 0700); err != nil {
logger.Println("failed to create temp folder", err)
return false

Check warning on line 160 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L156-L160

Added lines #L156 - L160 were not covered by tests
}
f, err := os.OpenFile(newFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
logger.Println("failed to open temp file", err)
return false

Check warning on line 165 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L162-L165

Added lines #L162 - L165 were not covered by tests
}
defer f.Close()
w.SetOutput(f)

Check warning on line 168 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L167-L168

Added lines #L167 - L168 were not covered by tests

if err := w.RewriteFile(*file); err != nil {
logger.Println("failed to rewrite file", err)
return false

Check warning on line 172 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L170-L172

Added lines #L170 - L172 were not covered by tests
}
if !w.GetRewritten() {
return false

Check warning on line 175 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L174-L175

Added lines #L174 - L175 were not covered by tests
}
*file = newFile
return true

Check warning on line 178 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L177-L178

Added lines #L177 - L178 were not covered by tests
}

func writeExtraFile(filePath, packageName, module string) error {
bindingContent := fmt.Sprintf(`
package %s

Check warning on line 183 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L182-L183

Added lines #L182 - L183 were not covered by tests

func %s(name string) string {
return "%s/" + name

Check warning on line 186 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L186

Added line #L186 was not covered by tests
}
`, packageName, code.ExtendPkgName, module)
return os.WriteFile(filePath, []byte(bindingContent), 0644)

Check warning on line 189 in failpoint-toolexec/main.go

View check run for this annotation

Codecov / codecov/patch

failpoint-toolexec/main.go#L188-L189

Added lines #L188 - L189 were not covered by tests
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/sergi/go-diff v1.1.0
github.com/stretchr/testify v1.7.0
go.uber.org/goleak v1.1.10
golang.org/x/mod v0.17.0
)

go 1.13
Loading
Loading