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  }