golang源码阅读:test2json

2023-09-06 19:28:07 浏览数 (2)

go test加参数-json就能输出 json格式,下面我们用一个简单的例子看下对他进行分析,然后分析下相关源码。对于单测

代码语言:javascript复制
package test

import (
  "fmt"
  "testing"
)

func TestAdd(t *testing.T) {
  type args struct {
    a int
    b int
  }
  tests := []struct {
    name string
    args args
    want int
  }{
    // TODO: Add test cases.
    {
      args: args{
        a: 1,
        b: 2,
      },
      want: 3,
    },

    {
      args: args{
        a: 1,
        b: 2,
      },
      want: 4,
    },
  }
  for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
      if got := Add(tt.args.a, tt.args.b); got != tt.want {
        t.Errorf("Add() = %v, want %v", got, tt.want)
      }
    })
  }
}

func TestSub(t *testing.T) {
  type args struct {
    a int
    b int
  }
  tests := []struct {
    name string
    args args
    want int
  }{
    // TODO: Add test cases.
    {
      args: args{
        a: 1,
        b: 2,
      },
      want: -1,
    },
  }
  for _, tt := range tests {
    fmt.Println("before")
    fmt.Print("before not endline")
    t.Run(tt.name, func(t *testing.T) {
      if got := Sub(tt.args.a, tt.args.b); got != tt.want {
        t.Errorf("Sub() = %v, want %v", got, tt.want)
      }
    })
    fmt.Println("after")
    t.Skip("skipping test; 1 not set")
  }
}

执行命令

代码语言:javascript复制
% go test ./test/gotest/test/... -json

它的输出如下:

代码语言:javascript复制
{"Time":"2023-05-13T17:27:15.492681 08:00","Action":"run","Package":"learn/test/gotest/test","Test":"TestAdd"}
{"Time":"2023-05-13T17:27:15.493129 08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestAdd","Output":"=== RUN   TestAddn"}
{"Time":"2023-05-13T17:27:15.49316 08:00","Action":"run","Package":"learn/test/gotest/test","Test":"TestAdd/#00"}
{"Time":"2023-05-13T17:27:15.493168 08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestAdd/#00","Output":"=== RUN   TestAdd/#00n"}
{"Time":"2023-05-13T17:27:15.493185 08:00","Action":"run","Package":"learn/test/gotest/test","Test":"TestAdd/#01"}
{"Time":"2023-05-13T17:27:15.493191 08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestAdd/#01","Output":"=== RUN   TestAdd/#01n"}
{"Time":"2023-05-13T17:27:15.493195 08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestAdd/#01","Output":"    add_test.go:38: Add() = 3, want 4n"}
{"Time":"2023-05-13T17:27:15.493444 08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestAdd","Output":"--- FAIL: TestAdd (0.00s)n"}
{"Time":"2023-05-13T17:27:15.493471 08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestAdd/#00","Output":"    --- PASS: TestAdd/#00 (0.00s)n"}
{"Time":"2023-05-13T17:27:15.493477 08:00","Action":"pass","Package":"learn/test/gotest/test","Test":"TestAdd/#00","Elapsed":0}
{"Time":"2023-05-13T17:27:15.49349 08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestAdd/#01","Output":"    --- FAIL: TestAdd/#01 (0.00s)n"}
{"Time":"2023-05-13T17:27:15.493495 08:00","Action":"fail","Package":"learn/test/gotest/test","Test":"TestAdd/#01","Elapsed":0}
{"Time":"2023-05-13T17:27:15.493499 08:00","Action":"fail","Package":"learn/test/gotest/test","Test":"TestAdd","Elapsed":0}
{"Time":"2023-05-13T17:27:15.493503 08:00","Action":"run","Package":"learn/test/gotest/test","Test":"TestSub"}
{"Time":"2023-05-13T17:27:15.493506 08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestSub","Output":"=== RUN   TestSubn"}
{"Time":"2023-05-13T17:27:15.49362 08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestSub","Output":"beforen"}
{"Time":"2023-05-13T17:27:15.493633 08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestSub","Output":"before not endline=== RUN   TestSub/#00n"}
{"Time":"2023-05-13T17:27:15.493642 08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestSub","Output":"aftern"}
{"Time":"2023-05-13T17:27:15.493646 08:00","Action":"cont","Package":"learn/test/gotest/test","Test":"TestSub"}
{"Time":"2023-05-13T17:27:15.49365 08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestSub","Output":"=== CONT  TestSubn"}
{"Time":"2023-05-13T17:27:15.493654 08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestSub","Output":"    add_test.go:72: skipping test; 1 not setn"}
{"Time":"2023-05-13T17:27:15.493659 08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestSub","Output":"--- SKIP: TestSub (0.00s)n"}
{"Time":"2023-05-13T17:27:15.493802 08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestSub/#00","Output":"    --- PASS: TestSub/#00 (0.00s)n"}
{"Time":"2023-05-13T17:27:15.493818 08:00","Action":"pass","Package":"learn/test/gotest/test","Test":"TestSub/#00","Elapsed":0}
{"Time":"2023-05-13T17:27:15.493824 08:00","Action":"skip","Package":"learn/test/gotest/test","Test":"TestSub","Elapsed":0}
{"Time":"2023-05-13T17:27:15.493827 08:00","Action":"output","Package":"learn/test/gotest/test","Output":"FAILn"}
{"Time":"2023-05-13T17:27:15.493899 08:00","Action":"output","Package":"learn/test/gotest/test","Output":"FAILtlearn/test/gotest/testt0.138sn"}
{"Time":"2023-05-13T17:27:15.493915 08:00","Action":"fail","Package":"learn/test/gotest/test","Elapsed":0.139}

