Skip to content

Commit

Permalink
Improve markdown output formatting (#75)
Browse files Browse the repository at this point in the history
  • Loading branch information
mfridman authored Jul 5, 2022
1 parent c26c97f commit e2da1ad
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 65 deletions.
18 changes: 8 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches:
- main
pull_request:
types: [opened, synchronize, reopened]

jobs:
build:
Expand All @@ -29,16 +30,13 @@ jobs:
run: go build -v .

- name: Run tests with GITHUB_STEP_SUMMARY
run: |
go test -count=1 -race ./... -json -coverpkg github.com/mfridman/tparse/parse | tee output.json | ./tparse -notests -follow -all
NO_COLOR=1 ./tparse -format markdown -file output.json -all -slow 5 > $GITHUB_STEP_SUMMARY
# Note the use of || true. This so the job doesn't fail at that line. We want to preserve -follow
# as part of the test output, but not output it to the summary page, which is done in the proceeding
# command when we parse the output.json file.
run: |
go test -count=1 -race ./... -json -coverpkg github.com/mfridman/tparse/parse \
| tee output.json | ./tparse -notests -follow -all || true
./tparse -format markdown -file output.json -all -slow 5 > $GITHUB_STEP_SUMMARY
- name: Run tparse w/ std lib
run: go test -count=1 fmt strings bytes bufio crypto log mime sort time -json -cover | ./tparse -follow -all

# - name: Run tparse w/ std lib (GITHUB_STEP_SUMMARY)
# env:
# NO_COLOR: 1
# run: |
# go test -count=1 fmt strings bytes bufio crypto log mime sort time -json -cover |\
# ./tparse -format=markdown >> $GITHUB_STEP_SUMMARY
67 changes: 52 additions & 15 deletions internal/app/console_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package app

import (
"io"
"os"
"strconv"

"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
Expand All @@ -27,15 +29,26 @@ type consoleWriter struct {
yellow colorOptionFunc
}

type colorOptionFunc func(s string, bold bool) string
type colorOptionFunc func(s string) string

// newColor is a helper function to set the base color.
func newColor(color lipgloss.TerminalColor) colorOptionFunc {
return func(text string, bold bool) string {
return lipgloss.NewStyle().Bold(bold).Foreground(color).Render(text)
return func(text string) string {
return lipgloss.NewStyle().Foreground(color).Render(text)
}
}

// newMarkdownColor is a helper function to set the base color for markdown.
func newMarkdownColor(s string) colorOptionFunc {
return func(text string) string {
return s + " " + text
}
}

func noColor() colorOptionFunc {
return func(text string) string { return text }
}

func newConsoleWriter(w io.Writer, format OutputFormat, disableColor bool) *consoleWriter {
if format == 0 {
format = OutputFormatBasic
Expand All @@ -44,18 +57,42 @@ func newConsoleWriter(w io.Writer, format OutputFormat, disableColor bool) *cons
w: w,
format: format,
}
if disableColor {
cw.red = newColor(lipgloss.NoColor{})
cw.green = newColor(lipgloss.NoColor{})
cw.yellow = newColor(lipgloss.NoColor{})
} else {
// TODO(mf): not sure why I have to do this. It's working just fine locally but in
// CI (GitHub Actions) it is not outputting with colors.
// https://github.com/charmbracelet/lipgloss/issues/74
lipgloss.SetColorProfile(termenv.TrueColor)
cw.red = newColor(lipgloss.Color("9"))
cw.green = newColor(lipgloss.Color("10"))
cw.yellow = newColor(lipgloss.Color("11"))
cw.red = noColor()
cw.green = noColor()
cw.yellow = noColor()

if !disableColor {
// NOTE(mf): The GitHub Actions CI env (and probably others) does not have an
// interactive TTY, and tparse will degrade to the "best available option" ..
// which is no colors. We can work around this by setting the color profile
// manually instead of relying on it to auto-detect.
// Ref: https://github.com/charmbracelet/lipgloss/issues/74
//
// TODO(mf): Should this be an explicit env variable instead? Such as TPARSE_FORCE_COLOR
//
// For now we best-effort the most common CI environments and set this manually.
if isCIEnvironment() {
lipgloss.SetColorProfile(termenv.TrueColor)
}
switch format {
case OutputFormatMarkdown:
cw.green = newMarkdownColor("🟢")
cw.yellow = newMarkdownColor("🟡")
cw.red = newMarkdownColor("🔴")
default:
cw.green = newColor(lipgloss.Color("10"))
cw.yellow = newColor(lipgloss.Color("11"))
cw.red = newColor(lipgloss.Color("9"))
}
}
return cw
}

func isCIEnvironment() bool {
if s := os.Getenv("CI"); s != "" {
if ok, err := strconv.ParseBool(s); err == nil && ok {
return true
}
}
return false
}
120 changes: 95 additions & 25 deletions internal/app/table_failed.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,17 @@ func (c *consoleWriter) printFailed(packages []*parse.Package) {
if pkg.HasPanic {
// TODO(mf): document why panics are handled separately. A panic may or may
// not be associated with tests, so we print it at the package level.
output := prepareStyledPanic(pkg.Summary.Package, pkg.Summary.Test, pkg.PanicEvents)
output := c.prepareStyledPanic(pkg.Summary.Package, pkg.Summary.Test, pkg.PanicEvents)
fmt.Fprintln(c.w, output)
continue
}
failedTests := pkg.TestsByAction(parse.ActionFail)
if len(failedTests) == 0 {
continue
}

styledPackageHeader := styledHeader(
strings.ToUpper(pkg.Summary.Action.String()),
strings.TrimSpace(pkg.Summary.Package),
styledPackageHeader := c.styledHeader(
pkg.Summary.Action.String(),
pkg.Summary.Package,
)
fmt.Fprintln(c.w, styledPackageHeader)
fmt.Fprintln(c.w)
Expand All @@ -43,9 +42,29 @@ func (c *consoleWriter) printFailed(packages []*parse.Package) {
divider := lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderTop(true).
Faint(true).
Faint(c.format != OutputFormatMarkdown).
Width(96)

/*
Note, some output such as the "--- FAIL: " line is prefixed
with spaces. Unfortunately when dumping this in markdown format
it renders as an code block.
"To produce a code block in Markdown, simply indent every line of the
block by at least 4 spaces or 1 tab."
Ref. https://daringfireball.net/projects/markdown/syntax
Example:
--- FAIL: Test (0.05s)
--- FAIL: Test/test_01 (0.01s)
--- FAIL: Test/test_01/sort (0.00s)
This is why we wrap the entire test output in a code block.
*/

if c.format == OutputFormatMarkdown {
fmt.Fprintln(c.w, fencedCodeBlock)
}
var key string
for i, t := range failedTests {
// Add top divider to all tests except first one.
Expand All @@ -54,11 +73,18 @@ func (c *consoleWriter) printFailed(packages []*parse.Package) {
fmt.Fprintln(c.w, divider.String())
}
key = base
fmt.Fprintln(c.w, prepareStyledTest(t))
fmt.Fprintln(c.w, c.prepareStyledTest(t))
}
if c.format == OutputFormatMarkdown {
fmt.Fprint(c.w, fencedCodeBlock+"\n\n")
}
}
}

const (
fencedCodeBlock string = "```"
)

// copied directly from strings.Cut (go1.18) to support older Go versions.
// In the future, replace this with the upstream function.
func cut(s, sep string) (before, after string, found bool) {
Expand All @@ -68,14 +94,15 @@ func cut(s, sep string) (before, after string, found bool) {
return s, "", false
}

func prepareStyledPanic(packageName, testName string, panicEvents []*parse.Event) string {
func (c *consoleWriter) prepareStyledPanic(
packageName string,
testName string,
panicEvents []*parse.Event,
) string {
if testName != "" {
packageName = packageName + " • " + testName
}
styledPackageHeader := styledHeader(
"PANIC",
packageName,
)
styledPackageHeader := c.styledHeader("PANIC", packageName)
// TODO(mf): can we pass this panic stack to another package and either by default,
// or optionally, build human-readable panic output with:
// https://github.com/maruel/panicparse
Expand All @@ -89,20 +116,35 @@ func prepareStyledPanic(packageName, testName string, panicEvents []*parse.Event
return lipgloss.JoinVertical(lipgloss.Left, styledPackageHeader, rows.String())
}

// styledHeader styles a header based on the status and package name:
//
// ╭───────────────────────────────────────────────────────────╮
// │ PANIC package: github.com/pressly/goose/v3/tests/e2e │
// ╰───────────────────────────────────────────────────────────╯
//
func styledHeader(status, packageName string) string {
func (c *consoleWriter) styledHeader(status, packageName string) string {
status = c.red(strings.ToUpper(status))
packageName = strings.TrimSpace(packageName)

if c.format == OutputFormatMarkdown {
msg := fmt.Sprintf("## %s • %s", status, packageName)
return msg
// TODO(mf): an alternative implementation is to add 2 horizontal lines above and below
// the package header output.
//
// var divider string
// for i := 0; i < len(msg); i++ {
// divider += "─"
// }
// return fmt.Sprintf("%s\n%s\n%s", divider, msg, divider)
}
/*
Need to rethink how to best support multiple output formats across
CI, local terminal development and markdown
See https://github.com/mfridman/tparse/issues/71
*/
headerStyle := lipgloss.NewStyle().
BorderStyle(lipgloss.ThickBorder()).
BorderForeground(lipgloss.Color("103"))
statusStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("9")).
PaddingLeft(3).
PaddingRight(2)
PaddingRight(2).
Foreground(lipgloss.Color("9"))
packageNameStyle := lipgloss.NewStyle().
PaddingRight(3)
headerRow := lipgloss.JoinHorizontal(
Expand All @@ -113,7 +155,11 @@ func styledHeader(status, packageName string) string {
return headerStyle.Render(headerRow)
}

func prepareStyledTest(t *parse.Test) string {
const (
failLine = "--- FAIL: "
)

func (c *consoleWriter) prepareStyledTest(t *parse.Test) string {
t.SortEvents()

var rows, headerRows strings.Builder
Expand All @@ -124,18 +170,42 @@ func prepareStyledTest(t *parse.Test) string {
if e.Action != parse.ActionOutput {
continue
}
if strings.Contains(e.Output, "--- FAIL: ") {
header := lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Render(e.Output)
if strings.Contains(e.Output, failLine) {
header := strings.TrimSuffix(e.Output, "\n")
// go test prefixes too much padding to the "--- FAIL: " output lines.
// Let's cut the padding by half, being careful to preserve the fail
// line and the proceeding output.
before, after, ok := cut(header, failLine)
var pad string
if ok {
var n int
for _, r := range before {
if r == 32 {
n++
}
}
for i := 0; i < n/2; i++ {
pad += " "
}
}
header = pad + failLine + after

// Avoid colorizing markdown output so it renders properly, otherwise add a subtle
// red color to the test headers.
if c.format != OutputFormatMarkdown {
header = lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Render(header)
}
headerRows.WriteString(header)
continue
}

if e.Output != "" {
rows.WriteString(e.Output)
}
}
out := headerRows.String()
if rows.Len() > 0 {
out += "\n" + rows.String()
out += "\n\n" + rows.String()
}
return out
}
Loading

0 comments on commit e2da1ad

Please sign in to comment.