coverage/report.go (259 lines of code) (raw):
package coverage
import (
"fmt"
"sort"
"strings"
"github.com/sirupsen/logrus"
)
// for now, we don't display bool and enum detail in coverage detail
const isBoolEnumDisplayed = false
type CoverageReport struct {
Coverages map[string]*CoverageItem
}
type CoverageItem struct {
ApiPath string
DisplayName string
Model *Model
}
func (c *CoverageReport) AddCoverageFromState(resourceId, resourceType string, jsonBody map[string]interface{}, swaggerPath string) error {
apiVersion := strings.Split(resourceType, "@")[1]
var swaggerModel *SwaggerModel
if swaggerPath != "" {
swaggerModelFromLocal, err := GetModelInfoFromLocalDir(resourceId, swaggerPath, "PUT")
if err != nil {
logrus.Warnf("error find the path for %s from local dir: %+v", resourceId, err)
}
swaggerModel = swaggerModelFromLocal
}
if swaggerModel == nil {
swaggerModelFromIndex, err := GetModelInfoFromIndex(resourceId, apiVersion, "PUT", "")
if err != nil {
return fmt.Errorf("error find the path for %s from index: %+v", resourceId, err)
}
swaggerModel = swaggerModelFromIndex
}
logrus.Infof("matched API path: %s; modelSwawggerPath: %s\n", swaggerModel.ApiPath, swaggerModel.SwaggerPath)
if _, ok := c.Coverages[swaggerModel.ApiPath]; !ok {
expanded, err := Expand(swaggerModel.ModelName, swaggerModel.SwaggerPath)
if err != nil {
return fmt.Errorf("error expand model %s property: %+v", swaggerModel.ModelName, err)
}
c.Coverages[swaggerModel.ApiPath] = &CoverageItem{
ApiPath: swaggerModel.ApiPath,
DisplayName: resourceType,
Model: expanded,
}
}
c.Coverages[swaggerModel.ApiPath].Model.MarkCovered(jsonBody)
c.Coverages[swaggerModel.ApiPath].Model.CountCoverage()
return nil
}
func (c *CoverageReport) MarkdownContent() string {
template := `
### Coverage Status:
#### Summary
${coverage_summary}
#### Details
${coverage_details}
`
fullyCoveredPath := make([]string, 0)
partiallyCoveredPath := make([]string, 0)
for _, v := range c.Coverages {
if v.Model.IsFullyCovered {
fullyCoveredPath = append(fullyCoveredPath, v.DisplayName)
} else {
partiallyCoveredPath = append(partiallyCoveredPath, fmt.Sprintf("%v (%v/%v)", v.DisplayName, v.Model.RootCoveredCount, v.Model.RootTotalCount))
}
}
summary := ""
if len(fullyCoveredPath) > 0 {
summary += fmt.Sprintf("Congratulations! The following resource types are 100%% covered:\n\n- %s\n\n", strings.Join(fullyCoveredPath, "\n- "))
}
if len(partiallyCoveredPath) > 0 {
summary += fmt.Sprintf("The following resource types are partially covered, please help add more test cases:\n\n- %s\n\n", strings.Join(partiallyCoveredPath, "\n- "))
}
content := strings.ReplaceAll(template, "${coverage_summary}", summary)
var coverages []string
count := 0
for _, v := range c.Coverages {
count++
reportDetail := getReport(v.Model.ModelName, v.Model)
sort.Strings(reportDetail)
coverages = append(coverages, fmt.Sprintf(`##### <!-- %[1]v -->
<details open>
<summary>%[1]v %[2]v</summary>
[swagger](%[3]v)
<blockquote>
%[4]v
</blockquote>
</details>
---
`, v.DisplayName, v.ApiPath, v.Model.SourceFile, strings.Join(reportDetail, "\n\n")))
}
sort.Strings(coverages)
content = strings.ReplaceAll(content, "${coverage_details}", strings.Join(coverages, "\n"))
return content
}
func (c *CoverageReport) MarkdownContentCompact() string {
template := `
## Coverage Report
|Operation|Tested properties|Total properties|Coverage|
|---|---|---|---|
`
content := ""
total := 0
covered := 0
for _, v := range c.Coverages {
coverage := 100.0
if v.Model.TotalCount > 0 {
coverage = float64(v.Model.RootCoveredCount * 100 / v.Model.RootTotalCount)
}
content += fmt.Sprintf("|%s|%d|%d|%.1f%%|\n", v.DisplayName, v.Model.RootCoveredCount, v.Model.RootTotalCount, coverage)
total += v.Model.RootTotalCount
covered += v.Model.RootCoveredCount
}
coverage := 100.0
if total > 0 {
coverage = float64(covered * 100 / total)
}
content = fmt.Sprintf("%s|%d|%d|%.1f%%|\n", "All", covered, total, coverage) + content
return template + content
}
func getReport(displayName string, model *Model) []string {
out := make([]string, 0)
style := getStyle(model.IsFullyCovered)
if hasNoDetail(model) {
// leaf property
out = append(out,
fmt.Sprintf(`<!-- %[1]v -->
<details>
<summary><span%[2]v>%[3]v</span></summary>
</details>`, model.Identifier, style, displayName),
)
return out
}
if isBoolEnumDisplayed {
if model.Enum != nil {
for k, isCovered := range *model.Enum {
out = append(out, getEnumBoolReport(k, isCovered))
}
return out
}
if model.Bool != nil {
for k, isCovered := range *model.Bool {
out = append(out, getEnumBoolReport(fmt.Sprintf("%v", k), isCovered))
}
return out
}
}
if model.Item != nil {
return getReport(displayName, model.Item)
}
if model.Properties != nil {
for k, v := range *model.Properties {
if v.IsReadOnly {
continue
}
if v.Item != nil && v.Item.IsReadOnly {
continue
}
if v.Variants != nil {
variantType := v.ModelName
if v.VariantType != nil {
variantType = *v.VariantType
}
variantKey := getDiscriminatorKey(k, variantType)
out = append(out, getReport(variantKey, v)...)
for variantType, variant := range *v.Variants {
variantType := variantType
if variant.VariantType != nil {
variantType = *variant.VariantType
}
variantKey := getDiscriminatorKey(k, variantType)
out = append(out, getReport(variantKey, variant)...)
}
continue
}
if v.Item != nil && v.Item.Variants != nil {
variantType := v.Item.ModelName
if v.Item.VariantType != nil {
variantType = *v.Item.VariantType
}
variantKey := getDiscriminatorKey(k, variantType)
out = append(out, getReport(variantKey, v)...)
for variantType, variant := range *v.Item.Variants {
variantType := variantType
if variant.VariantType != nil {
variantType = *variant.VariantType
}
variantKey := getDiscriminatorKey(k, variantType)
out = append(out, getReport(variantKey, variant)...)
}
continue
}
out = append(out, getReport(k, v)...)
}
}
sort.Strings(out)
outWithoutVariant := []string{
fmt.Sprintf(`<!-- %[1]v -->
<details>
<summary><span%[2]v>%[3]v %[4]v</span></summary>
<blockquote>
%[5]v
</blockquote>
</details>`, model.Identifier, style, displayName, getCoverageCount(model), strings.Join(out, "\n\n")),
}
if model.IsRoot {
var variants *map[string]*Model
if model.Variants != nil {
variants = model.Variants
}
if model.Item != nil && model.Item.Variants != nil {
variants = model.Item.Variants
}
if variants != nil {
outWithVariant := make([]string, 0)
outWithVariant = append(outWithVariant, outWithoutVariant...)
for variantType, variant := range *variants {
variantType := variantType
if variant.VariantType != nil {
variantType = *variant.VariantType
}
variantKey := getDiscriminatorKey(displayName, variantType)
outWithVariant = append(outWithVariant, getReport(variantKey, variant)...)
}
sort.Strings(outWithVariant)
return outWithVariant
}
}
return outWithoutVariant
}
func getEnumBoolReport(name string, isCovered bool) string {
return fmt.Sprintf("- <span %v>value=%v</span>", getStyle(isCovered), name)
}
func getCoverageCount(model *Model) string {
if model.Bool != nil {
return fmt.Sprintf("(bool=%v/%v)", model.BoolCoveredCount, 2)
}
if model.Enum != nil {
return fmt.Sprintf("(enum=%v/%v)", model.EnumCoveredCount, model.EnumTotalCount)
}
return fmt.Sprintf("(%v/%v)", model.CoveredCount, model.TotalCount)
}
func hasNoDetail(model *Model) bool {
if model.Properties == nil && model.Variants == nil && model.Item == nil && (!isBoolEnumDisplayed || (model.Bool == nil && model.Enum == nil)) {
return true
}
// array inside array is regarded as no detail
if model.Item != nil {
return hasNoDetail(model.Item)
}
return false
}
func getStyle(isFullyCovered bool) string {
if isFullyCovered {
return ""
}
return " style=\"color:red\""
}
func getDiscriminatorKey(modelName, variantType string) string {
return fmt.Sprintf("%s{%s}", modelName, variantType)
}