...

Source file src/gotest.tools/v3/internal/format/diff.go

Documentation: gotest.tools/v3/internal/format

     1  // Package format provides utilities for formatting diffs and messages.
     2  package format
     3  
     4  import (
     5  	"bytes"
     6  	"fmt"
     7  	"strings"
     8  	"unicode"
     9  
    10  	"gotest.tools/v3/internal/difflib"
    11  )
    12  
    13  const (
    14  	contextLines = 2
    15  )
    16  
    17  // DiffConfig for a unified diff
    18  type DiffConfig struct {
    19  	A    string
    20  	B    string
    21  	From string
    22  	To   string
    23  }
    24  
    25  // UnifiedDiff is a modified version of difflib.WriteUnifiedDiff with better
    26  // support for showing the whitespace differences.
    27  func UnifiedDiff(conf DiffConfig) string {
    28  	a := strings.SplitAfter(conf.A, "\n")
    29  	b := strings.SplitAfter(conf.B, "\n")
    30  	groups := difflib.NewMatcher(a, b).GetGroupedOpCodes(contextLines)
    31  	if len(groups) == 0 {
    32  		return ""
    33  	}
    34  
    35  	buf := new(bytes.Buffer)
    36  	writeFormat := func(format string, args ...interface{}) {
    37  		buf.WriteString(fmt.Sprintf(format, args...))
    38  	}
    39  	writeLine := func(prefix string, s string) {
    40  		buf.WriteString(prefix + s)
    41  	}
    42  	if hasWhitespaceDiffLines(groups, a, b) {
    43  		writeLine = visibleWhitespaceLine(writeLine)
    44  	}
    45  	formatHeader(writeFormat, conf)
    46  	for _, group := range groups {
    47  		formatRangeLine(writeFormat, group)
    48  		for _, opCode := range group {
    49  			in, out := a[opCode.I1:opCode.I2], b[opCode.J1:opCode.J2]
    50  			switch opCode.Tag {
    51  			case 'e':
    52  				formatLines(writeLine, " ", in)
    53  			case 'r':
    54  				formatLines(writeLine, "-", in)
    55  				formatLines(writeLine, "+", out)
    56  			case 'd':
    57  				formatLines(writeLine, "-", in)
    58  			case 'i':
    59  				formatLines(writeLine, "+", out)
    60  			}
    61  		}
    62  	}
    63  	return buf.String()
    64  }
    65  
    66  // hasWhitespaceDiffLines returns true if any diff groups is only different
    67  // because of whitespace characters.
    68  func hasWhitespaceDiffLines(groups [][]difflib.OpCode, a, b []string) bool {
    69  	for _, group := range groups {
    70  		in, out := new(bytes.Buffer), new(bytes.Buffer)
    71  		for _, opCode := range group {
    72  			if opCode.Tag == 'e' {
    73  				continue
    74  			}
    75  			for _, line := range a[opCode.I1:opCode.I2] {
    76  				in.WriteString(line)
    77  			}
    78  			for _, line := range b[opCode.J1:opCode.J2] {
    79  				out.WriteString(line)
    80  			}
    81  		}
    82  		if removeWhitespace(in.String()) == removeWhitespace(out.String()) {
    83  			return true
    84  		}
    85  	}
    86  	return false
    87  }
    88  
    89  func removeWhitespace(s string) string {
    90  	var result []rune
    91  	for _, r := range s {
    92  		if !unicode.IsSpace(r) {
    93  			result = append(result, r)
    94  		}
    95  	}
    96  	return string(result)
    97  }
    98  
    99  func visibleWhitespaceLine(ws func(string, string)) func(string, string) {
   100  	mapToVisibleSpace := func(r rune) rune {
   101  		switch r {
   102  		case '\n':
   103  		case ' ':
   104  			return '·'
   105  		case '\t':
   106  			return '▷'
   107  		case '\v':
   108  			return '▽'
   109  		case '\r':
   110  			return '↵'
   111  		case '\f':
   112  			return '↓'
   113  		default:
   114  			if unicode.IsSpace(r) {
   115  				return '�'
   116  			}
   117  		}
   118  		return r
   119  	}
   120  	return func(prefix, s string) {
   121  		ws(prefix, strings.Map(mapToVisibleSpace, s))
   122  	}
   123  }
   124  
   125  func formatHeader(wf func(string, ...interface{}), conf DiffConfig) {
   126  	if conf.From != "" || conf.To != "" {
   127  		wf("--- %s\n", conf.From)
   128  		wf("+++ %s\n", conf.To)
   129  	}
   130  }
   131  
   132  func formatRangeLine(wf func(string, ...interface{}), group []difflib.OpCode) {
   133  	first, last := group[0], group[len(group)-1]
   134  	range1 := formatRangeUnified(first.I1, last.I2)
   135  	range2 := formatRangeUnified(first.J1, last.J2)
   136  	wf("@@ -%s +%s @@\n", range1, range2)
   137  }
   138  
   139  // Convert range to the "ed" format
   140  func formatRangeUnified(start, stop int) string {
   141  	// Per the diff spec at http://www.unix.org/single_unix_specification/
   142  	beginning := start + 1 // lines start numbering with one
   143  	length := stop - start
   144  	if length == 1 {
   145  		return fmt.Sprintf("%d", beginning)
   146  	}
   147  	if length == 0 {
   148  		beginning-- // empty ranges begin at line just before the range
   149  	}
   150  	return fmt.Sprintf("%d,%d", beginning, length)
   151  }
   152  
   153  func formatLines(writeLine func(string, string), prefix string, lines []string) {
   154  	for _, line := range lines {
   155  		writeLine(prefix, line)
   156  	}
   157  	// Add a newline if the last line is missing one so that the diff displays
   158  	// properly.
   159  	if !strings.HasSuffix(lines[len(lines)-1], "\n") {
   160  		writeLine("", "\n")
   161  	}
   162  }
   163  

View as plain text