This repository has been archived by the owner on Jan 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 7
fix!: markdown processing returns errors #802
Merged
j-poecher
merged 10 commits into
master
from
fix/629/markdown-processing-returns-errors
May 12, 2022
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
d8b5b0d
changes log message
j-poecher eb315fe
parsing markdown now returns errors; split up tests
j-poecher 01f261a
fix documentation and add data types & restrictions
j-poecher f6a73b9
adds checks for duplicate keys and adds tests
j-poecher ef9ab20
adds checks for invalid values on total.pass & .warning; adds tests
j-poecher f03ea4b
adds more markdown tests
j-poecher 398af1c
adds integration test for markdown parsing
j-poecher c5eb4bc
adds more integration tests
j-poecher af8a92d
exports constants and uses them in tests
j-poecher 994ffef
adds tests for multiple markdown tiles for configuration
j-poecher File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
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 |
---|---|---|
@@ -1,12 +1,50 @@ | ||
package dashboard | ||
|
||
import ( | ||
"github.com/keptn-contrib/dynatrace-service/internal/dynatrace" | ||
keptncommon "github.com/keptn/go-utils/pkg/lib" | ||
"fmt" | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
|
||
keptncommon "github.com/keptn/go-utils/pkg/lib" | ||
|
||
"github.com/keptn-contrib/dynatrace-service/internal/dynatrace" | ||
) | ||
|
||
type markdownParsingErrors struct { | ||
errors []error | ||
} | ||
|
||
func (err *markdownParsingErrors) Error() string { | ||
var errStrings = make([]string, len(err.errors)) | ||
for i, e := range err.errors { | ||
errStrings[i] = e.Error() | ||
} | ||
return strings.Join(errStrings, ";") | ||
} | ||
|
||
type invalidValueError struct { | ||
key string | ||
value string | ||
} | ||
|
||
func (err *invalidValueError) Error() string { | ||
return fmt.Sprintf("invalid value for '%s': %s", err.key, err.value) | ||
} | ||
|
||
type duplicateKeyError struct { | ||
key string | ||
} | ||
|
||
func (err *duplicateKeyError) Error() string { | ||
return fmt.Sprintf("duplicate key '%s' in markdown definition", err.key) | ||
} | ||
|
||
type markdownParsingResult struct { | ||
totalScore keptncommon.SLOScore | ||
comparison keptncommon.SLOComparison | ||
} | ||
|
||
type MarkdownTileProcessing struct { | ||
} | ||
|
||
|
@@ -16,61 +54,158 @@ func NewMarkdownTileProcessing() *MarkdownTileProcessing { | |
} | ||
|
||
// Process will overwrite the default values for SLOScore and SLOComparison with the contents found in the markdown | ||
func (p *MarkdownTileProcessing) Process(tile *dynatrace.Tile, defaultScore keptncommon.SLOScore, defaultComparison keptncommon.SLOComparison) (*keptncommon.SLOScore, *keptncommon.SLOComparison) { | ||
func (p *MarkdownTileProcessing) Process(tile *dynatrace.Tile, defaultScore keptncommon.SLOScore, defaultComparison keptncommon.SLOComparison) (*markdownParsingResult, error) { | ||
// we allow the user to use a markdown to specify SLI/SLO properties, e.g: KQG.Total.Pass | ||
// if we find KQG. we process the markdown | ||
return parseMarkdownConfiguration(tile.Markdown, defaultScore, defaultComparison) | ||
} | ||
|
||
const ( | ||
TotalPass = "kqg.total.pass" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [golint] reported by reviewdog 🐶 |
||
TotalWarning = "kqg.total.warning" | ||
CompareWithScore = "kqg.compare.withscore" | ||
CompareWithScoreAll = "all" | ||
CompareWithScorePass = "pass" | ||
CompareWithScorePassOrWarn = "pass_or_warn" | ||
CompareResults = "kqg.compare.results" | ||
CompareResultsSingle = "single_result" | ||
CompareResultsMultiple = "several_results" | ||
CompareFunction = "kqg.compare.function" | ||
CompareFunctionAvg = "avg" | ||
CompareFunctionP50 = "p50" | ||
CompareFunctionP90 = "p90" | ||
CompareFunctionP95 = "p95" | ||
) | ||
|
||
// parseMarkdownConfiguration parses a text that can be used in a Markdown tile to specify global SLO properties | ||
func parseMarkdownConfiguration(markdown string, totalScore keptncommon.SLOScore, comparison keptncommon.SLOComparison) (*keptncommon.SLOScore, *keptncommon.SLOComparison) { | ||
func parseMarkdownConfiguration(markdown string, totalScore keptncommon.SLOScore, comparison keptncommon.SLOComparison) (*markdownParsingResult, error) { | ||
if !strings.Contains(markdown, "KQG.") { | ||
return nil, nil | ||
} | ||
|
||
result := &markdownParsingResult{ | ||
totalScore: totalScore, | ||
comparison: comparison, | ||
} | ||
|
||
var errs []error | ||
keyFound := make(map[string]bool) | ||
|
||
markdownSplits := strings.Split(markdown, ";") | ||
for _, markdownSplitValue := range markdownSplits { | ||
configValueSplits := strings.Split(markdownSplitValue, "=") | ||
if len(configValueSplits) != 2 { | ||
continue | ||
} | ||
|
||
// lets get configname and value | ||
configName := strings.ToLower(configValueSplits[0]) | ||
configValue := configValueSplits[1] | ||
|
||
switch configName { | ||
case "kqg.total.pass": | ||
totalScore.Pass = configValue | ||
case "kqg.total.warning": | ||
totalScore.Warning = configValue | ||
case "kqg.compare.withscore": | ||
comparison.IncludeResultWithScore = configValue | ||
if (configValue == "pass") || (configValue == "pass_or_warn") || (configValue == "all") { | ||
comparison.IncludeResultWithScore = configValue | ||
} else { | ||
comparison.IncludeResultWithScore = "pass" | ||
// lets separate key and value | ||
key := strings.ToLower(configValueSplits[0]) | ||
value := configValueSplits[1] | ||
|
||
switch key { | ||
case TotalPass: | ||
if keyFound[TotalPass] { | ||
errs = append(errs, &duplicateKeyError{key: TotalPass}) | ||
break | ||
} | ||
if isNotAPercentValue(value) { | ||
errs = append(errs, &invalidValueError{key: TotalPass, value: value}) | ||
} | ||
result.totalScore.Pass = value | ||
keyFound[TotalPass] = true | ||
case TotalWarning: | ||
if keyFound[TotalWarning] { | ||
errs = append(errs, &duplicateKeyError{key: TotalWarning}) | ||
break | ||
} | ||
case "kqg.compare.results": | ||
noresults, err := strconv.Atoi(configValue) | ||
if isNotAPercentValue(value) { | ||
errs = append(errs, &invalidValueError{key: TotalWarning, value: value}) | ||
} | ||
result.totalScore.Warning = value | ||
keyFound[TotalWarning] = true | ||
case CompareWithScore: | ||
if keyFound[CompareWithScore] { | ||
errs = append(errs, &duplicateKeyError{key: CompareWithScore}) | ||
break | ||
} | ||
score, err := parseCompareWithScore(value) | ||
if err != nil { | ||
comparison.NumberOfComparisonResults = 1 | ||
} else { | ||
comparison.NumberOfComparisonResults = noresults | ||
errs = append(errs, err) | ||
} | ||
if comparison.NumberOfComparisonResults > 1 { | ||
comparison.CompareWith = "several_results" | ||
} else { | ||
comparison.CompareWith = "single_result" | ||
result.comparison.IncludeResultWithScore = score | ||
keyFound[CompareWithScore] = true | ||
case CompareResults: | ||
if keyFound[CompareResults] { | ||
errs = append(errs, &duplicateKeyError{key: CompareResults}) | ||
break | ||
} | ||
numberOfResults, err := parseCompareNumberOfResults(value) | ||
if err != nil { | ||
errs = append(errs, err) | ||
} | ||
case "kqg.compare.function": | ||
if (configValue == "avg") || (configValue == "p50") || (configValue == "p90") || (configValue == "p95") { | ||
comparison.AggregateFunction = configValue | ||
} else { | ||
comparison.AggregateFunction = "avg" | ||
result.comparison.NumberOfComparisonResults = numberOfResults | ||
keyFound[CompareResults] = true | ||
case CompareFunction: | ||
if keyFound[CompareFunction] { | ||
errs = append(errs, &duplicateKeyError{key: CompareFunction}) | ||
break | ||
} | ||
aggregateFunc, err := parseAggregateFunction(value) | ||
if err != nil { | ||
errs = append(errs, err) | ||
} | ||
result.comparison.AggregateFunction = aggregateFunc | ||
keyFound[CompareFunction] = true | ||
} | ||
} | ||
|
||
if len(errs) > 0 { | ||
return nil, &markdownParsingErrors{ | ||
errors: errs, | ||
} | ||
} | ||
|
||
return &totalScore, &comparison | ||
result.comparison.CompareWith = CompareResultsSingle | ||
if result.comparison.NumberOfComparisonResults > 1 { | ||
result.comparison.CompareWith = CompareResultsMultiple | ||
} | ||
|
||
return result, nil | ||
} | ||
|
||
func isNotAPercentValue(value string) bool { | ||
pattern := regexp.MustCompile("^(\\d+|\\d+\\.\\d+)([%]?)$") | ||
|
||
return !pattern.MatchString(value) | ||
} | ||
|
||
func parseCompareWithScore(value string) (string, error) { | ||
switch value { | ||
case CompareWithScorePass, CompareWithScoreAll, CompareWithScorePassOrWarn: | ||
return value, nil | ||
} | ||
|
||
return "", &invalidValueError{key: CompareWithScore, value: value} | ||
} | ||
|
||
func parseCompareNumberOfResults(value string) (int, error) { | ||
numberOfResults, err := strconv.Atoi(value) | ||
if err != nil { | ||
return 0, &invalidValueError{key: CompareResults, value: value} | ||
} | ||
|
||
if numberOfResults < 1 { | ||
return 0, &invalidValueError{key: CompareResults, value: value} | ||
} | ||
|
||
return numberOfResults, nil | ||
} | ||
|
||
func parseAggregateFunction(value string) (string, error) { | ||
switch value { | ||
case CompareFunctionAvg, CompareFunctionP50, CompareFunctionP90, CompareFunctionP95: | ||
return value, nil | ||
} | ||
|
||
return "", &invalidValueError{key: CompareFunction, value: value} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[golint] reported by reviewdog 🐶
exported method Process returns unexported type *dashboard.markdownParsingResult, which can be annoying to use