可以看到,和直直接输出相比,输出格式变成了json,每条输出都是一个json对象,它包含属性有时间,类型、包、测试方法、以及输出等。比不带-json参数多了很多内容

代码语言:javascript复制
--- FAIL: TestAdd (0.00s)
    --- FAIL: TestAdd/#01 (0.00s)
        add_test.go:38: Add() = 3, want 4
before
before not endlineafter
FAIL
FAIL    learn/test/gotest/test  0.176s
FAIL

下面分析下它的源码实现,源码位于src/cmd/internal/test2json/test2json.go输出被定义成了event类型

代码语言:javascript复制
type event struct {
  Time    *time.Time `json:",omitempty"`
  Action  string
  Package string     `json:",omitempty"`
  Test    string     `json:",omitempty"`
  Elapsed *float64   `json:",omitempty"`
  Output  *textBytes `json:",omitempty"`
}

其中Converter负责将gotest的输出转化为event事件

代码语言:javascript复制
type Converter struct {
  w        io.Writer  // JSON output stream
  pkg      string     // package to name in events
  mode     Mode       // mode bits
  start    time.Time  // time converter started
  testName string     // name of current test, for output attribution
  report   []*event   // pending test result reports (nested for subtests)
  result   string     // overall test result if seen
  input    lineBuffer // input buffer
  output   lineBuffer // output buffer
}

构造方法如下

代码语言:javascript复制
func NewConverter(w io.Writer, pkg string, mode Mode) *Converter {
  c := new(Converter)
  *c = Converter{
    w:     w,
    pkg:   pkg,
    mode:  mode,
    start: time.Now(),
    input: lineBuffer{
      b:    make([]byte, 0, inBuffer),
      line: c.handleInputLine,
      part: c.output.write,
    },
    output: lineBuffer{
      b:    make([]byte, 0, outBuffer),
      line: c.writeOutputEvent,
      part: c.writeOutputEvent,
    },
  }
  return c
}

退出对应了pass和fail两种action

代码语言:javascript复制
func (c *Converter) Exited(err error) {
  if err == nil {
    c.result = "pass"
  } else {
    c.result = "fail"
  }
}

相应的文案定义如下,也就是我们常看到的单测结果

代码语言:javascript复制
  updates = [][]byte{
    []byte("=== RUN   "),
    []byte("=== PAUSE "),
    []byte("=== CONT  "),
  }

  reports = [][]byte{
    []byte("--- PASS: "),
    []byte("--- FAIL: "),
    []byte("--- SKIP: "),
    []byte("--- BENCH: "),
  }

根据输入的每一行内容,进行解析,得到event,如果都没有匹配成功,就把前一个测试的名字赋值给当前测试。否则根据冒号截取内容获取Action和Test。最后把原始输入,写入到Output字段里面。

代码语言:javascript复制
func (c *Converter) handleInputLine(line []byte) {
  // Final PASS or FAIL.
  if bytes.Equal(line, bigPass) || bytes.Equal(line, bigFail) || bytes.HasPrefix(line, bigFailErrorPrefix) {
    c.flushReport(0)
    c.output.write(line)
    if bytes.Equal(line, bigPass) {
      c.result = "pass"
    } else {
      c.result = "fail"
    }
    return
  }
  if bytes.HasPrefix(line, skipLinePrefix) && bytes.HasSuffix(line, skipLineSuffix) && len(c.report) == 0 {
    c.result = "skip"
  }
  
  for _, magic := range reports {
      if bytes.HasPrefix(line, magic) {
        actionColon = true
        ok = true
        break
      }
    }
   
    if indent > 0 && indent <= len(c.report) {
      c.testName = c.report[indent-1].Test
    }
   
     action := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(string(line[4:i])), ":"))
     name := strings.TrimSpace(string(line[i:]))
     
     c.output.write(origLine)

大于当前层级的结果输出

代码语言:javascript复制
func (c *Converter) flushReport(depth int) {
  c.testName = ""
  for len(c.report) > depth {
    e := c.report[len(c.report)-1]
    c.report = c.report[:len(c.report)-1]
    c.writeEvent(e)
  }
}

输出output是一个单独的事件

代码语言:javascript复制
func (c *Converter) writeOutputEvent(out []byte) {
  c.writeEvent(&event{
    Action: "output",
    Output: (*textBytes)(&out),
  })
}

最后来到了writevent函数,把包名字和测试名字赋值,然后序列化。加入换行然后输出。

代码语言:javascript复制
func (c *Converter) writeEvent(e *event) {
  e.Package = c.pkg
  if c.mode&Timestamp != 0 {
    t := time.Now()
    e.Time = &t
  }
  if e.Test == "" {
    e.Test = c.testName
  }
  js, err := json.Marshal(e)
  js = append(js, 'n')
  c.w.Write(js)

总的来说,就是解析字符串,转化成json的一个过程。

0 人点赞