diff --git a/cmd/build.go b/cmd/build.go index 98a4c0b..1681d25 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -2,6 +2,7 @@ package cmd import ( "github.com/qiniu/goc/v2/pkg/build" + "github.com/qiniu/goc/v2/pkg/config" "github.com/spf13/cobra" ) @@ -13,6 +14,7 @@ var buildCmd = &cobra.Command{ } func init() { + buildCmd.Flags().StringVarP(&config.GocConfig.Mode, "mode", "", "count", "coverage mode: set, count, atomic") rootCmd.AddCommand(buildCmd) } diff --git a/cmd/server.go b/cmd/server.go index c675795..c43da80 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -2,7 +2,6 @@ package cmd import ( "github.com/qiniu/goc/v2/pkg/build" - "github.com/qiniu/goc/v2/pkg/config" "github.com/spf13/cobra" ) @@ -16,8 +15,8 @@ var serverCmd = &cobra.Command{ } func init() { - serverCmd.Flags().IntVarP(&config.GocConfig.Port, "port", "", 7777, "listen port to start a coverage host center") - serverCmd.Flags().StringVarP(&config.GocConfig.StorePath, "storepath", "", "goc.store", "the file to save all goc server information") + // serverCmd.Flags().IntVarP(&config.GocConfig.Port, "port", "", 7777, "listen port to start a coverage host center") + // serverCmd.Flags().StringVarP(&config.GocConfig.StorePath, "storepath", "", "goc.store", "the file to save all goc server information") rootCmd.AddCommand(serverCmd) } diff --git a/pkg/build/goenv.go b/pkg/build/goenv.go index b1278a5..42d0e76 100644 --- a/pkg/build/goenv.go +++ b/pkg/build/goenv.go @@ -30,6 +30,7 @@ func (b *Build) readProjectMetaInfo() { } // 工程根目录 config.GocConfig.CurModProjectDir = pkg.Root + config.GocConfig.ImportPath = pkg.Module.Path break } @@ -38,6 +39,8 @@ func (b *Build) readProjectMetaInfo() { config.GocConfig.TmpModProjectDir = filepath.Join(os.TempDir(), tmpFolderName(config.GocConfig.CurModProjectDir)) // get cur pkg dir in the corresponding tmp dir config.GocConfig.TmpPkgDir = filepath.Join(config.GocConfig.TmpModProjectDir, config.GocConfig.CurPkgDir[len(config.GocConfig.CurModProjectDir):]) + // get GlobalCoverVarImportPath + config.GocConfig.GlobalCoverVarImportPath = tmpFolderName(config.GocConfig.CurModProjectDir) log.Donef("project meta information parsed") } diff --git a/pkg/config/config.go b/pkg/config/config.go index 325dfb6..3793506 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -3,19 +3,21 @@ package config import "time" type gocConfig struct { - Debug bool - CurPkgDir string - CurModProjectDir string - TmpModProjectDir string - TmpPkgDir string - BinaryName string - Pkgs map[string]*Package - GOPATH string - GOBIN string - IsMod bool // deprecated + Debug bool + ImportPath string // import path of the project + CurPkgDir string + CurModProjectDir string + TmpModProjectDir string + TmpPkgDir string + BinaryName string + Pkgs map[string]*Package + GOPATH string + GOBIN string + IsMod bool // deprecated + GlobalCoverVarImportPath string - Port int // used both by server & client - StorePath string // persist store location + Host string + Mode string // cover mode } // GocConfig 全局变量,存放 goc 的各种元属性 diff --git a/pkg/cover/cover.go b/pkg/cover/cover.go index a29b24f..1c4d149 100644 --- a/pkg/cover/cover.go +++ b/pkg/cover/cover.go @@ -1,87 +1,45 @@ package cover -import "time" +import ( + "crypto/sha256" + "fmt" + "path" -// PackageCover holds all the generate coverage variables of a package -type PackageCover struct { - Package *Package - Vars map[string]*FileVar -} - -// FileVar holds the name of the generated coverage variables targeting the named file. -type FileVar struct { - File string - Var string -} - -// Package map a package output by go list -// this is subset of package struct in: https://github.com/golang/go/blob/master/src/cmd/go/internal/load/pkg.go#L58 -type Package struct { - Dir string `json:"Dir"` // directory containing package sources - ImportPath string `json:"ImportPath"` // import path of package in dir - Name string `json:"Name"` // package name - Target string `json:",omitempty"` // installed target for this package (may be executable) - Root string `json:",omitempty"` // Go root, Go path dir, or module root dir containing this package - - Module *ModulePublic `json:",omitempty"` // info about package's module, if any - Goroot bool `json:"Goroot,omitempty"` // is this package in the Go root? - Standard bool `json:"Standard,omitempty"` // is this package part of the standard Go library? - DepOnly bool `json:"DepOnly,omitempty"` // package is only a dependency, not explicitly listed - - // Source files - GoFiles []string `json:"GoFiles,omitempty"` // .go source files (excluding CgoFiles, TestGoFiles, XTestGoFiles) - CgoFiles []string `json:"CgoFiles,omitempty"` // .go source files that import "C" - - // Dependency information - Deps []string `json:"Deps,omitempty"` // all (recursively) imported dependencies - Imports []string `json:",omitempty"` // import paths used by this package - ImportMap map[string]string `json:",omitempty"` // map from source import to ImportPath (identity entries omitted) - - // Error information - Incomplete bool `json:"Incomplete,omitempty"` // this package or a dependency has an error - Error *PackageError `json:"Error,omitempty"` // error loading package - DepsErrors []*PackageError `json:"DepsErrors,omitempty"` // errors loading dependencies -} - -// ModulePublic represents the package info of a module -type ModulePublic struct { - Path string `json:",omitempty"` // module path - Version string `json:",omitempty"` // module version - Versions []string `json:",omitempty"` // available module versions - Replace *ModulePublic `json:",omitempty"` // replaced by this module - Time *time.Time `json:",omitempty"` // time version was created - Update *ModulePublic `json:",omitempty"` // available update (with -u) - Main bool `json:",omitempty"` // is this the main module? - Indirect bool `json:",omitempty"` // module is only indirectly needed by main module - Dir string `json:",omitempty"` // directory holding local copy of files, if any - GoMod string `json:",omitempty"` // path to go.mod file describing module, if any - GoVersion string `json:",omitempty"` // go version used in module - Error *ModuleError `json:",omitempty"` // error loading module -} - -// ModuleError represents the error loading module -type ModuleError struct { - Err string // error text -} - -// PackageError is the error info for a package when list failed -type PackageError struct { - ImportStack []string // shortest path from package named on command line to this one - Pos string // position of error (if present, file:line:col) - Err string // the error itself -} - -// CoverBuildInfo retreives some info from build -type CoverInfo struct { - Target string - GoPath string - IsMod bool - ModRootPath string - GlobalCoverVarImportPath string // path for the injected global cover var file - OneMainPackage bool - Args string - Mode string - AgentPort string - Center string - Singleton bool + "github.com/qiniu/goc/v2/pkg/config" +) + +// declareCoverVars attaches the required cover variables names +// to the files, to be used when annotating the files. +func declareCoverVars(p *config.Package) map[string]*config.FileVar { + coverVars := make(map[string]*config.FileVar) + coverIndex := 0 + // We create the cover counters as new top-level variables in the package. + // We need to avoid collisions with user variables (GoCover_0 is unlikely but still) + // and more importantly with dot imports of other covered packages, + // so we append 12 hex digits from the SHA-256 of the import path. + // The point is only to avoid accidents, not to defeat users determined to + // break things. + sum := sha256.Sum256([]byte(p.ImportPath)) + h := fmt.Sprintf("%x", sum[:6]) + for _, file := range p.GoFiles { + // These names appear in the cmd/cover HTML interface. + var longFile = path.Join(p.ImportPath, file) + coverVars[file] = &config.FileVar{ + File: longFile, + Var: fmt.Sprintf("GoCover_%d_%x", coverIndex, h), + } + coverIndex++ + } + + for _, file := range p.CgoFiles { + // These names appear in the cmd/cover HTML interface. + var longFile = path.Join(p.ImportPath, file) + coverVars[file] = &config.FileVar{ + File: longFile, + Var: fmt.Sprintf("GoCover_%d_%x", coverIndex, h), + } + coverIndex++ + } + + return coverVars } diff --git a/pkg/cover/inject.go b/pkg/cover/inject.go index e246672..793420b 100644 --- a/pkg/cover/inject.go +++ b/pkg/cover/inject.go @@ -1,7 +1,11 @@ package cover import ( + "os" + "path/filepath" + "github.com/qiniu/goc/v2/pkg/config" + "github.com/qiniu/goc/v2/pkg/cover/internal/tool" "github.com/qiniu/goc/v2/pkg/log" ) @@ -9,20 +13,153 @@ import ( func Inject() { log.StartWait("injecting cover variables") - // var seen := make(map[string]*PackageCover) + var seen = make(map[string]*config.PackageCover) - for _, pkg := range config.GocConfig.Pkgs { + // 所有插桩变量定义声明 + allDecl := "" + + pkgs := config.GocConfig.Pkgs + for _, pkg := range pkgs { if pkg.Name == "main" { - log.Infof("handle package: %v", pkg.ImportPath) + log.Infof("handle main package: %v", pkg.ImportPath) + // 该 main 二进制所关联的所有插桩变量的元信息 + // 每个 main 之间是不相关的,需要重新定义 + allMainCovers := make([]*config.PackageCover, 0) + // 注入 main package + mainCover, mainDecl := addCounters(pkg) + // 收集插桩变量的定义和元信息 + allDecl += mainDecl + allMainCovers = append(allMainCovers, mainCover) + + // 向 main package 的依赖注入插桩变量 + for _, dep := range pkg.Deps { + if _, ok := seen[dep]; ok { + continue + } + + // 依赖需要忽略 Go 标准库和 go.mod 引入的第三方 + if depPkg, ok := pkgs[dep]; ok { + // 注入依赖的 package + packageCover, depDecl := addCounters(depPkg) + // 收集插桩变量的定义和元信息 + allDecl += depDecl + allMainCovers = append(allMainCovers, packageCover) + // 避免重复访问 + seen[dep] = packageCover + } + } + // 为每个 main 包注入 websocket handler + injectCoverHandler(getPkgTmpDir(pkg.Dir), allMainCovers) } } + // 在工程根目录注入所有插桩变量的声明+定义 + injectGlobalCoverVarFile(allDecl) + log.StopWait() log.Donef("cover variables injected") } -// declareCoverVars attaches the required cover variables names -// to the files, to be used when annotating the files. -func declareCoverVars(p *Package) map[string]*FileVar { +// addCounters is different from official go tool cover +// +// 1. only inject covervar++ into source file +// +// 2. no declarartions for these covervars +// +// 3. return the declarations as string +func addCounters(pkg *config.Package) (*config.PackageCover, string) { + mode := config.GocConfig.Mode + gobalCoverVarImportPath := config.GocConfig.GlobalCoverVarImportPath + + coverVarMap := declareCoverVars(pkg) + + decl := "" + for file, coverVar := range coverVarMap { + decl += "\n" + tool.Annotate(filepath.Join(getPkgTmpDir(pkg.Dir), file), mode, coverVar.Var, gobalCoverVarImportPath) + "\n" + } + + return &config.PackageCover{ + Package: pkg, + Vars: coverVarMap, + }, decl +} + +// getPkgTmpDir gets corresponding pkg dir in temporary project +// +// the reason is that config.GocConfig.Pkgs is get in the original project. +// we need to transfer the direcory. +// +// 在原工程目录已经做了一次 go list -json,在临时目录没有必要再做一遍,直接转换一下就能得到 +// 临时目录中的 pkg.Dir。 +func getPkgTmpDir(pkgDir string) string { + relDir, err := filepath.Rel(config.GocConfig.CurModProjectDir, pkgDir) + if err != nil { + log.Fatalf("go json -list meta info wrong: %v", err) + } + + return filepath.Join(config.GocConfig.TmpModProjectDir, relDir) +} + +// injectCoverHandler inject handlers like following +// +// - xxx.go +// - yyy_package +// - main.go +// - goc_http_cover_apis_auto_generated_11111_22222_bridge.go +// - goc_http_cover_apis_auto_generated_11111_22222_package +// | +// -- init.go +// +// 11111_22222_bridge.go just import 11111_22222_package, where package contains ws handlers. +// 使用 bridge.go 文件是为了避免插桩逻辑中的变量名污染 main 包 +func injectCoverHandler(where string, covers []*config.PackageCover) { + injectPkgName := "goc_http_cover_apis_auto_generated_11111_22222_package" + wherePkg := filepath.Join(where, injectPkgName) + err := os.MkdirAll(wherePkg, os.ModePerm) + if err != nil { + log.Fatalf("fail to generate %v directory: %v", injectPkgName, err) + } + + // create bridge file + whereBridge := filepath.Join(where, "goc_http_cover_apis_auto_generated_11111_22222_bridge.go") + f, err := os.Create(whereBridge) + if err != nil { + log.Fatalf("fail to create cover bridge file in temporary project: %v", err) + } + + tmplBridgeData := struct { + CoverImportPath string + }{ + // covers[0] is the main package + CoverImportPath: covers[0].Package.ImportPath + "/" + injectPkgName, + } + + if err := coverBridgeTmpl.Execute(f, tmplBridgeData); err != nil { + log.Fatalf("fail to generate cover bridge in temporary project: %v", err) + } + + // create ws handler files + dest := filepath.Join(wherePkg, "init.go") + + f, err = os.Create(dest) + if err != nil { + log.Fatalf("fail to create cover handlers in temporary project: %v", err) + } + + tmplData := struct { + Covers []*config.PackageCover + GlobalCoverVarImportPath string + Package string + }{ + Covers: covers, + GlobalCoverVarImportPath: config.GocConfig.GlobalCoverVarImportPath, + Package: injectPkgName, + } + + if err := coverMainTmpl.Execute(f, tmplData); err != nil { + log.Fatalf("fail to generate cover handlers in temporary project: %v", err) + } +} + +func injectGlobalCoverVarFile(decl string) { - return nil } diff --git a/pkg/cover/template.go b/pkg/cover/template.go new file mode 100644 index 0000000..11be2d5 --- /dev/null +++ b/pkg/cover/template.go @@ -0,0 +1,31 @@ +package cover + +import "html/template" + +var coverBridgeTmpl = template.Must(template.New("coverBridge").Parse(coverBridge)) + +const coverBridge = ` +// Code generated by goc system. DO NOT EDIT. + +package main + +import _ {{.CoverImportPath | printf "%q"}} +` + +var coverMainTmpl = template.Must(template.New("coverMain").Parse(coverMain)) + +const coverMain = ` +// Code generated by goc system. DO NOT EDIT. + +package {{.Package | printf ".%q"}} + +import ( + "log" + + _cover {{.GlobalCoverVarImportPath | printf "%q"}} +) + +func init() { + log.Println("hhhh") +} +`