gotest.tools/gotestsum@v1.11.0/cmd/handler.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  
    11  	"gotest.tools/gotestsum/internal/junitxml"
    12  	"gotest.tools/gotestsum/internal/log"
    13  	"gotest.tools/gotestsum/testjson"
    14  )
    15  
    16  type eventHandler struct {
    17  	formatter            testjson.EventFormatter
    18  	err                  *bufio.Writer
    19  	jsonFile             writeSyncer
    20  	jsonFileTimingEvents writeSyncer
    21  	maxFails             int
    22  }
    23  
    24  type writeSyncer interface {
    25  	io.WriteCloser
    26  	Sync() error
    27  }
    28  
    29  // nolint:errcheck
    30  func (h *eventHandler) Err(text string) error {
    31  	h.err.WriteString(text)
    32  	h.err.WriteRune('\n')
    33  	h.err.Flush()
    34  	// always return nil, no need to stop scanning if the stderr write fails
    35  	return nil
    36  }
    37  
    38  func (h *eventHandler) Event(event testjson.TestEvent, execution *testjson.Execution) error {
    39  	if err := writeWithNewline(h.jsonFile, event.Bytes()); err != nil {
    40  		return fmt.Errorf("failed to write JSON file: %w", err)
    41  	}
    42  	if event.Action.IsTerminal() {
    43  		if err := writeWithNewline(h.jsonFileTimingEvents, event.Bytes()); err != nil {
    44  			return fmt.Errorf("failed to write JSON file: %w", err)
    45  		}
    46  	}
    47  
    48  	err := h.formatter.Format(event, execution)
    49  	if err != nil {
    50  		return fmt.Errorf("failed to format event: %w", err)
    51  	}
    52  
    53  	if h.maxFails > 0 && len(execution.Failed()) >= h.maxFails {
    54  		return fmt.Errorf("ending test run because max failures was reached")
    55  	}
    56  	return nil
    57  }
    58  
    59  func writeWithNewline(out io.Writer, b []byte) error {
    60  	// ignore artificial events that have len(b) == 0
    61  	if out == nil || len(b) == 0 {
    62  		return nil
    63  	}
    64  	if _, err := out.Write(b); err != nil {
    65  		return err
    66  	}
    67  	_, err := out.Write([]byte{'\n'})
    68  	return err
    69  }
    70  
    71  func (h *eventHandler) Flush() {
    72  	if h.jsonFile != nil {
    73  		if err := h.jsonFile.Sync(); err != nil {
    74  			log.Errorf("Failed to sync JSON file: %v", err)
    75  		}
    76  	}
    77  	if h.jsonFileTimingEvents != nil {
    78  		if err := h.jsonFileTimingEvents.Sync(); err != nil {
    79  			log.Errorf("Failed to sync JSON file: %v", err)
    80  		}
    81  	}
    82  }
    83  
    84  func (h *eventHandler) Close() error {
    85  	if h.jsonFile != nil {
    86  		if err := h.jsonFile.Close(); err != nil {
    87  			log.Errorf("Failed to close JSON file: %v", err)
    88  		}
    89  	}
    90  	if h.jsonFileTimingEvents != nil {
    91  		if err := h.jsonFileTimingEvents.Close(); err != nil {
    92  			log.Errorf("Failed to close JSON file: %v", err)
    93  		}
    94  	}
    95  	return nil
    96  }
    97  
    98  var _ testjson.EventHandler = &eventHandler{}
    99  
   100  func newEventHandler(opts *options) (*eventHandler, error) {
   101  	formatter := testjson.NewEventFormatter(opts.stdout, opts.format, opts.formatOptions)
   102  	if formatter == nil {
   103  		return nil, fmt.Errorf("unknown format %s", opts.format)
   104  	}
   105  	handler := &eventHandler{
   106  		formatter: formatter,
   107  		err:       bufio.NewWriter(opts.stderr),
   108  		maxFails:  opts.maxFails,
   109  	}
   110  
   111  	switch opts.format {
   112  	case "dots", "dots-v1", "dots-v2":
   113  		// Discard the error from the handler to prevent extra lines. The
   114  		// error will be printed in the summary.
   115  		handler.err = bufio.NewWriter(io.Discard)
   116  	}
   117  
   118  	var err error
   119  	if opts.jsonFile != "" {
   120  		_ = os.MkdirAll(filepath.Dir(opts.jsonFile), 0o755)
   121  		handler.jsonFile, err = os.Create(opts.jsonFile)
   122  		if err != nil {
   123  			return handler, fmt.Errorf("failed to create file: %w", err)
   124  		}
   125  	}
   126  	if opts.jsonFileTimingEvents != "" {
   127  		_ = os.MkdirAll(filepath.Dir(opts.jsonFileTimingEvents), 0o755)
   128  		handler.jsonFileTimingEvents, err = os.Create(opts.jsonFileTimingEvents)
   129  		if err != nil {
   130  			return handler, fmt.Errorf("failed to create file: %w", err)
   131  		}
   132  	}
   133  	return handler, nil
   134  }
   135  
   136  func writeJUnitFile(opts *options, execution *testjson.Execution) error {
   137  	if opts.junitFile == "" {
   138  		return nil
   139  	}
   140  	_ = os.MkdirAll(filepath.Dir(opts.junitFile), 0o755)
   141  	junitFile, err := os.Create(opts.junitFile)
   142  	if err != nil {
   143  		return fmt.Errorf("failed to open JUnit file: %v", err)
   144  	}
   145  	defer func() {
   146  		if err := junitFile.Close(); err != nil {
   147  			log.Errorf("Failed to close JUnit file: %v", err)
   148  		}
   149  	}()
   150  
   151  	return junitxml.Write(junitFile, execution, junitxml.Config{
   152  		ProjectName:             opts.junitProjectName,
   153  		FormatTestSuiteName:     opts.junitTestSuiteNameFormat.Value(),
   154  		FormatTestCaseClassname: opts.junitTestCaseClassnameFormat.Value(),
   155  		HideEmptyPackages:       opts.junitHideEmptyPackages,
   156  	})
   157  }
   158  
   159  func postRunHook(opts *options, execution *testjson.Execution) error {
   160  	command := opts.postRunHookCmd.Value()
   161  	if len(command) == 0 {
   162  		return nil
   163  	}
   164  	log.Debugf("exec: %s", command)
   165  
   166  	cmd := exec.Command(command[0], command[1:]...)
   167  	cmd.Stdout = opts.stdout
   168  	cmd.Stderr = opts.stderr
   169  	cmd.Env = append(
   170  		os.Environ(),
   171  		"GOTESTSUM_JSONFILE="+opts.jsonFile,
   172  		"GOTESTSUM_JSONFILE_TIMING_EVENTS="+opts.jsonFileTimingEvents,
   173  		"GOTESTSUM_JUNITFILE="+opts.junitFile,
   174  		fmt.Sprintf("GOTESTSUM_ELAPSED=%.3fs", execution.Elapsed().Seconds()),
   175  		fmt.Sprintf("TESTS_TOTAL=%d", execution.Total()),
   176  		fmt.Sprintf("TESTS_FAILED=%d", len(execution.Failed())),
   177  		fmt.Sprintf("TESTS_SKIPPED=%d", len(execution.Skipped())),
   178  		fmt.Sprintf("TESTS_ERRORS=%d", len(execution.Errors())),
   179  	)
   180  	return cmd.Run()
   181  }