diff --git a/.github/workflows/denoise-test.yml b/.github/workflows/denoise-test.yml new file mode 100644 index 0000000000..d6c3c61c6d --- /dev/null +++ b/.github/workflows/denoise-test.yml @@ -0,0 +1,21 @@ +name: Golang On Linux +on: [pull_request] +jobs: + golang_linux: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Build binary + run: | + go build -race -tags=viper_bind_struct -o keployv2 + - name: Checkout the samples-go repository + uses: actions/checkout@v2 + with: + repository: Sarthak160/goapi + path: goapi + - name: Run samples-go application + run: | + cd goapi + source ./../../.github/workflows/test_workflow_scripts/golang-linux.sh \ No newline at end of file diff --git a/.github/workflows/test_workflow_scripts/denoise-test.sh b/.github/workflows/test_workflow_scripts/denoise-test.sh new file mode 100644 index 0000000000..c4516d50ac --- /dev/null +++ b/.github/workflows/test_workflow_scripts/denoise-test.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +source ./../../.github/workflows/test_workflow_scripts/test-iid.sh + +# Checkout a different branch +git fetch origin +git checkout native-linux + + +# Build the binary. +go build -o goapi + + +# Start the goapi app in test mode. +sudo -E env PATH="$PATH" ./../../keployv2 test -c "./goapi" --delay 7 &> test_logs.txt + +if grep "ERROR" "test_logs.txt"; then + echo "Error found in pipeline..." + cat "test_logs.txt" + exit 1 +fi + +if grep "WARNING: DATA RACE" "test_logs.txt"; then + echo "Race condition detected in test, stopping pipeline..." + cat "test_logs.txt" + exit 1 +fi + +all_passed=true + + +# Get the test results from the testReport file. +for i in {0..1} +do + # Define the report file for each test set + report_file="./keploy/reports/test-run-0/test-set-$i-report.yaml" + + # Extract the test status + test_status=$(grep 'status:' "$report_file" | head -n 1 | awk '{print $2}') + + # Print the status for debugging + echo "Test status for test-set-$i: $test_status" + + # Check if any test set did not pass + if [ "$test_status" != "PASSED" ]; then + all_passed=false + echo "Test-set-$i did not pass." + break # Exit the loop early as all tests need to pass + fi +done + +# Check the overall test status and exit accordingly +if [ "$all_passed" = true ]; then + echo "All tests passed" + exit 0 +else + cat "test_logs.txt" + exit 1 +fi \ No newline at end of file diff --git a/go.mod b/go.mod index db9f9f9738..82091ffab8 100755 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22.0 replace github.com/jackc/pgproto3/v2 => github.com/keploy/pgproto3/v2 v2.0.5 +replace github.com/keploy/jsonDiff v1.0.3 => /Users/sarthak_1/Documents/Keploy/Lima-workspace/jsonDiff require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/cilium/ebpf v0.13.2 @@ -12,7 +13,7 @@ require ( github.com/docker/docker v24.0.4+incompatible github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/fatih/color v1.16.0 + github.com/fatih/color v1.17.0 github.com/k0kubun/pp/v3 v3.2.0 github.com/miekg/dns v1.1.55 github.com/moby/term v0.5.0 // indirect @@ -58,7 +59,7 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/tidwall/gjson v1.17.0 // indirect + github.com/tidwall/gjson v1.17.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect @@ -115,6 +116,7 @@ require ( github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/uuid v1.6.0 github.com/jackc/pgproto3/v2 v2.3.2 + github.com/keploy/jsonDiff v1.0.3 github.com/shirou/gopsutil/v3 v3.24.3 github.com/spf13/viper v1.19.0 github.com/wI2L/jsondiff v0.5.0 diff --git a/go.sum b/go.sum index 56768491b0..55ce713f80 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -130,6 +130,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/k0kubun/pp/v3 v3.2.0 h1:h33hNTZ9nVFNP3u2Fsgz8JXiF5JINoZfFq4SvKJwNcs= github.com/k0kubun/pp/v3 v3.2.0/go.mod h1:ODtJQbQcIRfAD3N+theGCV1m/CBxweERz2dapdz1EwA= +github.com/keploy/jsonDiff v1.0.3 h1:MPSZwbHgTFuxV1eLUaD/sg2e57cKlRgamOBnO0oj3ec= +github.com/keploy/jsonDiff v1.0.3/go.mod h1:wUuLbVs3Oe3mIQ61C7G88bppP//ArLtoDU0S9Awwv+s= github.com/keploy/pgproto3/v2 v2.0.5 h1:8spdNKZ+nOnHVxiimDsqulBRN6viPXPghkA7xppnzJ8= github.com/keploy/pgproto3/v2 v2.0.5/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -275,8 +277,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= -github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= diff --git a/pkg/matcher/utils.go b/pkg/matcher/utils.go index fea9dde9bf..6e569b0a31 100644 --- a/pkg/matcher/utils.go +++ b/pkg/matcher/utils.go @@ -2,6 +2,7 @@ package matcher import ( + "bufio" "bytes" "encoding/json" "errors" @@ -15,9 +16,8 @@ import ( "github.com/7sDream/geko" "github.com/fatih/color" + jsonDiff "github.com/keploy/jsonDiff" "github.com/olekukonko/tablewriter" - "github.com/yudai/gojsondiff" - "github.com/yudai/gojsondiff/formatter" "go.keploy.io/server/v2/pkg/models" "go.keploy.io/server/v2/utils" "go.uber.org/zap" @@ -266,8 +266,12 @@ func UnmarshallJSON(s string, log *zap.Logger) (interface{}, error) { return result, nil } -// MAX_LINE_LENGTH is chars PER expected/actual string. Can be changed no problem -const MAX_LINE_LENGTH = 50 +// maxLineLength is chars PER expected/actual string. Can be changed no problem +const maxLineLength = 50 + +var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) + +var ansiResetCode = "\x1b[0m" type DiffsPrinter struct { testCase string @@ -441,24 +445,12 @@ func (d *DiffsPrinter) RenderAppender() error { */ func sprintDiffHeader(expect, actual map[string]string) string { - expectAll := "" - actualAll := "" - for key, expValue := range expect { - actValue := key + ": " + actual[key] - expValue = key + ": " + expValue - // Offset will be where the string start to unmatch - offset, _ := diffIndex(expValue, actValue) + diff := jsonDiff.CompareHeaders(expect, actual) - // Color of the unmatch, can be changed - cE, cA := color.FgHiRed, color.FgHiGreen - - expectAll += breakWithColor(expValue, &cE, offset) - actualAll += breakWithColor(actValue, &cA, offset) - } - if len(expect) > MAX_LINE_LENGTH || len(actual) > MAX_LINE_LENGTH { - return expectActualTable(expectAll, actualAll, "header", false) // Don't centerize + if len(expect) > maxLineLength || len(actual) > maxLineLength { + return expectActualTable(diff.Actual, diff.Expected, "header", false) // Don't centerize } - return expectActualTable(expectAll, actualAll, "header", true) + return expectActualTable(diff.Actual, diff.Expected, "header", true) } /* @@ -468,18 +460,12 @@ func sprintDiffHeader(expect, actual map[string]string) string { */ func sprintDiff(expect, actual, field string) string { - // Offset will be where the string start to unmatch - offset, _ := diffIndex(expect, actual) + diff := jsonDiff.Compare(expect, actual) - // Color of the unmatch, can be changed - cE, cA := color.FgHiRed, color.FgHiGreen - - exp := breakWithColor(expect, &cE, offset) - act := breakWithColor(actual, &cA, offset) - if len(expect) > MAX_LINE_LENGTH || len(actual) > MAX_LINE_LENGTH { - return expectActualTable(exp, act, field, false) // Don't centerize + if len(expect) > maxLineLength || len(actual) > maxLineLength { + return expectActualTable(diff.Expected, diff.Actual, field, false) } - return expectActualTable(exp, act, field, true) + return expectActualTable(diff.Expected, diff.Actual, field, true) } /* This will return the json diffs in a beautifull way. It will in fact @@ -490,200 +476,75 @@ func sprintDiff(expect, actual, field string) string { * its better to use a generic diff output as the SprintDiff. */ func sprintJSONDiff(json1 []byte, json2 []byte, field string, noise map[string][]string) (string, error) { - diffString, err := calculateJSONDiffs(json1, json2) + diff, err := jsonDiff.CompareJSON(json1, json2, noise, false) if err != nil { return "", err } - expect, actual := separateAndColorize(diffString, noise) - result := expectActualTable(expect, actual, field, false) + result := expectActualTable(diff.Expected, diff.Actual, field, false) return result, nil } -// Find the diff between two strings returning index where -// the difference begin -func diffIndex(s1, s2 string) (int, bool) { - diff := false - i := -1 - - // Check if one string is smaller than another, if so theres a diff - if len(s1) < len(s2) { - i = len(s1) - diff = true - } else if len(s2) < len(s1) { - diff = true - i = len(s2) - } - - // Check for unmatched characters - for i := 0; i < len(s1) && i < len(s2); i++ { - if s1[i] != s2[i] { - return i, true - } - } - - return i, diff -} - -/* Will perform the calculation of the diffs, returning a string that - * containes the lines that does not match represented by either a - * minus or add symbol followed by the respective line. - */ -func calculateJSONDiffs(json1 []byte, json2 []byte) (string, error) { - var diff = gojsondiff.New() - dObj, err := diff.Compare(json1, json2) - if err != nil { - return "", err - } - - var jsonObject map[string]interface{} - err = json.Unmarshal([]byte(json1), &jsonObject) - if err != nil { - return "", err - } - - diffString, _ := formatter.NewAsciiFormatter(jsonObject, formatter.AsciiFormatterConfig{ - ShowArrayIndex: true, - Coloring: false, // We will color our way - }).Format(dObj) - - return diffString, nil -} - -// Will receive a string that has the differences represented -// by a plus or a minus sign and separate it. Just works with json -// Modified separateAndColorize function to handle nested JSON paths for noise keys -// Updated separateAndColorize function -func separateAndColorize(diffStr string, noise map[string][]string) (string, string) { - expect, actual := "", "" - diffLines := strings.Split(diffStr, "\n") - jsonPath := []string{} // Stack to keep track of nested paths - - for i, line := range diffLines { - if len(line) > 0 { - noised := false - lineContent := line[1:] // Remove the diff indicator (+/-) - trimmedLine := strings.TrimSpace(lineContent) - // Update the JSON path stack based on the line content - if strings.HasSuffix(trimmedLine, "{") { - key := strings.TrimSpace(trimmedLine[:len(trimmedLine)-1]) // Remove '{' - key = strings.Trim(key, `"`) // Remove surrounding quotes - jsonPath = append(jsonPath, key) - } else if trimmedLine == "}," || trimmedLine == "}" { - jsonPath = jsonPath[:len(jsonPath)-1] // Pop from stack - } else { - // For key-value pairs, extract the key - if colonIndex := strings.Index(trimmedLine, ":"); colonIndex != -1 { - key := strings.TrimSpace(trimmedLine[:colonIndex]) - key = strings.Trim(key, `"`) // Remove surrounding quotes - if len(jsonPath) > 0 { - jsonPath = append(jsonPath[:len(jsonPath)-1], key) - } else { - jsonPath = append(jsonPath, key) - } - } - } - - currentPath := strings.Join(jsonPath, ".") +func wrapTextWithAnsi(input string) string { + scanner := bufio.NewScanner(strings.NewReader(input)) // Create a scanner to read the input string line by line. + var wrappedBuilder strings.Builder // Builder for the resulting wrapped text. + currentAnsiCode := "" // Variable to hold the current ANSI escape sequence. + lastAnsiCode := "" // Variable to hold the last ANSI escape sequence. - // Check for noise based on the current JSON path - for noisePath := range noise { - if strings.HasPrefix(strings.ToLower(currentPath), noisePath) { - line = " " + lineContent - if line[0] == '-' { - expect += breakWithColor(line, nil, 0) - } else if line[0] == '+' { - actual += breakWithColor(line, nil, 0) - } - noised = true - break - } - } - if noised { - continue - } + // Iterate over each line in the input string. + for scanner.Scan() { + line := scanner.Text() // Get the current line. - // Process lines without noise - if line[0] == '-' { - c := color.FgRed - if i+1 < len(diffLines) && diffLines[i+1][0] == '+' { - offset, _ := diffIndex(lineContent, diffLines[i+1][1:]) - expect += breakWithColor(line, &c, offset+1) - } else { - expect += breakWithColor(line, &c, 0) - } - } else if line[0] == '+' { - c := color.FgGreen - if i > 0 && diffLines[i-1][0] == '-' { - offset, _ := diffIndex(lineContent, diffLines[i-1][1:]) - actual += breakWithColor(line, &c, offset+1) - } else { - actual += breakWithColor(line, &c, 0) - } - } else { - expect += breakWithColor(line, nil, 0) - actual += breakWithColor(line, nil, 0) - } + // If there is a current ANSI code, append it to the builder. + if currentAnsiCode != "" { + wrappedBuilder.WriteString(currentAnsiCode) } - } - return expect, actual -} -// Will colorize the strubg and do the job of break it if it pass MAX_LINE_LENGTH, -// always respecting the reset of ascii colors before the break line to dont -func breakWithColor(input string, c *color.Attribute, offset int) string { - var output []string - var paint func(a ...interface{}) string - colorize := false - - if c != nil { - colorize = true - paint = color.New(*c).SprintFunc() - } - - for i := 0; i < len(input); i += MAX_LINE_LENGTH { - end := i + MAX_LINE_LENGTH - - if end > len(input) { - end = len(input) + // Find all ANSI escape sequences in the current line. + startAnsiCodes := ansiRegex.FindAllString(line, -1) + if len(startAnsiCodes) > 0 { + // Update the last ANSI escape sequence to the last one found in the line. + lastAnsiCode = startAnsiCodes[len(startAnsiCodes)-1] } - // This conditions joins if we are at line where the offset begins - if colorize && i+MAX_LINE_LENGTH > offset { - paintedStart := i - if paintedStart < offset { - paintedStart = offset - } + // Append the current line to the builder. + wrappedBuilder.WriteString(line) - // Will basically concatenated the non-painted string with the - // painted - prePaint := input[i:paintedStart] // Start at i ends at offset - postPaint := paint(input[paintedStart:end]) // Starts at offset (diff begins), goes til maxLength - substr := prePaint + postPaint + "\n" // Concatenate - output = append(output, substr) + // Check if the current ANSI code needs to be reset or updated. + if (currentAnsiCode != "" && !strings.HasSuffix(line, ansiResetCode)) || len(startAnsiCodes) > 0 { + // If the current line does not end with a reset code or if there are ANSI codes, append a reset code. + wrappedBuilder.WriteString(ansiResetCode) + // Update the current ANSI code to the last one found in the line. + currentAnsiCode = lastAnsiCode } else { - substr := input[i:end] + "\n" - output = append(output, substr) + // If no ANSI codes need to be maintained, reset the current ANSI code. + currentAnsiCode = "" } + + // Append a newline character to the builder. + wrappedBuilder.WriteString("\n") } - return strings.Join(output, "") + + // Return the processed string with properly wrapped ANSI escape sequences. + return wrappedBuilder.String() } -// Will return a string in a two columns table where the left -// side is the expected string and the right is the actual -// field: body, header, status... func expectActualTable(exp string, act string, field string, centerize bool) string { buf := &bytes.Buffer{} table := tablewriter.NewWriter(buf) if centerize { table.SetAlignment(tablewriter.ALIGN_CENTER) + } else { + table.SetAlignment(tablewriter.ALIGN_LEFT) } - + // jsonDiff.JsonDiff() + exp = wrapTextWithAnsi(exp) + act = wrapTextWithAnsi(act) table.SetHeader([]string{fmt.Sprintf("Expect %v", field), fmt.Sprintf("Actual %v", field)}) table.SetAutoWrapText(false) table.SetBorder(false) - table.SetColMinWidth(0, MAX_LINE_LENGTH) - table.SetColMinWidth(1, MAX_LINE_LENGTH) + table.SetColMinWidth(0, maxLineLength) + table.SetColMinWidth(1, maxLineLength) table.Append([]string{exp, act}) table.Render() return buf.String() @@ -973,6 +834,9 @@ func matchJSONWithNoiseHandling(key string, expected, actual interface{}, noiseM copiedExpMap := make(map[string]interface{}) copiedActMap := make(map[string]interface{}) + if regexArr, isNoisy := CheckStringExist(key, noiseMap); isNoisy && len(regexArr) == 0 { + break + } // Copy each key-value pair from expMap to copiedExpMap for key, value := range expMap { copiedExpMap[key] = value @@ -1016,7 +880,7 @@ func matchJSONWithNoiseHandling(key string, expected, actual interface{}, noiseM matchJSONComparisonResult.differences = append(matchJSONComparisonResult.differences, differences...) return matchJSONComparisonResult, nil case reflect.Slice: - if regexArr, isNoisy := CheckStringExist(key, noiseMap); isNoisy && len(regexArr) != 0 { + if regexArr, isNoisy := CheckStringExist(key, noiseMap); isNoisy && len(regexArr) == 0 { break } expSlice := reflect.ValueOf(expected) @@ -1029,7 +893,8 @@ func matchJSONWithNoiseHandling(key string, expected, actual interface{}, noiseM for i := 0; i < expSlice.Len(); i++ { matched := false for j := 0; j < actSlice.Len(); j++ { - if valMatchJSONComparisonResult, err := matchJSONWithNoiseHandling(key, expSlice.Index(i).Interface(), actSlice.Index(j).Interface(), noiseMap, ignoreOrdering); err == nil && valMatchJSONComparisonResult.matches { + prefixedVal := key + "[" + fmt.Sprint(j) + "]" + if valMatchJSONComparisonResult, err := matchJSONWithNoiseHandling(prefixedVal, expSlice.Index(i).Interface(), actSlice.Index(j).Interface(), noiseMap, ignoreOrdering); err == nil && valMatchJSONComparisonResult.matches { if !valMatchJSONComparisonResult.isExact { for _, val := range valMatchJSONComparisonResult.differences { prefixedVal := key + "[" + fmt.Sprint(j) + "]." + val // Prefix the value