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 }