-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
510 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
load("@bazel_gazelle//:def.bzl", "gazelle") | ||
|
||
# gazelle:prefix github.com/jsannemo/omogenexec | ||
gazelle(name = "gazelle") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module github.com/jsannemo/omogenexec | ||
|
||
go 1.16 | ||
|
||
require github.com/google/logger v1.1.1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ= | ||
github.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ= | ||
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 h1:dXfMednGJh/SUUFjTLsWJz3P+TQt9qnR11GgeI3vWKs= | ||
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") | ||
|
||
go_library( | ||
name = "diff", | ||
srcs = ["diff.go"], | ||
importpath = "github.com/jsannemo/omogenexec/runner/diff", | ||
visibility = ["//visibility:public"], | ||
deps = ["@com_github_google_logger//:logger"], | ||
) | ||
|
||
go_test( | ||
name = "diff_test", | ||
srcs = ["diff_test.go"], | ||
embed = [":diff"], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
// Package diff implements a structural, non-exact diff of two strings. | ||
// | ||
// The default mode is that the strings are compared in a token-by-token manner, where for example | ||
// tokens that can be parsed as floating-point integers are parsed as such according to some common | ||
// formats | ||
package diff | ||
|
||
import ( | ||
"bufio" | ||
"fmt" | ||
"github.com/google/logger" | ||
"io" | ||
"math" | ||
"strings" | ||
) | ||
|
||
// DiffResult describes a comparison of two strings. | ||
type DiffResult struct { | ||
// Whether the strings matched. | ||
Match bool | ||
// A textual description of the difference. | ||
Description string | ||
} | ||
|
||
// DiffArgs specifies rules | ||
type DiffArgs struct { | ||
// Whether tokens representing floating point integers should be parsed as such and compared | ||
// using precisions RelativePrec and AbsolutePrec rather than as strings. | ||
ParseFloats bool | ||
// Indicates that floating-point tokens should be accepted if they are within relative error ≤ ε | ||
RelativePrec float64 | ||
// Indicates that floating-point tokens should be accepted if they are within absolute error ≤ ε | ||
AbsolutePrec float64 | ||
// Indicates that comparisons should be case-sensitive | ||
CaseSensitive bool | ||
// Indicates that changes in the amount of whitespace should be rejected (the default is that | ||
// any sequence of 1 or more whitespace characters are equivalent) | ||
SpaceSensitive bool | ||
} | ||
|
||
// Diff compares the a Reader against a "correct" reference Reader by tokenizing them. | ||
func Diff(reference, output io.Reader, args DiffArgs) (*DiffResult, error) { | ||
ref := newPositionedScanner(bufio.NewReader(reference), args.SpaceSensitive) | ||
out := newPositionedScanner(bufio.NewReader(output), args.SpaceSensitive) | ||
for { | ||
refToken, err := ref.Scan() | ||
if err == io.EOF { | ||
break | ||
} else if err != nil { | ||
return nil, err | ||
} | ||
refStr := string(refToken.Token) | ||
outToken, err := out.Scan() | ||
if err == io.EOF { | ||
return &DiffResult{false, fmt.Sprintf("Expected more output (next reference token: %s at %v)", refStr, refToken.Pos)}, nil | ||
} else if err != nil { | ||
return nil, err | ||
} | ||
outStr := string(outToken.Token) | ||
match, desc := matchToken(refStr, outStr, args) | ||
if !match { | ||
return &DiffResult{false, fmt.Sprintf("%s (output %v, reference %v)", desc, outToken.Pos, refToken.Pos)}, nil | ||
} | ||
} | ||
outToken, err := out.Scan() | ||
if err != io.EOF { | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &DiffResult{false, fmt.Sprintf("Too much output (next output token: %s at %v)", string(outToken.Token), outToken.Pos)}, nil | ||
} | ||
return &DiffResult{Match: true}, nil | ||
} | ||
|
||
func matchToken(ref, out string, args DiffArgs) (bool, string) { | ||
logger.Infof("diff %s %s", ref, out) | ||
if args.ParseFloats { | ||
var refFloat, outFloat float64 | ||
strVal := "" | ||
if scanned, _ := fmt.Sscanf(ref, "%f%s", &refFloat, &strVal); scanned == 1 { | ||
if scanned, _ := fmt.Sscanf(out, "%f%s", &outFloat, &strVal); scanned != 1 { | ||
return false, fmt.Sprintf("Reference was decimal, output was: %s", out) | ||
} | ||
logger.Infof("got decimals %f %f", refFloat, outFloat) | ||
diff := math.Abs(refFloat - outFloat) | ||
logger.Infof("float diff %f", diff) | ||
if !(diff <= args.AbsolutePrec) && | ||
!(diff <= args.RelativePrec*math.Abs(refFloat)) { | ||
return false, fmt.Sprintf( | ||
"Too large decimal difference. Reference: %s, output: %s, difference: %f (absolute difference > %f and relative difference > %f)", | ||
ref, out, diff, args.AbsolutePrec, args.RelativePrec) | ||
} | ||
return true, "" | ||
} | ||
} | ||
diff := !strings.EqualFold(ref, out) | ||
logger.Infof("diff w/o case? %v", diff) | ||
if diff { | ||
return false, fmt.Sprintf("Output was %s, expected %s", out, ref) | ||
} | ||
if args.CaseSensitive && ref != out { | ||
return false, fmt.Sprintf("Output was %s, expected %s (difference in casing)", out, ref) | ||
} | ||
return true, "" | ||
} | ||
|
||
type position struct { | ||
Line int | ||
Col int | ||
} | ||
|
||
func (p *position) String() string { | ||
return fmt.Sprintf("%d:%d", p.Line, p.Col) | ||
} | ||
|
||
type positionedScanner struct { | ||
reader *bufio.Reader | ||
spaceSensitive bool | ||
pos position | ||
} | ||
|
||
func newPositionedScanner(reader *bufio.Reader, spaceSensitive bool) positionedScanner { | ||
return positionedScanner{ | ||
reader: reader, | ||
spaceSensitive: spaceSensitive, | ||
pos: position{ | ||
Line: 1, | ||
Col: 1, | ||
}, | ||
} | ||
} | ||
|
||
type posToken struct { | ||
Pos position | ||
Token []byte | ||
} | ||
|
||
func (sc *positionedScanner) Scan() (*posToken, error) { | ||
// Fast-forward through spaces | ||
if !sc.spaceSensitive { | ||
for { | ||
nextByte, err := sc.peekByte() | ||
if err != nil { | ||
return nil, err | ||
} | ||
if isSpace(nextByte) { | ||
if _, err := sc.eatByte(); err != nil { | ||
return nil, err | ||
} | ||
} else { | ||
break | ||
} | ||
} | ||
} | ||
token := &posToken{ | ||
Pos: sc.pos, | ||
Token: make([]byte, 0), | ||
} | ||
for { | ||
nextByte, err := sc.peekByte() | ||
if err == io.EOF { | ||
if len(token.Token) == 0 { | ||
return token, io.EOF | ||
} else { | ||
return token, nil | ||
} | ||
} else if err != nil { | ||
return nil, err | ||
} | ||
if isSpace(nextByte) { | ||
if !sc.spaceSensitive { | ||
if len(token.Token) == 0 { | ||
logger.Fatal("Peeked space after empty token!?") | ||
} | ||
return token, nil | ||
} else { | ||
// If we're space sensitive, single spaces are tokens, but spaces should never be | ||
// included with a token if we had non-space chars already | ||
if len(token.Token) == 0 { | ||
if _, err := sc.eatByte(); err != nil { | ||
return nil, err | ||
} | ||
token.Token = []byte{nextByte} | ||
} | ||
return token, nil | ||
} | ||
} else { | ||
if _, err := sc.eatByte(); err != nil { | ||
return nil, err | ||
} | ||
token.Token = append(token.Token, nextByte) | ||
} | ||
} | ||
} | ||
|
||
func (sc *positionedScanner) peekByte() (byte, error) { | ||
peek, err := sc.reader.Peek(1) | ||
if err != nil { | ||
return 0, err | ||
} | ||
return peek[0], nil | ||
} | ||
|
||
func (sc *positionedScanner) eatByte() (byte, error) { | ||
nextByte, err := sc.reader.ReadByte() | ||
if err != nil { | ||
return 0, err | ||
} | ||
if nextByte == '\n' { | ||
sc.pos.Line++ | ||
sc.pos.Col = 1 | ||
} else { | ||
sc.pos.Col++ | ||
} | ||
return nextByte, nil | ||
} | ||
|
||
func isSpace(b byte) bool { | ||
return (9 <= b && b <= 13) || b == 32 | ||
} |
Oops, something went wrong.