github.com/gdubicki/ets@v0.2.3-0.20240420195337-e89d6a2fdbda/main_test.go (about) 1 package main_test 2 3 import ( 4 "io" 5 "log" 6 "os" 7 "os/exec" 8 "path" 9 "reflect" 10 "regexp" 11 "runtime" 12 "strconv" 13 "strings" 14 "syscall" 15 "testing" 16 "time" 17 18 "github.com/creack/pty" 19 ) 20 21 var rootdir string 22 var tempdir string 23 var executable string 24 25 func init() { 26 _, currentFile, _, _ := runtime.Caller(0) 27 rootdir = path.Dir(currentFile) 28 } 29 30 func compile(moduledir string, output string) { 31 cmd := exec.Command("go", "build", "-o", output) 32 cmd.Dir = moduledir 33 if err := cmd.Run(); err != nil { 34 log.Fatalf("failed to compile %s: %s", moduledir, err) 35 } 36 } 37 38 func TestMain(m *testing.M) { 39 var retcode int 40 var err error 41 42 defer func() { os.Exit(retcode) }() 43 44 tempdir, err = os.MkdirTemp("", "*") 45 if err != nil { 46 log.Fatal(err) 47 } 48 defer os.RemoveAll(tempdir) 49 50 executable = path.Join(tempdir, "ets") 51 52 // Build ets and test fixtures to tempdir. 53 compile(rootdir, executable) 54 fixturesdir := path.Join(rootdir, "fixtures") 55 content, err := os.ReadDir(fixturesdir) 56 if err != nil { 57 log.Fatal(err) 58 } 59 for _, entry := range content { 60 if entry.IsDir() { 61 name := entry.Name() 62 compile(path.Join(fixturesdir, name), path.Join(tempdir, name)) 63 } 64 } 65 66 err = os.Chdir(tempdir) 67 if err != nil { 68 log.Fatal(err) 69 } 70 71 retcode = m.Run() 72 } 73 74 type parsedLine struct { 75 raw string 76 prefix string 77 output string 78 captures map[string]string 79 } 80 81 func parseOutput(output []byte, prefixPattern string) []*parsedLine { 82 linePattern := regexp.MustCompile(`^(?P<prefix>` + prefixPattern + `) (?P<output>.*)$`) 83 lines := strings.Split(string(output), "\n") 84 if lines[len(lines)-1] == "" { 85 lines = lines[:len(lines)-1] // Drop final empty line. 86 } 87 parsed := make([]*parsedLine, 0) 88 for _, line := range lines { 89 // Drop final CR if there is one. 90 if line != "" && line[len(line)-1] == '\r' { 91 line = line[:len(line)-1] 92 } 93 m := linePattern.FindStringSubmatch(line) 94 if m == nil { 95 parsed = append(parsed, &parsedLine{ 96 raw: line, 97 prefix: "", 98 output: "", 99 captures: nil, 100 }) 101 } else { 102 captures := make(map[string]string) 103 for i, name := range linePattern.SubexpNames() { 104 if i != 0 && name != "" { 105 captures[name] = m[i] 106 } 107 } 108 parsed = append(parsed, &parsedLine{ 109 raw: line, 110 prefix: captures["prefix"], 111 output: captures["output"], 112 captures: captures, 113 }) 114 } 115 } 116 return parsed 117 } 118 119 func TestBasic(t *testing.T) { 120 defaultOutputs := []string{"out1", "err1", "out2", "err2", "out3", "err3"} 121 tests := []struct { 122 name string 123 args []string 124 prefixPattern string 125 expectedOutputs []string 126 }{ 127 { 128 "basic", 129 []string{"./basic"}, 130 `\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]`, 131 defaultOutputs, 132 }, 133 { 134 "basic-format", 135 []string{"-f", "%m/%d/%y %T:", "./basic"}, 136 `\d{2}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}:`, 137 defaultOutputs, 138 }, 139 { 140 "basic-elapsed", 141 []string{"-s", "./basic"}, 142 `\[00:00:00\]`, 143 defaultOutputs, 144 }, 145 { 146 "basic-elapsed-format", 147 []string{"-s", "-f", "%T.%f", "./basic"}, 148 `00:00:00\.\d{6}`, 149 defaultOutputs, 150 }, 151 { 152 "basic-incremental", 153 []string{"-i", "./basic"}, 154 `\[00:00:00\]`, 155 defaultOutputs, 156 }, 157 { 158 "basic-incremental-format", 159 []string{"-i", "-f", "%T.%f", "./basic"}, 160 `00:00:00\.\d{6}`, 161 defaultOutputs, 162 }, 163 { 164 "basic-utc-format", 165 []string{"-u", "-f", "[%F %T%z]", "./basic"}, 166 `\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\+0000\]`, 167 defaultOutputs, 168 }, 169 { 170 "basic-timezone-format", 171 []string{"-z", "America/Los_Angeles", "-f", "[%F %T %Z]", "./basic"}, 172 `\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} P[DS]T\]`, 173 defaultOutputs, 174 }, 175 { 176 "basic-shell", 177 []string{"./basic 2>/dev/null | nl -w1 -s' '"}, 178 `\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]`, 179 []string{"1 out1", "2 out2", "3 out3"}, 180 }, 181 } 182 for _, test := range tests { 183 t.Run(test.name, func(t *testing.T) { 184 cmd := exec.Command("./ets", test.args...) 185 output, err := cmd.Output() 186 if err != nil { 187 t.Fatalf("command failed: %s", err) 188 } 189 parsed := parseOutput(output, test.prefixPattern) 190 outputs := make([]string, 0) 191 for _, pl := range parsed { 192 if pl.prefix == "" { 193 t.Errorf("unexpected line: %s", pl.raw) 194 } 195 outputs = append(outputs, pl.output) 196 } 197 if !reflect.DeepEqual(outputs, test.expectedOutputs) { 198 t.Fatalf("wrong outputs: expected %#v, got %#v", test.expectedOutputs, outputs) 199 } 200 }) 201 } 202 } 203 204 func TestCR(t *testing.T) { 205 cmd := exec.Command("./ets", "-f", "[timestamp]", "echo '1\r2'") 206 expectedOutput := "[timestamp] 1\r[timestamp] 2\n" 207 output, err := cmd.Output() 208 if err != nil { 209 t.Fatalf("command failed: %s", err) 210 } 211 if string(output) != expectedOutput { 212 t.Fatalf("wrong output: expected %#v, got %#v", expectedOutput, string(output)) 213 } 214 } 215 216 func TestStdin(t *testing.T) { 217 input := "out1\nout2\nout3\n" 218 expectedOutputs := []string{"out1", "out2", "out3"} 219 cmd := exec.Command("./ets") 220 stdin, _ := cmd.StdinPipe() 221 go func() { 222 defer stdin.Close() 223 _, _ = stdin.Write([]byte(input)) 224 }() 225 output, err := cmd.Output() 226 if err != nil { 227 t.Fatalf("command failed: %s", err) 228 } 229 parsed := parseOutput(output, `\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]`) 230 outputs := make([]string, 0) 231 for _, pl := range parsed { 232 if pl.prefix == "" { 233 t.Errorf("unexpected line: %s", pl.raw) 234 } 235 outputs = append(outputs, pl.output) 236 } 237 if !reflect.DeepEqual(outputs, expectedOutputs) { 238 t.Fatalf("wrong outputs: expected %#v, got %#v", expectedOutputs, outputs) 239 } 240 } 241 242 func TestElapsedMode(t *testing.T) { 243 if testing.Short() { 244 t.Skip("skipping slow test in short mode") 245 } 246 expectedOutput := "[1] out1\n[2] out2\n[3] out3\n" 247 cmd := exec.Command("./ets", "-s", "-f", "[%s]", "./timed") 248 output, err := cmd.Output() 249 if err != nil { 250 t.Fatalf("command failed: %s", err) 251 } 252 if string(output) != expectedOutput { 253 t.Fatalf("wrong output: expected %#v, got %#v", expectedOutput, string(output)) 254 } 255 } 256 257 func TestIncrementalMode(t *testing.T) { 258 if testing.Short() { 259 t.Skip("skipping slow test in short mode") 260 } 261 expectedOutput := "[1] out1\n[1] out2\n[1] out3\n" 262 cmd := exec.Command("./ets", "-i", "-f", "[%s]", "./timed") 263 output, err := cmd.Output() 264 if err != nil { 265 t.Fatalf("command failed: %s", err) 266 } 267 if string(output) != expectedOutput { 268 t.Fatalf("wrong output: expected %#v, got %#v", expectedOutput, string(output)) 269 } 270 } 271 272 func TestExitCode(t *testing.T) { 273 for code := 1; code < 6; code++ { 274 t.Run("exitcode-"+strconv.Itoa(code), func(t *testing.T) { 275 cmd := exec.Command("./ets", "./basic", "-exitcode", strconv.Itoa(code)) 276 err := cmd.Run() 277 errExit, ok := err.(*exec.ExitError) 278 if !ok { 279 t.Fatalf("expected ExitError, got %#v", err) 280 } 281 if errExit.ExitCode() != code { 282 t.Fatalf("expected exit code %d, got %d", code, errExit.ExitCode()) 283 } 284 }) 285 } 286 } 287 288 func TestSignals(t *testing.T) { 289 if testing.Short() { 290 t.Skip("skipping slow test in short mode") 291 } 292 cmd := exec.Command("./ets", "./signals") 293 go func() { 294 time.Sleep(time.Second) 295 _ = cmd.Process.Signal(syscall.SIGINT) 296 time.Sleep(time.Second) 297 _ = cmd.Process.Signal(syscall.SIGTERM) 298 }() 299 output, err := cmd.Output() 300 if err != nil { 301 t.Fatalf("command failed: %s", err) 302 } 303 parsed := parseOutput(output, `\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]`) 304 outputs := make([]string, 0) 305 for _, pl := range parsed { 306 if pl.prefix == "" { 307 t.Errorf("unexpected line: %s", pl.raw) 308 } 309 outputs = append(outputs, pl.output) 310 } 311 for _, expectedOutput := range []string{ 312 "busy waiting", 313 "ignored SIGINT", 314 "shutting down after receiving SIGTERM", 315 } { 316 found := false 317 for _, output := range outputs { 318 if output == expectedOutput { 319 found = true 320 break 321 } 322 } 323 if !found { 324 t.Errorf("expected output %#v not found in outputs %#v", expectedOutput, outputs) 325 } 326 } 327 } 328 329 func TestWindowSize(t *testing.T) { 330 tests := []struct { 331 name string 332 args []string 333 prefixPattern string 334 rows uint16 335 cols uint16 336 expectedOutput string 337 }{ 338 { 339 "default", 340 []string{"./winsize"}, 341 `\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]`, 342 24, 343 80, 344 "58x24", 345 }, 346 { 347 "color", 348 []string{"-f", "\x1b[32m[%Y-%m-%d %H:%M:%S]\x1b[0m", "./winsize"}, 349 `\x1b\[32m\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]\x1b\[0m`, 350 24, 351 80, 352 "58x24", 353 }, 354 { 355 "wide-chars", 356 []string{"-f", "[时间 %Y-%m-%d %H:%M:%S]", "./winsize"}, 357 `\[时间 \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]`, 358 24, 359 80, 360 "53x24", 361 }, 362 { 363 "narrow-terminal", 364 []string{"./winsize"}, 365 `\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]`, 366 24, 367 10, 368 "0x24", 369 }, 370 } 371 for _, test := range tests { 372 t.Run(test.name, func(t *testing.T) { 373 expectedOutputs := []string{test.expectedOutput} 374 cmd := exec.Command("./ets", test.args...) 375 ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: test.rows, Cols: test.cols, X: 0, Y: 0}) 376 if err != nil { 377 t.Fatalf("failed to start command in pty: %s", err) 378 } 379 defer func() { _ = ptmx.Close() }() 380 output, err := io.ReadAll(ptmx) 381 // TODO: figure out why we get &os.PathError{Op:"read", Path:"/dev/ptmx", Err:0x5} on Linux. 382 // https://github.com/creack/pty/issues/100 383 if len(output) == 0 && err != nil { 384 t.Fatalf("failed to read pty output: %s", err) 385 } 386 parsed := parseOutput(output, test.prefixPattern) 387 outputs := make([]string, 0) 388 for _, pl := range parsed { 389 if pl.prefix == "" { 390 t.Errorf("unexpected line: %s", pl.raw) 391 } 392 outputs = append(outputs, pl.output) 393 } 394 if !reflect.DeepEqual(outputs, expectedOutputs) { 395 t.Fatalf("wrong outputs: expected %#v, got %#v", expectedOutputs, outputs) 396 } 397 }) 398 } 399 }