前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >golang源码分析:goc集成测试覆盖率实现原理(2)

golang源码分析:goc集成测试覆盖率实现原理(2)

作者头像
golangLeetcode
发布2023-03-01 16:20:56
1K0
发布2023-03-01 16:20:56
举报
文章被收录于专栏:golang算法架构leetcode技术php

下面我们进入源代码来分析goc的具体实现,它的入口在goc.go文件里,是用来cobra的命令解析方式。

代码语言:javascript
复制
    cmd.Execute()

然后会启动一个http代理服务cmd/server.go

代码语言:javascript
复制
server, err := cover.NewFileBasedServer(localPersistence)
server.Run(port)

然后启用一个多路writer,分别写入文件和标准输出:

代码语言:javascript
复制
func (s *server) Run(port string) {
        f, err := os.Create(LogFile)
        mw := io.MultiWriter(f, os.Stdout)
        r := s.Route(mw)
        log.Fatal(r.Run(port))

然后基于gin框架,启动一个http服务

代码语言:javascript
复制
  func (s *server) Route(w io.Writer) *gin.Engine {
      r := gin.Default()
      r.StaticFile("static", "./"+s.PersistenceFile)
      v1 := r.Group("/v1")
      {
        v1.POST("/cover/register", s.registerService)
        v1.GET("/cover/profile", s.profile)
        v1.POST("/cover/profile", s.profile)
        v1.POST("/cover/clear", s.clear)
        v1.POST("/cover/init", s.initSystem)
        v1.GET("/cover/list", s.listServices)
        v1.POST("/cover/remove", s.removeServices)
      }

以服务注册为例,它类似于服务发现,用来把带桩的server注册到到goc服务上

代码语言:javascript
复制
func (s *server) registerService(c *gin.Context) {
        var service ServiceUnderTest
  if err := c.ShouldBind(&service); err != nil {
      u, err := url.Parse(service.Address)
      service.Address = fmt.Sprintf("%s://%s", u.Scheme, host)
        address := s.Store.Get(service.Name)
  if !contains(address, service.Address) {
    if err := s.Store.Add(service);

然后就可以看到进行性能分析的具体实现

代码语言:javascript
复制
func (s *server) profile(c *gin.Context) {
      if err := c.ShouldBind(&body); err != nil {
      allInfos := s.Store.GetAll()
      filterAddrInfoList, err := filterAddrInfo(body.Service, body.Address, body.Force, allInfos)
      for _, addrInfo := range filterAddrInfoList {
        pp, err := NewWorker(addrInfo.Address).Profile(ProfileParam{})
        profile, err := convertProfile(pp)
        mergedProfiles = append(mergedProfiles, profile)
      merged, err := cov.MergeMultipleProfiles(mergedProfiles)
      if err := cov.DumpProfile(merged, c.Writer); err != nil {

先拿到全量地址,然后过滤出我们需要的的地址,然后向对应地址发送请求,获取该服务的覆盖率信息。本质上是一个代理,解耦了被检测的服务和goc server,发起代理请求的代码实现位于:pkg/cover/client.go

代码语言:javascript
复制
func NewWorker(host string) Action {
      _, err := url.ParseRequestURI(host)
      client: http.DefaultClient,
代码语言:javascript
复制
func (c *client) Profile(param ProfileParam) ([]byte, error) {
      u := fmt.Sprintf("%s%s", c.Host, CoverProfileAPI)
      res, profile, err := c.do("POST", u, "application/json", bytes.NewReader(body))

分析完提供覆盖率服务的过程,我们分析下编译出带桩二进制代码的过程cmd/build.go

代码语言:javascript
复制
    wd, err := os.Getwd()
    runBuild(args, wd)

先构建出编译需要的环境,然后进行打桩代码的插入,最后进行编译:

代码语言:javascript
复制
func runBuild(args []string, wd string) {
      gocBuild, err := build.NewBuild(buildFlags, args, wd, buildOutput)
      ci := &cover.CoverInfo{
      err = cover.Execute(ci)
      err = gocBuild.Build()

pkg/build/build.go

代码语言:javascript
复制
func NewBuild(buildflags string, args []string, workingDir string, outputDir string) (*Build, error) {
      b := &Build{
      if false == b.validatePackageForBuild() {
      b.MvProjectsToTmp()
      dir, err := b.determineOutputDir(outputDir)

编译的过程并不会在原来的目录,而是会在一个临时目录里进行,否则和以前的源码是会有冲突的。

代码语言:javascript
复制
b.Pkgs, err = cover.ListPackages(b.WorkingDir, strings.Join(listArgs, " "), "")
err = b.mvProjectsToTmp()
b.OriGOPATH = os.Getenv("GOPATH")
b.NewGOPATH = fmt.Sprintf("%v:%v", b.TmpDir, b.OriGOPATH)

首先是获取项目里所有的包名,其实是调用了go list命令

代码语言:javascript
复制
cmd := exec.Command("/bin/bash", "-c", "go list "+args)

然后进行路径的转换操作

代码语言:javascript
复制
b.TmpDir = filepath.Join(os.TempDir(), tmpFolderName(b.WorkingDir))
os.RemoveAll(b.TmpDir)
b.GlobalCoverVarImportPath = filepath.Join("src", tmpPackageName(b.WorkingDir))
err := os.MkdirAll(filepath.Join(b.TmpDir, b.GlobalCoverVarImportPath), os.ModePerm)
b.IsMod, b.Root, err = b.traversePkgsList()
b.TmpWorkingDir, err = b.getTmpwd()
b.cpGoModulesProject()
updated, newGoModContent, err := b.updateGoModFile()   

最后更新go mod文件

代码语言:javascript
复制
tempModfile := filepath.Join(b.TmpDir, "go.mod")
buf, err := ioutil.ReadFile(tempModfile)

紧接着就会进行编译操作,就是调用go build

代码语言:javascript
复制
func (b *Build) Build() error {
      cmd := exec.Command("/bin/bash", "-c", "go build "+b.BuildFlags+" "+b.Packages)

比较核心的是pkg/cover/cover.go的Execute方法

代码语言:javascript
复制
func Execute(coverInfo *CoverInfo) error {
      globalCoverVarImportPath = filepath.Join(coverInfo.ModRootPath, globalCoverVarImportPath)
      pkgs, err := ListPackages(target, strings.Join(listArgs, " "), newGopath)
        for _, pkg := range pkgs {
    if pkg.Name == "main" {
        mainCover, mainDecl := AddCounters(pkg, mode, globalCoverVarImportPath)
    for _, dep := range pkg.Deps {
        packageCover, depDecl := AddCounters(depPkg, mode, globalCoverVarImportPath)
      var httpCoverApis = fmt.Sprintf("%s/http_cover_apis_auto_generated.go", pkg.Dir)
      if err := InjectCountersHandlers(tc, httpCoverApis); err != nil {
      return injectGlobalCoverVarFile(coverInfo, allDecl)

获取所有包名

代码语言:javascript
复制
 cmd := exec.Command("/bin/bash", "-c", "go list "+args)

对main包,和它依赖的包分别加上counter

代码语言:javascript
复制
func AddCounters(pkg *Package, mode string, globalCoverVarImportPath string) (*PackageCover, string) {
   coverVarMap := declareCoverVars(pkg)
   Var:  fmt.Sprintf("GoCover_%d_%x", coverIndex, h),
    for file, coverVar := range coverVarMap {
       decl += "\n" + tool.Annotate(path.Join(pkg.Dir, file), mode, coverVar.Var, globalCoverVarImportPath) + "\n"
              switch mode {
                case "set":
                  counterStmt = setCounterStmt
                case "count":
                  counterStmt = incCounterStmt
                case "atomic":
                  counterStmt = atomicCounterStmt
                default:
                  counterStmt = incCounterStmt
                }
  parsedFile, err := parser.ParseFile(fset, name, content, parser.ParseComments)
  ast.Walk(file, file.astFile)

核心原理就是解析文件源码得到抽象语法书,然后遍历语法树,加上对应的打桩代码。最后一步是加下import文件

代码语言:javascript
复制
packageName := "package " + filepath.Base(ci.GlobalCoverVarImportPath) + "\n\n"
packageName := "package " + filepath.Base(ci.GlobalCoverVarImportPath) + "\n\n"

为了和官方实现做对比,有相关的单测:

代码语言:javascript
复制
func buildCoverCmd(file string, coverVar *FileVar, pkg *Package, mode, newgopath string) *exec.Cmd {
      go tool cover -mode=atomic -o dest src (note: dest==src)
      cmd := exec.Command("go", newArgs...)

pkg/cover/cover_test.go

代码语言:javascript
复制
func TestBuildCoverCmd(t *testing.T) {
     cmd := buildCoverCmd(testCase.file, testCase.coverVar, testCase.pkg, testCase.mode, testCase.newgopath)
      if !reflect.DeepEqual(cmd, testCase.expectCmd) {

代码插桩的逻辑,核心实现是访问者模式pkg/cover/internal/tool/cover.go

代码语言:javascript
复制
func (f *File) Visit(node ast.Node) ast.Visitor {
        switch n := node.(type) {
  case *ast.BlockStmt:
        case *ast.CaseClause: // switch
          for _, n := range n.List {
          clause := n.(*ast.CaseClause)
          f.addCounters(clause.Colon+1, clause.Colon+1, clause.End(), clause.Body, false)
      case *ast.CommClause: // select
      f.addCounters(n.Lbrace, n.Lbrace+1, n.Rbrace+1, n.List, true)
      case *ast.IfStmt:
        ast.Walk(f, n.Init)
        ast.Walk(f, n.Cond)
        ast.Walk(f, n.Body)

对于每一个逻辑分支,添加打桩代码

代码语言:javascript
复制
func (f *File) addCounters(pos, insertPos, blockEnd token.Pos, list []ast.Stmt, extendToClosingBrace bool) {
代码语言:javascript
复制
func Annotate(name string, mode string, varVar string, globalCoverVarImportPath string) string {

对于查询覆盖率,逻辑是一样的cmd/cover.go

代码语言:javascript
复制
    runCover(target)
      _ = cover.Execute(ci)

源码实现的同时也实现了对应的的vscode插件,首先可以看下它的配置

tools/vscode-ext/package.json

代码语言:javascript
复制
"configuration": {
      "title": "Goc",
      "properties": {
        "goc.serverUrl": {
          "type": "string",
          "default": "http://127.0.0.1:7777",
          "description": "Specify the goc server url."
        },
        "goc.debug": {
          "type": "boolean",
          "default": false,
          "description": "Turn on debug mode to log more details."
        }
      }
    }

对应入口文件是:tools/vscode-ext/src/extension.ts,会检查环境然后执行覆盖率收集和展示:

代码语言:javascript
复制
let err = gocserver.checkGoEnv()
let packages = gocserver.getGoList();
await gocserver.startQueryLoop(packages);

核心逻辑位于tools/vscode-ext/src/gocserver.ts,首先是编辑器上覆盖率展示的代码:

代码语言:javascript
复制
private highlightDecorationType = vscode.window.createTextEditorDecorationType({
        backgroundColor: 'green',
        border:  '2px solid white',
        color:  'white'
    });;
代码语言:javascript
复制
async startQueryLoop(packages: any[]) {
   this.getConfigurations();
    this.setDebugLogger();
    let profile = await this.getLatestProfile();
    this.renderFile(packages, profile);
代码语言:javascript
复制
clearHightlight() {
    vscode.window.visibleTextEditors.forEach(visibleEditor => {
     visibleEditor.setDecorations(this.highlightDecorationType, []);
  });

获取最新覆盖率,其实是发起了一个http请求去查询最新的覆盖率信息:

代码语言:javascript
复制
 async getLatestProfile(): Promise<string> {
        let profileApi = `${this._serverUrl}/v1/cover/profile?force=true`;
      let res = await axios.get(profileApi, );
      let body: string = res.data.toString();
      this._logger.debug(body);
代码语言:javascript
复制
checkGoEnv() : Boolean {
  let output = spawnSync('go', ['version']);

根据覆盖率信息,更新代码的展示样式:

代码语言:javascript
复制
getGoList(): Array<any> {
       let output = spawnSync('go', ['list', '-json', './...'], opts);
        let packages = JSON.parse('[' + output.stdout.toString().replace(/}\n{/g, '},\n{') + ']');
    renderFile(packages: Array<any>, profile: string) {
        for (let i=0; i<packages.length; i++) {
              this.triggerUpdateDecoration(ranges);
        triggerUpdateDecoration(ranges: vscode.Range[]) {
                  vscode.window.activeTextEditor.setDecorations(
                this.highlightDecorationType,
                ranges
)
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-02-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 golang算法架构leetcode技术php 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
命令行工具
腾讯云命令行工具 TCCLI 是管理腾讯云资源的统一工具。使用腾讯云命令行工具,您可以快速调用腾讯云 API 来管理您的腾讯云资源。此外,您还可以基于腾讯云的命令行工具来做自动化和脚本处理,以更多样的方式进行组合和重用。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档