github.com/xuoe/logwrap@v0.1.1-0.20231108152724-8c21b6241c7d/main_test.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"sort"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/buildkite/shellwords"
    16  )
    17  
    18  func init() {
    19  	notice = nopNotice
    20  }
    21  
    22  func TestInvoke(gt *testing.T) {
    23  	t := newCliTest(gt)
    24  	t.In("./testdata", func(t *cliTest) {
    25  		type (
    26  			streams struct {
    27  				stdin          string // <command> | logwrap
    28  				stdout, stderr string
    29  				pipe           string // <command> | logwrap <command>
    30  			}
    31  		)
    32  		const (
    33  			defaultArgs = "-1 '{text}' -2 '{text}'"
    34  		)
    35  		var (
    36  			trim = func(s string) string {
    37  				return trimWhitespace(s, wsBOF, wsBOL)
    38  			}
    39  			args = func(args ...string) (res string) {
    40  				res = defaultArgs
    41  				if len(args) > 0 {
    42  					res += " " + strings.Join(args, " ")
    43  				}
    44  				return
    45  			}
    46  		)
    47  
    48  		// Build a binary that we can invoke to print to stdout/stderr.
    49  		t.build("printer.go", printerCode)
    50  
    51  		for _, test := range []struct {
    52  			name   string
    53  			args   string
    54  			env    map[string]string
    55  			input  streams
    56  			output streams
    57  			pre    files
    58  			post   files
    59  		}{
    60  			{
    61  				name: "basic",
    62  				args: args(),
    63  				input: streams{
    64  					stdout: `
    65  					123
    66  					456
    67  				`,
    68  					stderr: `
    69  					a
    70  					b
    71  					c
    72  				`,
    73  				},
    74  				output: streams{
    75  					stdout: `
    76  					123
    77  					456
    78  				`,
    79  					stderr: `
    80  					a
    81  					b
    82  					c
    83  				`,
    84  				},
    85  			},
    86  			{
    87  				name: "default name placeholder",
    88  				args: args("-1 '{name}: {text}'"),
    89  				input: streams{
    90  					stdout: `
    91  					a b c
    92  					`,
    93  				},
    94  				output: streams{
    95  					stdout: `
    96  					printer: a b c
    97  					`,
    98  				},
    99  			},
   100  			{
   101  				name: "custom name placeholder",
   102  				args: args("--name test", "-1 '{name}: {text}'"),
   103  				input: streams{
   104  					stdout: `
   105  					a b c
   106  					c b a
   107  					`,
   108  				},
   109  				output: streams{
   110  					stdout: `
   111  					test: a b c
   112  					test: c b a
   113  					`,
   114  				},
   115  			},
   116  			{
   117  				name: "strip ansi",
   118  				args: args("--ansi 2"),
   119  				input: streams{
   120  					stdout: fmt.Sprintf("a %s c", codes["fg"]["red"].wrap("b")),
   121  					stderr: fmt.Sprintf("A %s C", codes["fg"]["red"].wrap("B")),
   122  				},
   123  				output: streams{
   124  					stdout: `
   125  					a b c
   126  					`,
   127  					stderr: fmt.Sprintf("A %s C\n", codes["fg"]["red"].wrap("B")),
   128  				},
   129  			},
   130  			{
   131  				name: "all streams",
   132  				args: args("-1 '1: {text}'", "-2 '2: {text}'"),
   133  				input: streams{
   134  					stdout: `
   135  					a
   136  					b
   137  					`,
   138  					stderr: `
   139  					C
   140  					D
   141  					`,
   142  				},
   143  				output: streams{
   144  					stdout: `
   145  					1: a
   146  					1: b
   147  					`,
   148  					stderr: `
   149  					2: C
   150  					2: D
   151  					`,
   152  				},
   153  				post: files{
   154  					// There's no way to guarantee the write order of the goroutines started
   155  					// by exec.Command.Start().
   156  				},
   157  			},
   158  			{
   159  				name: "env",
   160  				args: "--name test",
   161  				env: map[string]string{
   162  					"LOGWRAP_STDOUT": "OUT: {name}: {text}",
   163  					"LOGWRAP_STDERR": "ERR: {name}: {text}",
   164  					"LOGWRAP_OPTS":   "--name TEST",
   165  				},
   166  				input: streams{
   167  					stdout: "hi",
   168  					stderr: "hello",
   169  				},
   170  				output: streams{
   171  					stdout: `
   172  					OUT: test: hi
   173  					`,
   174  					stderr: `
   175  					ERR: test: hello
   176  					`,
   177  				},
   178  			},
   179  			{
   180  				name: "truncate logfile",
   181  				args: args("-f log", "--max-size 5b"),
   182  				input: streams{
   183  					stdout: `
   184  					test
   185  					TEST
   186  					Test
   187  					`,
   188  				},
   189  				output: streams{
   190  					stdout: `
   191  					test
   192  					TEST
   193  					Test
   194  					`,
   195  				},
   196  				post: files{
   197  					"log": `
   198  					Test
   199  					`,
   200  				},
   201  			},
   202  			{
   203  				name: "rotate logfile",
   204  				args: args("-f log", "--max-size 5b", "--max-count 2"),
   205  				input: streams{
   206  					stdout: `
   207  					test
   208  					TEST
   209  					Test
   210  					`,
   211  				},
   212  				output: streams{
   213  					stdout: `
   214  					test
   215  					TEST
   216  					Test
   217  					`,
   218  				},
   219  				post: files{
   220  					"log": `
   221  					Test
   222  					`,
   223  					"log.0": `
   224  					TEST
   225  					`,
   226  					"log.1": `
   227  					test
   228  					`,
   229  				},
   230  			},
   231  			{
   232  				name: "logfile filename order",
   233  				args: args("-f log", "--max-size 2b", "--max-count 3"),
   234  				input: streams{
   235  					stdout: `
   236  					0
   237  					1
   238  					2
   239  					3
   240  					4
   241  					5
   242  					`,
   243  				},
   244  				output: streams{
   245  					stdout: `
   246  					0
   247  					1
   248  					2
   249  					3
   250  					4
   251  					5
   252  					`,
   253  				},
   254  				post: files{
   255  					"log":   "5\n",
   256  					"log.0": "4\n",
   257  					"log.1": "3\n",
   258  					"log.2": "2\n",
   259  				},
   260  			},
   261  			{
   262  				name: "logfile filename order",
   263  				args: args("-f log", "--max-size 2b", "--max-count 100"),
   264  				input: streams{
   265  					stdout: `
   266  					0
   267  					1
   268  					2
   269  					3
   270  					4
   271  					5
   272  					6
   273  					7
   274  					8
   275  					9
   276  					0
   277  					1
   278  					2
   279  					`,
   280  				},
   281  				output: streams{
   282  					stdout: `
   283  					0
   284  					1
   285  					2
   286  					3
   287  					4
   288  					5
   289  					6
   290  					7
   291  					8
   292  					9
   293  					0
   294  					1
   295  					2
   296  					`,
   297  				},
   298  				post: files{
   299  					"log":    "2\n",
   300  					"log.00": "1\n",
   301  					"log.01": "0\n",
   302  					"log.02": "9\n",
   303  					"log.03": "8\n",
   304  					"log.04": "7\n",
   305  					"log.05": "6\n",
   306  					"log.06": "5\n",
   307  					"log.07": "4\n",
   308  					"log.08": "3\n",
   309  					"log.09": "2\n",
   310  					"log.10": "1\n",
   311  					"log.11": "0\n",
   312  				},
   313  			},
   314  			{
   315  				name: "drop unused logfiles",
   316  				args: args("-f log", "--max-size 4b", "--max-count 3"),
   317  				input: streams{
   318  					stdout: `
   319  					abc
   320  					def
   321  					ghi
   322  					`,
   323  				},
   324  				output: streams{
   325  					stdout: `
   326  					abc
   327  					def
   328  					ghi
   329  					`,
   330  				},
   331  				pre: files{
   332  					"log.013": "A", // newest
   333  					"log.030": "B",
   334  					"log.213": "C",
   335  					"log.481": "D",
   336  					"log.999": "E",
   337  				},
   338  				post: files{
   339  					"log":   "ghi\n",
   340  					"log.0": "def\n",
   341  					"log.1": "abc\n",
   342  					"log.2": "A", // oldest
   343  				},
   344  			},
   345  			{
   346  				name: "drop unused logfiles",
   347  				args: args("-f log", "--max-size 4b", "--max-count 3"),
   348  				input: streams{
   349  					stdout: `
   350  					abc
   351  					def
   352  					`,
   353  				},
   354  				output: streams{
   355  					stdout: `
   356  					abc
   357  					def
   358  					`,
   359  				},
   360  				pre: files{
   361  					"log.01": "A", // newest
   362  					"log.11": "B",
   363  					"log.22": "C",
   364  					"log.33": "X",
   365  					"log.44": "Y",
   366  					"log.55": "Z",
   367  				},
   368  				post: files{
   369  					"log":   "def\n",
   370  					"log.0": "abc\n", // newest
   371  					"log.1": "A",
   372  					"log.2": "B",
   373  				},
   374  			},
   375  			{
   376  				name: "keep but rename unused logfiles if no limit given",
   377  				args: args("-f log", "--max-size 4b"),
   378  				input: streams{
   379  					stdout: `
   380  					abc
   381  					def
   382  					`,
   383  				},
   384  				output: streams{
   385  					stdout: `
   386  					abc
   387  					def
   388  					`,
   389  				},
   390  				pre: files{
   391  					"log.013": "A", // newest
   392  					"log.030": "B",
   393  					"log.213": "C",
   394  					"log.481": "D",
   395  					"log.999": "E",
   396  				},
   397  				post: files{
   398  					"log":   "def\n",
   399  					"log.0": "A",
   400  					"log.1": "B",
   401  					"log.2": "C",
   402  					"log.3": "D",
   403  					"log.4": "E",
   404  				},
   405  			},
   406  			{
   407  				name: "rotate if no space left",
   408  				args: args("-f log", "--max-size 4b"),
   409  				input: streams{
   410  					stdout: `
   411  					abc ABC
   412  					xyz XYZ
   413  					`,
   414  				},
   415  				output: streams{
   416  					stdout: `
   417  					abc ABC
   418  					xyz XYZ
   419  					`,
   420  				},
   421  				pre: files{
   422  					"log": `
   423  					123
   424  					`,
   425  				},
   426  				post: files{
   427  					"log": `
   428  					xyz XYZ
   429  					`,
   430  				},
   431  			},
   432  			{
   433  				name: "rotate if no space left",
   434  				args: args("-f log", "--max-size 4b", "--max-count 1"),
   435  				input: streams{
   436  					stdout: `
   437  					abc ABC
   438  					xyz XYZ
   439  					`,
   440  				},
   441  				output: streams{
   442  					stdout: `
   443  					abc ABC
   444  					xyz XYZ
   445  					`,
   446  				},
   447  				pre: files{
   448  					"log": `
   449  					123
   450  					`,
   451  				},
   452  				post: files{
   453  					"log": `
   454  					xyz XYZ
   455  					`,
   456  					"log.0": `
   457  					abc ABC
   458  					`,
   459  				},
   460  			},
   461  			{
   462  				name: "rotate if no space left",
   463  				args: args("-f log", "--max-size 10b", "--max-count 1"),
   464  				input: streams{
   465  					stdout: `
   466  					new line
   467  					`,
   468  				},
   469  				output: streams{
   470  					stdout: `
   471  					new line
   472  					`,
   473  				},
   474  				pre: files{
   475  					"log": `
   476  					yadda yadda
   477  					`,
   478  				},
   479  				post: files{
   480  					"log": `
   481  					new line
   482  					`,
   483  					"log.0": `
   484  					yadda yadda
   485  					`,
   486  				},
   487  			},
   488  			{
   489  				name: "ensure logfile is closed only after writing",
   490  				args: args("-f log"),
   491  				input: streams{
   492  					stdout: `new line`,
   493  				},
   494  				output: streams{
   495  					stdout: `
   496  					new line
   497  					`,
   498  				},
   499  				pre: files{
   500  					"log": `
   501  					yadda yadda
   502  					`,
   503  				},
   504  				post: files{
   505  					"log": `
   506  					yadda yadda
   507  					new line
   508  					`,
   509  				},
   510  			},
   511  			{
   512  				name: "stdin",
   513  				args: args("-f log"),
   514  				input: streams{
   515  					pipe: `
   516  					hi from stdin
   517  					test
   518  					`,
   519  				},
   520  				output: streams{
   521  					stdout: `
   522  					hi from stdin
   523  					test
   524  					`,
   525  				},
   526  				post: files{
   527  					"log": `
   528  					hi from stdin
   529  					test
   530  					`,
   531  				},
   532  			},
   533  			{
   534  				name: "stdin piped to command",
   535  				args: args("-f log -1 '{name}: {text}'"),
   536  				input: streams{
   537  					stdin: `
   538  					hi from stdin
   539  					`,
   540  				},
   541  				output: streams{
   542  					stdout: `
   543  					printer: hi from stdin
   544  					`,
   545  				},
   546  				post: files{
   547  					"log": `
   548  					printer: hi from stdin
   549  					`,
   550  				},
   551  			},
   552  			{
   553  				name: "placeholders tryout",
   554  				args: args("-1 '{name}{rjust 6 {name}}' --name test"),
   555  				input: streams{
   556  					stdin: `
   557  					asdf
   558  					`,
   559  				},
   560  				output: streams{
   561  					stdout: `
   562  						test  test
   563  						`,
   564  				},
   565  			},
   566  		} {
   567  			t.Run(test.name, func(t *cliTest) {
   568  				// Populate the directory with "pre-existing" files.
   569  				for file, content := range test.pre {
   570  					t.write(file, trim(content))
   571  				}
   572  
   573  				// Set the environment.
   574  				for k, v := range test.env {
   575  					t.env(k, v)
   576  				}
   577  
   578  				// Mock the inputs to files.
   579  				for _, input := range []struct {
   580  					name string
   581  					data string
   582  				}{
   583  					{"STDIN", trim(test.input.stdin)},
   584  					{"STDOUT", trim(test.input.stdout)},
   585  					{"STDERR", trim(test.input.stderr)},
   586  				} {
   587  					if input.data != "" {
   588  						t.write(input.name, input.data)
   589  					}
   590  				}
   591  
   592  				// Prep args and invoke.
   593  				var (
   594  					stdout, stderr bytes.Buffer
   595  					stdin          io.Reader
   596  					argfmt         = "%s --"
   597  				)
   598  				if test.input.pipe != "" {
   599  					stdin = strings.NewReader(trim(test.input.pipe))
   600  				} else {
   601  					argfmt += " ./printer"
   602  				}
   603  				args, err := shellwords.Split(fmt.Sprintf(argfmt, test.args))
   604  				if err != nil {
   605  					t.Fatal(err)
   606  				}
   607  				if err := invoke(stdin, &stdout, &stderr, args); err != nil {
   608  					t.Fatal(err)
   609  				}
   610  
   611  				// Check if the generated files match the test content.
   612  				for _, file := range exclude(t.ls("."), "printer*", "STD*") {
   613  					if !test.post.has(file) {
   614  						t.Errorf("\nextra file: %s: %q", file, t.read(file))
   615  					}
   616  				}
   617  				for _, file := range test.post.names() { // maintain an order
   618  					content := test.post.content(file)
   619  					if !t.exists(file) {
   620  						t.Errorf("\nmissing file: %s: %q", file, content)
   621  						continue
   622  					}
   623  					if exp, got := content, t.read(file); exp != got {
   624  						t.Errorf("\n%s: -%q +%q", file, exp, got)
   625  					}
   626  				}
   627  
   628  				// Check outputs.
   629  				for _, o := range []struct {
   630  					name     string
   631  					exp, got string
   632  				}{
   633  					{"stdout", trim(test.output.stdout), stdout.String()},
   634  					{"stderr", trim(test.output.stderr), stderr.String()},
   635  				} {
   636  					if o.exp != o.got {
   637  						t.Errorf("\n%s: -%q +%q", o.name, o.exp, o.got)
   638  					}
   639  				}
   640  			})
   641  		}
   642  	})
   643  }
   644  
   645  type files map[string]string
   646  
   647  func (fs files) has(f string) (ok bool) {
   648  	if fs == nil {
   649  		return
   650  	}
   651  	_, ok = fs[f]
   652  	return
   653  }
   654  
   655  func (fs files) content(f string) string {
   656  	if fs == nil {
   657  		return ""
   658  	}
   659  	return trimWhitespace(fs[f], wsBOF, wsBOL)
   660  }
   661  
   662  func (fs files) names() (res []string) {
   663  	for f := range fs {
   664  		res = append(res, f)
   665  	}
   666  	sort.Strings(res)
   667  	return
   668  }
   669  
   670  const printerCode = `
   671  package main
   672  
   673  import (
   674      "os"
   675      "io"
   676      "io/ioutil"
   677      "bytes"
   678  )
   679  
   680  func main() {
   681  	in, _ := ioutil.ReadFile("./STDIN")
   682  	out, _ := ioutil.ReadFile("./STDOUT")
   683  	err, _ := ioutil.ReadFile("./STDERR")
   684  
   685  	if len(in) > 0 {
   686  		io.Copy(os.Stdout, bytes.NewReader(in))
   687  		return
   688  	}
   689  
   690  	if len(out) > 0 {
   691  		io.Copy(os.Stdout, bytes.NewReader(out))
   692  	}
   693  	if len(err) > 0 {
   694  		io.Copy(os.Stderr, bytes.NewReader(err))
   695  	}
   696  }
   697  `
   698  
   699  func newCliTest(t *testing.T) *cliTest {
   700  	ct := &cliTest{
   701  		T:     t,
   702  		reset: func() {},
   703  	}
   704  	return ct
   705  }
   706  
   707  type cliTest struct {
   708  	*testing.T
   709  	reset func()
   710  }
   711  
   712  func (t *cliTest) Run(name string, fn func(*cliTest)) {
   713  	t.T.Run(name, func(T *testing.T) {
   714  		t := newCliTest(T)
   715  		defer t.Reset()
   716  		fn(t)
   717  	})
   718  }
   719  
   720  func (t *cliTest) RunIn(name, dir string, fn func(*cliTest)) {
   721  	t.Run(name, func(t *cliTest) {
   722  		t.mkdir(dir)
   723  		defer t.cd(t.cd(dir))
   724  		fn(t)
   725  	})
   726  }
   727  
   728  func (t *cliTest) In(dir string, fn func(*cliTest)) {
   729  	t.mkdir(dir)
   730  	defer t.Reset()
   731  	defer t.cd(t.cd(dir))
   732  	fn(t)
   733  }
   734  
   735  func (t *cliTest) Reset() {
   736  	if t.reset != nil {
   737  		t.reset()
   738  	}
   739  }
   740  
   741  func (t *cliTest) ensure(do func()) {
   742  	old, new := t.reset, do
   743  	t.reset = func() {
   744  		defer old()
   745  		new()
   746  	}
   747  }
   748  
   749  func (t *cliTest) register(path string) {
   750  	path = filepath.FromSlash(path)
   751  	abs, err := filepath.Abs(path)
   752  	if err != nil {
   753  		t.Fatal(err)
   754  	}
   755  	t.ensure(func() { os.Remove(abs) })
   756  }
   757  
   758  func (t *cliTest) env(k, v string) {
   759  	prev, ok := os.LookupEnv(k)
   760  	if err := os.Setenv(k, v); err != nil {
   761  		t.Fatal(err)
   762  	}
   763  
   764  	var reset func()
   765  	if ok {
   766  		reset = func() { os.Setenv(k, prev) }
   767  	} else {
   768  		reset = func() { os.Unsetenv(k) }
   769  	}
   770  	t.ensure(reset)
   771  }
   772  
   773  func (t *cliTest) pwd() string {
   774  	t.Helper()
   775  	wd, err := os.Getwd()
   776  	if err != nil {
   777  		t.Fatal(err)
   778  	}
   779  	return wd
   780  }
   781  
   782  func (t *cliTest) cd(dir string) string {
   783  	dir = filepath.FromSlash(dir)
   784  	t.Helper()
   785  	wd := t.pwd()
   786  	if err := os.Chdir(dir); err != nil {
   787  		t.Fatal(err)
   788  	}
   789  	return wd
   790  }
   791  
   792  func (t *cliTest) ls(dir string) []string {
   793  	d, err := os.Open(dir)
   794  	if err != nil {
   795  		t.Fatal("ls:", err)
   796  	}
   797  	fs, err := d.Readdirnames(0)
   798  	if err != nil {
   799  		t.Fatal("ls:", err)
   800  	}
   801  	sort.Strings(fs)
   802  	return fs
   803  }
   804  
   805  func (t *cliTest) exists(path string) bool {
   806  	path = filepath.FromSlash(path)
   807  	_, err := os.Stat(path)
   808  	return err == nil
   809  }
   810  
   811  func (t *cliTest) mkdir(dir string) {
   812  	dir = filepath.FromSlash(dir)
   813  	t.Helper()
   814  	if err := os.Mkdir(dir, 0755); err != nil {
   815  		t.Fatal(err)
   816  	}
   817  	t.ensure(func() { os.Remove(dir) })
   818  }
   819  
   820  func (t *cliTest) read(src interface{}) (res string) {
   821  	var (
   822  		bs  []byte
   823  		err error
   824  	)
   825  	switch src := src.(type) {
   826  	case io.Reader:
   827  		bs, err = ioutil.ReadAll(src)
   828  	case string:
   829  		src = filepath.FromSlash(src)
   830  		bs, err = ioutil.ReadFile(src)
   831  		if err == nil {
   832  			t.register(src)
   833  		}
   834  	default:
   835  		t.Errorf("read: invalid type: %T", src)
   836  	}
   837  	if err != nil {
   838  		t.Error(err)
   839  	}
   840  	return string(bs)
   841  }
   842  
   843  func (t *cliTest) write(dst interface{}, content string) {
   844  	var err error
   845  	switch dst := dst.(type) {
   846  	case io.Writer:
   847  		_, err = io.WriteString(dst, content)
   848  	case string: // path
   849  		err = ioutil.WriteFile(dst, []byte(content), 0644)
   850  		if err == nil {
   851  			t.register(dst)
   852  		}
   853  	default:
   854  		t.Errorf("write: invalid type: %T", dst)
   855  	}
   856  	if err != nil {
   857  		t.Fatal(err)
   858  	}
   859  }
   860  
   861  func (t *cliTest) build(file, content string) {
   862  	t.write(file, content)
   863  	cmd := exec.Command("go", "build", file)
   864  	buf := new(bytes.Buffer)
   865  	cmd.Stderr = buf
   866  	if err := cmd.Run(); err != nil {
   867  		t.Fatalf("\nbuild: %s\n%s", err, buf.String())
   868  	}
   869  
   870  	// Remember to delete the binary as well.
   871  	t.register(strings.TrimSuffix(file, filepath.Ext(file)))
   872  }