blob: b4229e2235f538a8e42adb51c2b61cfdcdcbcd2d [file] [log] [blame]
// Copyright 2020 The Bazel Authors. All rights reserved.
//
// 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 bzltestutil
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"github.com/bazelbuild/rules_go/go/tools/bzltestutil/chdir"
)
// TestWrapperAbnormalExit is used by Wrap to indicate the child
// process exitted without an exit code (for example being killed by a signal).
// We use 6, in line with Bazel's RUN_FAILURE.
const TestWrapperAbnormalExit = 6
func ShouldWrap() bool {
if wrapEnv, ok := os.LookupEnv("GO_TEST_WRAP"); ok {
wrap, err := strconv.ParseBool(wrapEnv)
if err != nil {
log.Fatalf("invalid value for GO_TEST_WRAP: %q", wrapEnv)
}
return wrap
}
_, ok := os.LookupEnv("XML_OUTPUT_FILE")
return ok
}
// shouldAddTestV indicates if the test wrapper should prepend a -test.v flag to
// the test args. This is required to get information about passing tests from
// test2json for complete XML reports.
func shouldAddTestV() bool {
if wrapEnv, ok := os.LookupEnv("GO_TEST_WRAP_TESTV"); ok {
wrap, err := strconv.ParseBool(wrapEnv)
if err != nil {
log.Fatalf("invalid value for GO_TEST_WRAP_TESTV: %q", wrapEnv)
}
return wrap
}
return false
}
// streamMerger intelligently merges an input stdout and stderr stream and dumps
// the output to the writer `inner`. Additional synchronization is applied to
// ensure that one line at a time is written to the inner writer.
type streamMerger struct {
OutW, ErrW *io.PipeWriter
mutex sync.Mutex
inner io.Writer
wg sync.WaitGroup
outR, errR *bufio.Reader
}
func NewStreamMerger(w io.Writer) *streamMerger {
outR, outW := io.Pipe()
errR, errW := io.Pipe()
return &streamMerger{
inner: w,
OutW: outW,
ErrW: errW,
outR: bufio.NewReader(outR),
errR: bufio.NewReader(errR),
}
}
func (m *streamMerger) Start() {
m.wg.Add(2)
process := func(r *bufio.Reader) {
for {
s, err := r.ReadString('\n')
if len(s) > 0 {
m.mutex.Lock()
io.WriteString(m.inner, s)
m.mutex.Unlock()
}
if err == io.EOF {
break
}
}
m.wg.Done()
}
go process(m.outR)
go process(m.errR)
}
func (m *streamMerger) Wait() {
m.wg.Wait()
}
func Wrap(pkg string) error {
var jsonBuffer bytes.Buffer
jsonConverter := NewConverter(&jsonBuffer, pkg, Timestamp)
streamMerger := NewStreamMerger(jsonConverter)
args := os.Args[1:]
if shouldAddTestV() {
args = append([]string{"-test.v"}, args...)
}
exePath := os.Args[0]
if !filepath.IsAbs(exePath) && strings.ContainsRune(exePath, filepath.Separator) && chdir.TestExecDir != "" {
exePath = filepath.Join(chdir.TestExecDir, exePath)
}
// If Bazel sends a SIGTERM because the test timed out, it sends it to all child processes. However,
// we want the wrapper to be around to capute and forward the test output when this happens. Thus,
// we need to ignore the signal. This wrapper will natually ends after the Go test ends, either by
// SIGTERM or the time set by -test.timeout expires. If that doesn't happen, the test and this warpper
// will be killed by Bazel after the grace period (15s) expires.
signal.Ignore(syscall.SIGTERM)
cmd := exec.Command(exePath, args...)
cmd.Env = append(os.Environ(), "GO_TEST_WRAP=0")
cmd.Stderr = io.MultiWriter(os.Stderr, streamMerger.ErrW)
cmd.Stdout = io.MultiWriter(os.Stdout, streamMerger.OutW)
streamMerger.Start()
err := cmd.Run()
streamMerger.ErrW.Close()
streamMerger.OutW.Close()
streamMerger.Wait()
jsonConverter.Close()
if out, ok := os.LookupEnv("XML_OUTPUT_FILE"); ok {
werr := writeReport(jsonBuffer, pkg, out)
if werr != nil {
if err != nil {
return fmt.Errorf("error while generating testreport: %s, (error wrapping test execution: %s)", werr, err)
}
return fmt.Errorf("error while generating testreport: %s", werr)
}
}
return err
}
func writeReport(jsonBuffer bytes.Buffer, pkg string, path string) error {
xml, cerr := json2xml(&jsonBuffer, pkg)
if cerr != nil {
return fmt.Errorf("error converting test output to xml: %s", cerr)
}
if err := ioutil.WriteFile(path, xml, 0664); err != nil {
return fmt.Errorf("error writing test xml: %s", err)
}
return nil
}