diff --git a/lib/helpers.go b/lib/helpers.go index e73ee59..4c7ab5c 100644 --- a/lib/helpers.go +++ b/lib/helpers.go @@ -89,7 +89,7 @@ func PrepareColumns(data *Tabdata) error { } // prepare headers: add numbers to headers -func numberizeHeaders(data *Tabdata) { +func numberizeAndReduceHeaders(data *Tabdata) { numberedHeaders := []string{} maxwidth := 0 // start from scratch, so we only look at displayed column widths diff --git a/lib/helpers_test.go b/lib/helpers_test.go index d9d5668..795c00c 100644 --- a/lib/helpers_test.go +++ b/lib/helpers_test.go @@ -158,9 +158,9 @@ func TestNumberizeHeaders(t *testing.T) { UseColumns = tt.columns NoNumbering = tt.nonum usedata := data - numberizeHeaders(&usedata) + numberizeAndReduceHeaders(&usedata) if !reflect.DeepEqual(usedata.headers, tt.expect) { - t.Errorf("numberizeHeaders returned invalid data:\ngot: %+v\nexp: %+v", + t.Errorf("numberizeAndReduceHeaders returned invalid data:\ngot: %+v\nexp: %+v", usedata.headers, tt.expect) } }) diff --git a/lib/io.go b/lib/io.go index 6b12c40..ae37052 100644 --- a/lib/io.go +++ b/lib/io.go @@ -50,7 +50,7 @@ func ProcessFiles(args []string) error { return err } - printData(&data) + printData(os.Stdout, &data) } return nil diff --git a/lib/printer.go b/lib/printer.go index bcb749b..1887e0c 100644 --- a/lib/printer.go +++ b/lib/printer.go @@ -22,20 +22,18 @@ import ( "github.com/gookit/color" "github.com/olekukonko/tablewriter" "gopkg.in/yaml.v3" + "io" "log" - "os" "regexp" "strconv" "strings" ) -func printData(data *Tabdata) { +func printData(w io.Writer, data *Tabdata) string { // some output preparations: - if OutputMode != "shell" { - // not needed in eval string - numberizeHeaders(data) - } + // add numbers to headers and remove this we're not interested in + numberizeAndReduceHeaders(data) // remove unwanted columns, if any reduceColumns(data) @@ -45,26 +43,31 @@ func printData(data *Tabdata) { switch OutputMode { case "extended": - printExtendedData(data) + return printExtendedData(w, data) case "ascii": - printAsciiData(data) + return printAsciiData(w, data) case "orgtbl": - printOrgmodeData(data) + return printOrgmodeData(w, data) case "markdown": - printMarkdownData(data) + return printMarkdownData(w, data) case "shell": - printShellData(data) + return printShellData(w, data) case "yaml": - printYamlData(data) + return printYamlData(w, data) default: - printAsciiData(data) + return printAsciiData(w, data) } } +func output(w io.Writer, str string) string { + fmt.Fprint(w, str) + return str +} + /* Emacs org-mode compatible table (also orgtbl-mode) */ -func printOrgmodeData(data *Tabdata) { +func printOrgmodeData(w io.Writer, data *Tabdata) string { tableString := &strings.Builder{} table := tablewriter.NewWriter(tableString) @@ -90,16 +93,16 @@ func printOrgmodeData(data *Tabdata) { leftR := regexp.MustCompile("(?m)^\\+") rightR := regexp.MustCompile("\\+(?m)$") - color.Print( + return output(w, color.Sprint( colorizeData( rightR.ReplaceAllString( - leftR.ReplaceAllString(tableString.String(), "|"), "|"))) + leftR.ReplaceAllString(tableString.String(), "|"), "|")))) } /* Markdown table */ -func printMarkdownData(data *Tabdata) { +func printMarkdownData(w io.Writer, data *Tabdata) string { tableString := &strings.Builder{} table := tablewriter.NewWriter(tableString) @@ -113,13 +116,13 @@ func printMarkdownData(data *Tabdata) { table.SetCenterSeparator("|") table.Render() - color.Print(colorizeData(tableString.String())) + return output(w, color.Sprint(colorizeData(tableString.String()))) } /* Simple ASCII table without any borders etc, just like the input we expect */ -func printAsciiData(data *Tabdata) { +func printAsciiData(w io.Writer, data *Tabdata) string { tableString := &strings.Builder{} table := tablewriter.NewWriter(tableString) @@ -143,53 +146,48 @@ func printAsciiData(data *Tabdata) { table.SetNoWhiteSpace(true) table.Render() - color.Print(colorizeData(tableString.String())) + return output(w, color.Sprint(colorizeData(tableString.String()))) } /* We simulate the \x command of psql (the PostgreSQL client) */ -func printExtendedData(data *Tabdata) { +func printExtendedData(w io.Writer, data *Tabdata) string { // needed for data output format := fmt.Sprintf("%%%ds: %%s\n", data.maxwidthHeader) - + out := "" if len(data.entries) > 0 { for _, entry := range data.entries { for i, value := range entry { - color.Printf(format, data.headers[i], value) + out += color.Sprintf(format, data.headers[i], value) } - fmt.Println() + out += "\n" } } + + return output(w, out) } /* Shell output, ready to be eval'd. Just like FreeBSD stat(1) */ -func printShellData(data *Tabdata) { +func printShellData(w io.Writer, data *Tabdata) string { + out := "" if len(data.entries) > 0 { - var idx int for _, entry := range data.entries { - idx = 0 shentries := []string{} for i, value := range entry { - if len(Columns) > 0 { - if !contains(UseColumns, i+1) { - continue - } - } - shentries = append(shentries, fmt.Sprintf("%s=\"%s\"", - data.headers[idx], value)) - idx++ + data.headers[i], value)) } - fmt.Println(strings.Join(shentries, " ")) + out += fmt.Sprint(strings.Join(shentries, " ")) + "\n" } } + return output(w, out) } -func printYamlData(data *Tabdata) { +func printYamlData(w io.Writer, data *Tabdata) string { type D struct { Entries []map[string]interface{} `yaml:"entries"` } @@ -222,5 +220,5 @@ func printYamlData(data *Tabdata) { log.Fatal(err) } - os.Stdout.Write(yamlstr) + return output(w, string(yamlstr)) } diff --git a/lib/printer_test.go b/lib/printer_test.go index 2ff0b1e..035c6b1 100644 --- a/lib/printer_test.go +++ b/lib/printer_test.go @@ -18,334 +18,273 @@ along with this program. If not, see . package lib import ( + "bytes" "fmt" - "github.com/gookit/color" - "os" + //"github.com/alecthomas/repr" "strings" "testing" ) -func stdout2pipe(t *testing.T) (*os.File, *os.File) { - reader, writer, err := os.Pipe() - if err != nil { - t.Fatal(err) - } - origStdout := os.Stdout - os.Stdout = writer - - // we need to tell the color mode the io.Writer, even if we don't usw colorization - color.SetOutput(writer) - - return origStdout, reader +var mockdata = Tabdata{ + maxwidthHeader: 8, + maxwidthPerCol: []int{ + 5, + 9, + 3, + 26, + }, + columns: 4, + headers: []string{ + "NAME", + "DURATION", + "COUNT", + "WHEN", + }, + entries: [][]string{ + { + "beta", + "1d10h5m1s", + "33", + "3/1/2014", + }, + { + "alpha", + "4h35m", + "170", + "2013-Feb-03", + }, + { + "ceta", + "33d12h", + "9", + "06/Jan/2008 15:04:05 -0700", + }, + }, } -func TestPrinter(t *testing.T) { - startdata := Tabdata{ - maxwidthHeader: 5, - maxwidthPerCol: []int{ - 5, - 5, - 8, - }, - columns: 3, - headers: []string{ - "ONE", "TWO", "THREE", - }, - entries: [][]string{ - { - "asd", "igig", "cxxxncnc", - }, - { - "19191", "EDD 1", "X", - }, - }, - } +var tests = []struct { + name string // so we can identify which one fails, can be the same + // for multiple tests, because flags will be appended to the name + sortby string // empty == default + column int // sort by this column, 0 == default first or NO Sort + desc bool // sort in descending order, default == ascending + nonum bool // hide numbering + mode string // shell, orgtbl, etc. empty == default: ascii + usecol []int // columns to display, empty == display all + usecolstr string // for testname, must match usecol + expect string // rendered output we expect +}{ + // --------------------- Default settings mode tests `` + { + mode: "ascii", + name: "default", + expect: ` +NAME(1) DURATION(2) COUNT(3) WHEN(4) +beta 1d10h5m1s 33 3/1/2014 +alpha 4h35m 170 2013-Feb-03 +ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`, + }, + { + name: "default", + mode: "orgtbl", + expect: ` +|---------+-------------+----------+----------------------------| +| NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) | +|---------+-------------+----------+----------------------------| +| beta | 1d10h5m1s | 33 | 3/1/2014 | +| alpha | 4h35m | 170 | 2013-Feb-03 | +| ceta | 33d12h | 9 | 06/Jan/2008 15:04:05 -0700 | +|---------+-------------+----------+----------------------------|`, + }, + { + name: "default", + mode: "markdown", + expect: ` +| NAME(1) | DURATION(2) | COUNT(3) | WHEN(4) | +|---------|-------------|----------|----------------------------| +| beta | 1d10h5m1s | 33 | 3/1/2014 | +| alpha | 4h35m | 170 | 2013-Feb-03 | +| ceta | 33d12h | 9 | 06/Jan/2008 15:04:05 -0700 |`, + }, + { + name: "default", + mode: "shell", + nonum: true, + expect: ` +NAME="beta" DURATION="1d10h5m1s" COUNT="33" WHEN="3/1/2014" +NAME="alpha" DURATION="4h35m" COUNT="170" WHEN="2013-Feb-03" +NAME="ceta" DURATION="33d12h" COUNT="9" WHEN="06/Jan/2008 15:04:05 -0700"`, + }, + { + name: "default", + mode: "yaml", + nonum: true, + expect: ` +entries: + - count: 33 + duration: "1d10h5m1s" + name: "beta" + when: "3/1/2014" + - count: 170 + duration: "4h35m" + name: "alpha" + when: "2013-Feb-03" + - count: 9 + duration: "33d12h" + name: "ceta" + when: "06/Jan/2008 15:04:05 -0700"`, + }, + { + name: "default", + mode: "extended", + expect: ` + NAME(1): beta +DURATION(2): 1d10h5m1s + COUNT(3): 33 + WHEN(4): 3/1/2014 - expects := map[string]string{ - "ascii": `ONE(1) TWO(2) THREE(3) -asd igig cxxxncnc -19191 EDD 1 X`, + NAME(1): alpha +DURATION(2): 4h35m + COUNT(3): 170 + WHEN(4): 2013-Feb-03 - "orgtbl": `|--------+--------+----------| -| ONE(1) | TWO(2) | THREE(3) | -|--------+--------+----------| -| asd | igig | cxxxncnc | -| 19191 | EDD 1 | X | -|--------+--------+----------|`, + NAME(1): ceta +DURATION(2): 33d12h + COUNT(3): 9 + WHEN(4): 06/Jan/2008 15:04:05 -0700`, + }, - "markdown": `| ONE(1) | TWO(2) | THREE(3) | -|--------|--------|----------| -| asd | igig | cxxxncnc | -| 19191 | EDD 1 | X |`, + // ----------------------- UseColumns Tests FIXME: when we put + // these tests AFTER the sort tests, then the order of rows + // follows the last sort call (-k 4 here), even if we set + // SortByColumn to 0, which is also the default if unset in the + // test struct. Somehow the SliceStable() seens to modify the data + // structure, which seems impossible, since it's copied during + // every test run. So, I really don't understand the + // issue. However, I'll keep this for the moment, because this + // only seems to be a unit test related issue. In real use, the + // program exits after each call anyway - and it works everytime. - "shell": `ONE="asd" TWO="igig" THREE="cxxxncnc" -ONE="19191" TWO="EDD 1" THREE="X"`, + { + name: "usecolumns", + usecol: []int{2, 4}, + usecolstr: "2,4", + expect: ` +DURATION(2) WHEN(4) +1d10h5m1s 3/1/2014 +4h35m 2013-Feb-03 +33d12h 06/Jan/2008 15:04:05 -0700`, + }, + { + name: "usecolumns", + usecol: []int{1, 4}, + usecolstr: "1,4", + expect: ` +NAME(1) WHEN(4) +beta 3/1/2014 +alpha 2013-Feb-03 +ceta 06/Jan/2008 15:04:05 -0700`, + }, + { + name: "usecolumns", + usecol: []int{2}, + usecolstr: "2", + expect: ` +DURATION(2) +1d10h5m1s +4h35m +33d12h`, + }, + { + name: "usecolumns", + usecol: []int{3}, + usecolstr: "3", + expect: ` +COUNT(3) +33 +170 +9`, + }, + { + name: "usecolumns", + column: 0, + usecol: []int{1, 3}, + usecolstr: "1,3", + expect: ` +NAME(1) COUNT(3) +beta 33 +alpha 170 +ceta 9`, + }, - "extended": `ONE(1): asd - TWO(2): igig -THREE(3): cxxxncnc - - ONE(1): 19191 - TWO(2): EDD 1 -THREE(3): X`, - "yaml": `entries: - - one: "asd" - three: "cxxxncnc" - two: "igig" - - one: 19191 - three: "X" - two: "EDD 1"`, - } - - NoColor = true - SortByColumn = 0 // disable sorting - - origStdout, reader := stdout2pipe(t) - - for mode, expect := range expects { - testname := fmt.Sprintf("print-%s", mode) - t.Run(testname, func(t *testing.T) { - - OutputMode = mode - - if mode == "yaml" { - NoNumbering = true - } else { - NoNumbering = false - } - - // we need to reset our mock data, since it's being - // modified in printData() - data := startdata - printData(&data) - - buf := make([]byte, 1024) - n, err := reader.Read(buf) - if err != nil { - t.Fatal(err) - } - buf = buf[:n] - output := strings.TrimSpace(string(buf)) - - if output != expect { - t.Errorf("output mode: %s, got:\n%s\nwant:\n%s\n (%d <=> %d)", - mode, output, expect, len(output), len(expect)) - } - }) - } - - // Restore - os.Stdout = origStdout - NoNumbering = false -} - -func TestSortPrinter(t *testing.T) { - startdata := Tabdata{ - maxwidthHeader: 5, - maxwidthPerCol: []int{ - 3, - 3, - 2, - }, - columns: 3, - headers: []string{ - "ONE", "TWO", "THREE", - }, - entries: [][]string{ - { - "abc", "345", "b1", - }, - { - "bcd", "234", "a2", - }, - { - "cde", "123", "c3", - }, - }, - } - - var tests = []struct { - data Tabdata - sortby int - desc bool - expect string - }{ - { - data: startdata, - sortby: 1, - desc: false, - expect: `ONE(1) TWO(2) THREE(3) -abc 345 b1 -bcd 234 a2 -cde 123 c3`, - }, - - { - data: startdata, - sortby: 2, - desc: false, - expect: `ONE(1) TWO(2) THREE(3) -cde 123 c3 -bcd 234 a2 -abc 345 b1`, - }, - - { - data: startdata, - sortby: 3, - desc: false, - expect: `ONE(1) TWO(2) THREE(3) -bcd 234 a2 -abc 345 b1 -cde 123 c3`, - }, - { - data: startdata, - sortby: 1, - desc: true, - expect: `ONE(1) TWO(2) THREE(3) -cde 123 c3 -bcd 234 a2 -abc 345 b1`, - }, - } - - NoColor = true - OutputMode = "ascii" - origStdout, reader := stdout2pipe(t) - - for _, tt := range tests { - testname := fmt.Sprintf("print-sorted-table-by-column-%d-desc-%t", - tt.sortby, tt.desc) - t.Run(testname, func(t *testing.T) { - SortByColumn = tt.sortby - SortDescending = tt.desc - - printData(&tt.data) - - buf := make([]byte, 1024) - n, err := reader.Read(buf) - if err != nil { - t.Fatal(err) - } - buf = buf[:n] - output := strings.TrimSpace(string(buf)) - - if output != tt.expect { - t.Errorf("sort column: %d, got:\n%s\nwant:\n%s", - tt.sortby, output, tt.expect) - } - }) - } - - // Restore - os.Stdout = origStdout -} - -func TestSortByPrinter(t *testing.T) { - data := Tabdata{ - maxwidthHeader: 8, - maxwidthPerCol: []int{ - 5, - 9, - 3, - 26, - }, - columns: 4, - headers: []string{ - "NAME", - "DURATION", - "COUNT", - "WHEN", - }, - entries: [][]string{ - { - "beta", - "1d10h5m1s", - "33", - "3/1/2014", - }, - { - "alpha", - "4h35m", - "170", - "2013-Feb-03", - }, - { - "ceta", - "33d12h", - "9", - "06/Jan/2008 15:04:05 -0700", - }, - }, - } - - var tests = []struct { - sortby string - column int - desc bool - expect string - }{ - { - column: 3, - sortby: "numeric", - desc: false, - expect: `NAME(1) DURATION(2) COUNT(3) WHEN(4) + //------------------------ SORT TESTS + { + name: "sortbycolumn", + column: 3, + sortby: "numeric", + desc: false, + expect: ` +NAME(1) DURATION(2) COUNT(3) WHEN(4) ceta 33d12h 9 06/Jan/2008 15:04:05 -0700 beta 1d10h5m1s 33 3/1/2014 alpha 4h35m 170 2013-Feb-03`, - }, - { - column: 2, - sortby: "duration", - desc: false, - expect: `NAME(1) DURATION(2) COUNT(3) WHEN(4) + }, + { + name: "sortbycolumn", + column: 2, + sortby: "duration", + desc: false, + expect: ` +NAME(1) DURATION(2) COUNT(3) WHEN(4) alpha 4h35m 170 2013-Feb-03 beta 1d10h5m1s 33 3/1/2014 ceta 33d12h 9 06/Jan/2008 15:04:05 -0700`, - }, - { - column: 4, - sortby: "time", - desc: false, - expect: `NAME(1) DURATION(2) COUNT(3) WHEN(4) + }, + { + name: "sortbycolumn", + column: 4, + sortby: "time", + desc: false, + expect: ` +NAME(1) DURATION(2) COUNT(3) WHEN(4) ceta 33d12h 9 06/Jan/2008 15:04:05 -0700 alpha 4h35m 170 2013-Feb-03 beta 1d10h5m1s 33 3/1/2014`, - }, - } + }, +} +func TestPrinter(t *testing.T) { NoColor = true - OutputMode = "ascii" - origStdout, reader := stdout2pipe(t) for _, tt := range tests { - testname := fmt.Sprintf("print-sorted-table-by-column-%d-desc-%t-sort-by-%s", - tt.column, tt.desc, tt.sortby) - + testname := fmt.Sprintf("print-sortcol-%d-desc-%t-sortby-%s-mode-%s-usecolumns-%s", + tt.column, tt.desc, tt.sortby, tt.mode, tt.usecolstr) t.Run(testname, func(t *testing.T) { + // replaces os.Stdout, but we ignore it + var w bytes.Buffer + + // cmd flags SortByColumn = tt.column SortDescending = tt.desc SortMode = tt.sortby + OutputMode = tt.mode + NoNumbering = tt.nonum + UseColumns = tt.usecol - testdata := data - printData(&testdata) - - buf := make([]byte, 1024) - n, err := reader.Read(buf) - if err != nil { - t.Fatal(err) + // the test checks the len! + if len(tt.usecol) > 0 { + Columns = "yes" + } else { + Columns = "" } - buf = buf[:n] - output := strings.TrimSpace(string(buf)) - if output != tt.expect { - t.Errorf("sort column: %d, sortby: %s, got:\n%s\nwant:\n%s", - tt.column, tt.sortby, output, tt.expect) + testdata := mockdata + output := strings.TrimSpace(printData(&w, &testdata)) + exp := strings.TrimSpace(tt.expect) + if output != exp { + t.Errorf("not rendered correctly:\n+++ got:\n%s\n+++ want:\n%s", + output, exp) } }) } - - // Restore - os.Stdout = origStdout } diff --git a/lib/sort.go b/lib/sort.go index 486716e..90e4836 100644 --- a/lib/sort.go +++ b/lib/sort.go @@ -25,7 +25,7 @@ import ( ) func sortTable(data *Tabdata, col int) { - if col <= 0 { + if SortByColumn <= 0 { // no sorting wanted return }