Skip to content

Commit

Permalink
Merge pull request helm-unittest#521 from gofogo/issue-499
Browse files Browse the repository at this point in the history
  • Loading branch information
quintush authored Jan 8, 2025
2 parents 19ead1e + 2928d80 commit 2ab5f41
Show file tree
Hide file tree
Showing 13 changed files with 503 additions and 41 deletions.
56 changes: 56 additions & 0 deletions internal/common/utilities_ymlescape.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package common

import (
"bytes"
"regexp"
"strings"
)

const (
bsCode byte = byte('\\')
metaCharactersNeedsEscapePattern = `.*[.+*?()|[\]{}^$].*`
)

var metaRegex = regexp.MustCompile(metaCharactersNeedsEscapePattern)

type YmlEscapeHandlers struct{}

// Escape function is required, as yaml library no longer maintained
// yaml unmaintained library issue https://github.com/go-yaml/yaml/pull/862
func (y *YmlEscapeHandlers) Escape(content string) []byte {
if !strings.Contains(content, `\`) && !metaRegex.MatchString(content) {
return nil
}
return escapeBackslashes([]byte(content))
}

// escapeBackslashes escapes backslashes in the given byte slice.
// It ensures that an even number of backslashes are present by doubling any single backslash found.
func escapeBackslashes(content []byte) []byte {
var result bytes.Buffer
i := 0
for i < len(content) {
if content[i] != bsCode {
result.WriteByte(content[i])
i++
continue
}

count := 1
for i+count < len(content) && content[i+count] == bsCode {
count++
}

times := count
if count%2 == 1 {
times++
}

for j := 0; j < times; j++ {
result.WriteByte(bsCode)
}

i += count
}
return result.Bytes()
}
93 changes: 93 additions & 0 deletions internal/common/utilities_ymlescape_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package common_test

import (
"testing"

"github.com/stretchr/testify/assert"
. "github.com/helm-unittest/helm-unittest/internal/common"
)

const (
bsCode byte = byte('\\')
)

func TestEscape_InputAsRunes_EscapeBackslash_Code92(t *testing.T) {
y := &YmlEscapeHandlers{}
cases := []struct {
name string
content []byte
expected []byte
}{
{
name: "single and double backslashes",
// paradox \(root\\)
content: []byte{10, 9, 112, 97, 114, 97, 100, 111, 120, 32, bsCode, 40, 114, 111, 111, 116, bsCode, bsCode, 41, 10, 9},
expected: []byte{10, 9, 112, 97, 114, 97, 100, 111, 120, 32, bsCode, bsCode, 40, 114, 111, 111, 116, bsCode, bsCode, 41, 10, 9},
},
{
name: "single and triple backslashes with special characters",
// `runAsUser` is set to `0` \(root\\\
content: []byte{96, 114, 117, 110, 65, 115, 85, 115, 101, 114, 96, 32, 105, 115, 32, 115, 101, 116, 32, 116, 111, 32, 96, 48, 96, 32, bsCode, 40, 114, 111, 111, 116, bsCode, bsCode, bsCode},
// `runAsUser` is set to `0` \\(root\\\\
expected: []byte{96, 114, 117, 110, 65, 115, 85, 115, 101, 114, 96, 32, 105, 115, 32, 115, 101, 116, 32, 116, 111, 32, 96, 48, 96, 32, bsCode, bsCode, 40, 114, 111, 111, 116, bsCode, bsCode, bsCode, bsCode},
},
{
name: "single and triple backslashes with special characters",
// `runAsUser` is set to `0` \(root\\)
content: []byte{96, 114, 117, 110, 65, 115, 85, 115, 101, 114, 96, 32, 105, 115, 32, 115, 101, 116, 32, 116, 111, 32, 96, 48, 96, 32, bsCode, 40, 114, 111, 111, 116, bsCode, bsCode, bsCode},
// `runAsUser` is set to `0` \\(root\\\\
expected: []byte{96, 114, 117, 110, 65, 115, 85, 115, 101, 114, 96, 32, 105, 115, 32, 115, 101, 116, 32, 116, 111, 32, 96, 48, 96, 32, bsCode, bsCode, 40, 114, 111, 111, 116, bsCode, bsCode, bsCode, bsCode},
},
{
name: "special characters and backslashes",
// `run` is set \\0\\\ \(root\\\)\\\\
content: []byte{96, 114, 117, 110, 96, 32, 105, 115, 32, 115, 101, 116, 32, bsCode, 48, bsCode, bsCode, bsCode, 32, bsCode, 40, 114, 111, 111, 116, bsCode, bsCode, bsCode, 41, bsCode, bsCode, bsCode, bsCode},
// `run` is set \\0\\\\ \\(root\\\\)\\\\
expected: []byte{96, 114, 117, 110, 96, 32, 105, 115, 32, 115, 101, 116, 32, bsCode, bsCode, 48, bsCode, bsCode, bsCode, bsCode, 32, bsCode, bsCode, 40, 114, 111, 111, 116, bsCode, bsCode, bsCode, bsCode, 41, bsCode, bsCode, bsCode, bsCode},
},
{
name: "empty",
content: []byte{},
expected: nil,
},
{
name: "Mixed Backslashes",
content: []byte("hello\\world\\\\"),
expected: []byte("hello\\\\world\\\\"),
},
{
name: "no backslashes",
content: []byte("abrakadabra"),
expected: nil,
},
{
name: "contains meta characters but no slashes",
content: []byte("some text with [value] needs escaping"),
expected: []byte("some text with [value] needs escaping"),
},
{
name: "backslashes at the end",
content: []byte("hello world\\"),
expected: []byte("hello world\\\\"),
},
{
name: "backslashes in the middle",
content: []byte("hello\\world\\"),
expected: []byte("hello\\\\world\\\\"),
},
{
name: "even number of backslashes",
content: []byte("hello" + `\\\` + "world"),
expected: []byte("hello" + `\\\\` + "world"),
},
}

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
actual := y.Escape(string(tt.content))

assert.Equal(t, string(tt.expected), string(actual))
assert.Equal(t, tt.expected, actual)
})
}
}
6 changes: 2 additions & 4 deletions pkg/unittest/assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (a *Assertion) Assert(
invalidRender := "Error: rendered manifest is empty"
failInfo = append(failInfo, invalidRender)
} else {
emptyTemplate := []common.K8sManifest{}
var emptyTemplate []common.K8sManifest
validatePassed, singleFailInfo = a.validateTemplate(emptyTemplate, emptyTemplate, snapshotComparer, renderError, failfast)
}

Expand Down Expand Up @@ -95,7 +95,6 @@ func (a *Assertion) Assert(
singleTemplateResult[template] = rendered

selectedDocs := selectedDocsByTemplate[template]

validatePassed, singleFailInfo = a.validateTemplate(rendered, selectedDocs, snapshotComparer, renderError, failfast)

if !validatePassed {
Expand Down Expand Up @@ -145,7 +144,7 @@ func (a *Assertion) getDocumentsByDefaultTemplates(templatesResult map[string][]
}

func (a *Assertion) getKeys(docs map[string][]common.K8sManifest) []string {
keys := []string{}
var keys []string

for key := range docs {
keys = append(keys, key)
Expand Down Expand Up @@ -225,7 +224,6 @@ func (a *Assertion) UnmarshalYAML(unmarshal func(interface{}) error) error {
SkipEmptyTemplates: documentSelectorSkipEmptyTemplates,
}
}

if err := a.constructValidator(assertDef); err != nil {
return err
}
Expand Down
37 changes: 20 additions & 17 deletions pkg/unittest/test_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ import (

const LOG_TEST_JOB = "test-job"

// Split the error into several groups.
// those groups are required to parse the correct value.
// ^.+( |\()(.+):\d+:\d+\)?:(.+:)* (.+)$
// (?mU)^.+(?: |\\()(.+):\\d+:\\d+\\)?:(?:.+:)* (.+)$
// (?mU)^(?:.+: |.+ \()(?:(.+):\d+:\d+).+(?:.+>)*: (.+)$
// (?msU)
//
// --- m: Multi-line mode. ^ and $ match the start and end of each line.
// --- s: Dot-all mode. . matches any character, including newline.
// --- U: Ungreedy mode. Makes quantifiers lazy by default.
//
// const regexPattern string = "(?mU)^(?:.+: |.+ \\()(?:(.+):\\d+:\\d+).+(?:.+>)*: (.+)$"
const regexPattern string = "(?msU)^(?:.+: |.+ \\()(?:(.+):\\d+:\\d+).+(?:.+>)*: (.+)$"

var regexErrorPattern = regexp.MustCompile(regexPattern)

func spliteChartRoutes(routePath string) []string {
splited := strings.Split(routePath, string(filepath.Separator))
routes := make([]string, len(splited)/2+1)
Expand All @@ -49,26 +65,17 @@ func scopeValuesWithRoutes(routes []string, values map[string]interface{}) map[s
}

func parseV3RenderError(errorMessage string) (string, map[string]string) {
// Split the error into several groups.
// those groups are required to parse the correct value.
// ^.+( |\()(.+):\d+:\d+\)?:(.+:)* (.+)$
// (?mU)^.+(?: |\\()(.+):\\d+:\\d+\\)?:(?:.+:)* (.+)$
// (?mU)^(?:.+: |.+ \()(?:(.+):\d+:\d+).+(?:.+>)*: (.+)$
const regexPattern string = "(?mU)^(?:.+: |.+ \\()(?:(.+):\\d+:\\d+).+(?:.+>)*: (.+)$"

filePath, content := parseRenderError(regexPattern, errorMessage)

filePath, content := parseRenderError(errorMessage)
return filePath, content
}

func parseRenderError(regexPattern, errorMessage string) (string, map[string]string) {
func parseRenderError(errorMessage string) (string, map[string]string) {
filePath := ""
content := map[string]string{
common.RAW: "",
}

r := regexp.MustCompile(regexPattern)
result := r.FindStringSubmatch(errorMessage)
result := regexErrorPattern.FindStringSubmatch(errorMessage)

if len(result) == 3 {
filePath = result[1]
Expand Down Expand Up @@ -207,7 +214,6 @@ func (t *TestJob) RunV3(
}

outputOfFiles, renderSucceed, renderError := t.renderV3Chart(targetChart, []byte(userValues))

writeError := writeRenderedOutput(renderPath, outputOfFiles)
if writeError != nil {
result.ExecError = writeError
Expand All @@ -224,7 +230,6 @@ func (t *TestJob) RunV3(
result.ExecError = err
return result
}

// Setup Assertion Templates based on the chartname, documentIndex and outputOfFiles
t.polishAssertionsTemplate(targetChart.Name(), outputOfFiles)
snapshotComparer := &orderedSnapshotComparer{cache: cache, test: t.Name}
Expand Down Expand Up @@ -314,7 +319,6 @@ func (t *TestJob) renderV3Chart(targetChart *v3chart.Chart, userValues []byte) (
if err != nil {
return nil, false, err
}

// When defaultTemplatesToAssert is empty, ensure all templates will be validated.
if len(t.defaultTemplatesToAssert) == 0 {
// Set all files
Expand All @@ -325,7 +329,6 @@ func (t *TestJob) renderV3Chart(targetChart *v3chart.Chart, userValues []byte) (
filteredChart := CopyV3Chart(t.chartRoute, targetChart.Name(), t.defaultTemplatesToAssert, targetChart)

var outputOfFiles map[string]string

// modify chart metadata before rendering
t.ModifyChartMetadata(targetChart)
if len(t.KubernetesProvider.Objects) > 0 {
Expand All @@ -336,10 +339,10 @@ func (t *TestJob) renderV3Chart(targetChart *v3chart.Chart, userValues []byte) (

var renderSucceed bool
outputOfFiles, renderSucceed, err = t.translateErrorToOutputFiles(err, outputOfFiles)
log.WithField(LOG_TEST_JOB, "render-v3-chart").Debug("outputOfFiles:", outputOfFiles, "renderSucceed:", renderSucceed, "err:", err)
if err != nil {
return nil, false, err
}

return outputOfFiles, renderSucceed, nil
}

Expand Down
89 changes: 89 additions & 0 deletions pkg/unittest/test_job_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package unittest_test

import (
"errors"
"fmt"
"os"
"path"
Expand Down Expand Up @@ -929,3 +930,91 @@ ingress:
assert.True(t, testResult.Passed)
assert.Equal(t, 3, len(testResult.AssertsResult))
}

func TestV3RunJob_TplFunction_Fail_WithoutAssertion(t *testing.T) {
c := &v3chart.Chart{
Metadata: &v3chart.Metadata{
Name: "moby",
Version: "1.2.3",
},
Templates: []*v3chart.File{},
Values: map[string]interface{}{},
}

tests := []struct {
template *v3chart.File
error error
}{
{
template: &v3chart.File{Name: "templates/validate.tpl", Data: []byte("{{- fail (printf \"`root`\") }}")},
error: errors.New("execution error at (moby/templates/validate.tpl:1:4): `root`"),
},
{
template: &v3chart.File{Name: "templates/validate.tpl", Data: []byte("{{- fail (printf \"\n`root`\") }}")},
error: errors.New("parse error at (moby/templates/validate.tpl:1): unterminated quoted string"),
},
}

a := assert.New(t)

for _, test := range tests {
tj := TestJob{}
c.Templates = []*v3chart.File{test.template}
testResult := tj.RunV3(c, &snapshot.Cache{}, true, "", &results.TestJobResult{})
a.Error(testResult.ExecError)
a.False(testResult.Passed)
a.EqualError(testResult.ExecError, test.error.Error())
}
}

func TestV3RunJob_TplFunction_Fail_WithAssertion(t *testing.T) {
c := &v3chart.Chart{
Metadata: &v3chart.Metadata{
Name: "moby",
Version: "1.2.3",
},
Templates: []*v3chart.File{},
Values: map[string]interface{}{},
}

tests := []struct {
template *v3chart.File
error error
expected bool
}{
{
template: &v3chart.File{Name: "templates/validate.tpl", Data: []byte("{{- fail (printf \"`root`\") }}")},
error: nil,
expected: true,
},
{
template: &v3chart.File{Name: "templates/validate.tpl", Data: []byte("{{- fail (printf \"\n`root`\") }}")},
error: errors.New("parse error at (moby/templates/validate.tpl:1): unterminated quoted string"),
expected: false,
},
}

manifest := `
it: should validate failure message
template: templates/validate.tpl
asserts:
- failedTemplate:
errorPattern: "` + "`root`" + `"
`
var tj TestJob
unmarshalJobTestHelper(manifest, &tj, t)

for _, test := range tests {
c.Templates = []*v3chart.File{test.template}
testResult := tj.RunV3(c, &snapshot.Cache{}, true, "", &results.TestJobResult{})
assert.Equal(t, test.expected, testResult.Passed)
if test.error != nil {
assert.NotNil(t, testResult.ExecError)
assert.False(t, testResult.Passed)
assert.EqualError(t, testResult.ExecError, test.error.Error())
} else {
assert.Nil(t, testResult.ExecError)
assert.True(t, testResult.Passed)
}
}
}
Loading

0 comments on commit 2ab5f41

Please sign in to comment.