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 }