scripts/testing/testing_coverage_rate_check.go (537 lines of code) (raw):
//nolint:all
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"github.com/aliyun/terraform-provider-alicloud/alicloud"
mapset "github.com/deckarep/golang-set"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
log "github.com/sirupsen/logrus"
"github.com/waigani/diffparser"
)
func init() {
customFormatter := new(log.TextFormatter)
customFormatter.FullTimestamp = true
customFormatter.TimestampFormat = "2006-01-02 15:04:05"
customFormatter.DisableTimestamp = false
customFormatter.DisableColors = false
customFormatter.ForceColors = true
log.SetFormatter(customFormatter)
log.SetOutput(os.Stdout)
log.SetLevel(log.DebugLevel)
}
var (
fileNames = flag.String("fileNames", "", "the files to check diff")
resourceFileRegex = regexp.MustCompile("alicloud/(resource)[0-9a-zA-Z_]*")
resourceFileTestRegex = regexp.MustCompile("alicloud/(resource)[0-9a-zA-Z_]*_test.go")
)
func main() {
exitCode := 0
flag.Parse()
if fileNames != nil && len(*fileNames) == 0 {
log.Infof("the diff file is empty, shipped!")
return
}
byt, _ := os.ReadFile(*fileNames)
diff, _ := diffparser.Parse(string(byt))
resourceNameMap := make(map[string]struct{})
for _, file := range diff.Files {
isNameCorrect = true
resourceName := ""
isResource := true
fileType := "resource"
if resourceFileTestRegex.MatchString(file.NewName) {
resourceName = strings.TrimSuffix(strings.Split(file.NewName, "/")[1], "_test.go")
} else if resourceFileRegex.MatchString(file.NewName) {
resourceName = strings.TrimSuffix(strings.Split(file.NewName, "/")[1], ".go")
} else {
continue
}
if strings.HasPrefix(resourceName, "data_source_") {
isResource = false
fileType = "data source"
resourceName = strings.TrimPrefix(resourceName, "data_source_")
} else {
resourceName = strings.TrimPrefix(resourceName, "resource_")
}
if _, ok := resourceNameMap[resourceName]; ok {
continue
} else {
resourceNameMap[resourceName] = struct{}{}
}
log.Infof("==> Getting %s %s attributes...", fileType, resourceName)
var resource = &schema.Resource{}
var ok bool
if isResource {
resource, ok = alicloud.Provider().(*schema.Provider).ResourcesMap[resourceName]
if !ok || resource == nil {
log.Errorf("resource %s is not found in the provider ResourceMap\n\n", resourceName)
exitCode = 1
continue
}
} else {
resource, ok = alicloud.Provider().(*schema.Provider).DataSourcesMap[resourceName]
if !ok || resource == nil {
log.Errorf("data source %s is not found in the provider DataSourcesMap\n\n", resourceName)
exitCode = 1
continue
}
}
schemaAllSet, schemaMustSet, schemaModifySet, schemaForceNewSet :=
mapset.NewSet(), mapset.NewSet(), mapset.NewSet(), mapset.NewSet()
getSchemaAttr(false, resource.Schema, &schemaAllSet, &schemaMustSet, &schemaModifySet, &schemaForceNewSet)
log.Infof("==> Getting %s %s attributes in test cases...", fileType, resourceName)
testMustSet, testModifySet, testIgnoreSet :=
mapset.NewSet(), mapset.NewSet(), mapset.NewSet()
filePath := "alicloud/"
if isResource {
filePath += "resource_" + resourceName + "_test.go"
} else {
filePath += "data_source_" + resourceName + "_test.go"
}
check := getTestCaseAttr(filePath, resourceName, &testMustSet, &testModifySet, &testIgnoreSet)
// "check" denotes the test code is using a standard template
if check {
log.Infof("==> checking %s %s attributes' coverage rate", fileType, resourceName)
if checkAttributeSet(resourceName, fileType, schemaMustSet, testMustSet,
schemaModifySet, testModifySet, schemaForceNewSet, schemaAllSet, testIgnoreSet) && isNameCorrect {
log.Infof("--- PASS!\n\n")
continue
}
}
log.Errorf("--- Failed!\n\n")
exitCode = 1
}
os.Exit(exitCode)
}
// get the schema
func getSchemaAttr(isResource bool, schema map[string]*schema.Schema,
schemaAllSet, schemaMustSet, schemaModifySet, schemaForceNewSet *mapset.Set) {
schemaAttributes := make(map[string]SchemaAttribute)
getSchemaAttributes("", schemaAttributes, schema)
for key, value := range schemaAttributes {
// "dry_run" or deperacated
if key == "dry_run" || value.Deprecated != "" {
continue
}
(*schemaAllSet).Add(key)
if value.Optional || value.Required {
(*schemaMustSet).Add(key)
if !value.ForceNew {
(*schemaModifySet).Add(key)
}
}
if value.ForceNew {
(*schemaForceNewSet).Add(key)
}
}
}
func getSchemaAttributes(rootName string, schemaAttributes map[string]SchemaAttribute,
resourceSchema map[string]*schema.Schema) {
for key, value := range resourceSchema {
if len(value.Removed) != 0 {
continue
}
if rootName != "" {
key = rootName + "." + key
}
if _, ok := schemaAttributes[key]; !ok {
schemaAttributes[key] = SchemaAttribute{
Name: key,
Type: value.Type.String(),
Optional: value.Optional,
Required: value.Required,
ForceNew: value.ForceNew,
Default: fmt.Sprint(value.Default),
Deprecated: value.Deprecated,
}
}
if value.Type == schema.TypeSet || value.Type == schema.TypeList {
if v, ok := value.Elem.(*schema.Schema); ok {
vv := schemaAttributes[key]
vv.ElemType = v.Type.String()
schemaAttributes[key] = vv
} else {
vv := schemaAttributes[key]
vv.ElemType = "Object"
schemaAttributes[key] = vv
getSchemaAttributes(key, schemaAttributes, value.Elem.(*schema.Resource).Schema)
}
}
}
}
type SchemaAttribute struct {
Name string
Type string
Optional bool
Required bool
ForceNew bool
Default string
ElemType string
Deprecated string
DocsLineNum int
Removed string
}
// get the attribute which have been tested
func getTestCaseAttr(filePath string, resourceName string,
testMustSet, testModifySet, testIgnoreSet *mapset.Set) bool {
file, err := os.Open(filePath)
if err != nil {
log.Errorf("fail to open test file %s. Error: %s", filePath, err)
return false
}
defer file.Close()
scanner := bufio.NewScanner(file)
resourceTest := ResourceTest{
resourceName: filePath,
funcs: map[string]FuncTest{},
}
line := 0
funcName := ""
stepNumber := 0
configStr := ""
inConfig := false
inFunc := false
ignoreStr := ""
inIgnore := false
for scanner.Scan() {
line += 1
text := scanner.Text()
// commented line
if commentedRegex.MatchString(text) {
continue
} else if text == "}" {
inFunc = false
} else if normalFuncRegex.MatchString(text) {
if unitFuncRegex.MatchString(text) {
continue
}
if !standardFuncRegex.MatchString(text) {
name := text[strings.Index(text, "T"):strings.Index(text, "(")]
log.Errorf("testcase %s should start with TestAccAliCloud", name)
isNameCorrect = false
}
inFunc = true
funcName = text[strings.Index(text, "T"):strings.Index(text, "(")]
stepNumber = 0
resourceTest.funcs[funcName] = FuncTest{
stepStr: map[int]string{},
stepAttributes: map[int]map[string]interface{}{},
}
}
if inFunc {
if configRegex.MatchString(text) {
configStr += text
inConfig = true
continue
}
if inConfig {
if checkRegex.MatchString(text) {
resourceTest.funcs[funcName].stepStr[stepNumber] = configStr
inConfig = false
configStr = ""
stepNumber++
} else {
// remove extra spaces and '\'
text = symbolRegex.ReplaceAllString(text, "")
if len(text) == 0 || strings.HasPrefix(text, "//") {
continue
}
configStr += text + "\n"
}
}
if ignoreRegex.MatchString(text) {
inIgnore = true
ignoreStr += text
continue
}
if inIgnore {
ignoreStr += text
if strings.Contains(text, "}") {
inIgnore = false
ignoreStr = strings.ReplaceAll(ignoreStr, "\"", "")
ignoreStr = symbolRegex.ReplaceAllString(ignoreStr, "")
attrStr := ignoreStr[strings.Index(ignoreStr, "{")+1 : strings.Index(ignoreStr, "}")]
if len(attrStr) != 0 {
attrSlice := strings.Split(attrStr, ",")
for _, v := range attrSlice {
if len(v) != 0 && v != "dry_run" {
(*testIgnoreSet).Add(v)
}
}
}
ignoreStr = ""
}
}
}
}
return parseConfig(resourceTest, testMustSet, testModifySet)
}
func parseConfig(resourceTest ResourceTest,
testMustSet, testModifySet *mapset.Set) (toCheck bool) {
for funcName, f := range resourceTest.funcs {
// attribute-value map in a test func
attributeValueMap := map[string]string{}
for configIndex := 0; configIndex < len(f.stepStr); configIndex++ {
configStr := f.stepStr[configIndex]
// the test code is not using a standard template
if !strings.Contains(configStr, "{") {
log.Infof("the test case in [%s] does not use a standard template, need manual check", resourceTest.resourceName)
return false
}
configStr = configStr[strings.Index(configStr, "{")+2 : strings.LastIndex(configStr, "}")+1]
if configStr == "{}" {
continue
}
configSlice := strings.Split(configStr, "\n")
// traverse each line of config
for i, v := range configSlice {
valueIndex := strings.Index(v, ":")
// "xxx:xxx",
if strings.Contains(v, ":") {
splitIndex := strings.Index(v, ":")
beforeCount := strings.Count(v[:splitIndex], "\"")
if beforeCount%2 != 0 {
valueIndex = -1
}
}
valueSuffix := string(v[len(v)-1])
valueStr := v[valueIndex+1 : len(v)-1]
bracketCount := strings.Count(v, "]") + strings.Count(v, "[")
if v[valueIndex+1] == '"' && strings.HasSuffix(v, "},") ||
strings.HasSuffix(v, "],") && bracketCount%2 == 1 {
valueSuffix = v[len(v)-2:]
valueStr = v[valueIndex+1 : len(v)-2]
}
// "xxx"+xx, "xxx"+xxx+"xxx, `xxx`+"xxx"+`xxx`
if strings.Contains(valueStr, "+") {
v := valueStr
index := strings.Index(v, "+")
for index != -1 {
beforeStr := v[:index]
beforeStr = strings.TrimSpace(beforeStr)
if strings.Count(beforeStr, "\"")%2 == 0 ||
(beforeStr[0] == '`' && beforeStr[len(beforeStr)-1] == '`') {
v = v[index+1:]
index = strings.Index(v, "+")
} else {
break
}
}
if index == -1 {
valueStr = strings.ReplaceAll(valueStr, "\"", "")
valueStr = strings.ReplaceAll(valueStr, "`", "")
valueStr = "\"" + valueStr + "\""
}
}
// `xxx`
if strings.HasPrefix(valueStr, "`") && strings.HasSuffix(valueStr, "`") {
valueStr = "\"" + valueStr[:len(valueStr)-1] + "\"" + valueSuffix
} else {
valueStr += valueSuffix
}
// value is a map or slice
for i := 0; i < len(matchMap); i++ {
k := matchMap[i][0]
v := matchMap[i][1]
if strings.Contains(valueStr, k) {
valueStr = strings.ReplaceAll(valueStr, k, v)
}
}
// value with variable or func
if variableRegex.MatchString(valueStr) || valueFuncRegex.MatchString(valueStr) {
valueStr = strings.ReplaceAll(valueStr, "\\\"", "*")
valueStr = strings.ReplaceAll(valueStr, "\"", "")
valueStr = "\"" + valueStr + "\"" + valueSuffix
}
// "xxx/xxx", "xxx/"xxx"
if valueOnlySymbol.MatchString(valueStr) {
valueStr = strings.ReplaceAll(valueStr, "\\", "*")
valueStr = strings.ReplaceAll(valueStr, "\"", "*")
valueStr = "\"" + valueStr[1:len(valueStr)-1] + "\"" + valueSuffix
}
configSlice[i] = v[:valueIndex+1] + valueStr
}
configStr = strings.Join(configSlice, "")
configRune := []rune(configStr)
// match the bracket
if toCheck = bracketMatch(funcName, configIndex, configRune); !toCheck {
return toCheck
}
configStr = string(configRune)
jsonData := []byte(configStr)
var v interface{}
err := json.Unmarshal(jsonData, &v)
if err != nil {
log.Errorf("fail to unmarshal func %v's number %v config: \n%s\n%s", funcName, configIndex, configStr, err)
return false
}
data := v.(map[string]interface{})
f.stepAttributes[configIndex] = data
parseAttr(configIndex, "", data, attributeValueMap, testMustSet, testModifySet)
}
}
return true
}
func bracketMatch(funcName string, configIndex int, s []rune) bool {
var stack []string
for i := 0; i < len(s); i++ {
r := string(s[i])
switch r {
// remove the extra comma
case ",":
if i != len(s)-1 && (s[i+1] == '}' || s[i+1] == ']') {
s[i] = rune(' ')
}
case "{", "[":
stack = append(stack, r)
case "}", "]":
{
if len(stack) == 0 {
log.Errorf("fail to math bracket [ ] in func %s's number %d config:%s\n", funcName, configIndex, string(s))
return false
}
temp := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if temp == "[" && r == "}" {
s[i] = ']'
}
if bracket[temp] == r {
break
}
}
}
}
return true
}
func parseAttr(configIndex int, rootName string, data interface{}, attributeValueMap map[string]string,
testMustSet, testModifySet *mapset.Set) {
if d, ok := data.(map[string]interface{}); ok {
for key, value := range d {
if rootName != "" {
key = rootName + "." + key
}
(*testMustSet).Add(key)
// check if the attribute has been updated
if v, ok := attributeValueMap[key]; ok {
if fmt.Sprintf("%v", value) != v {
(*testModifySet).Add(key)
}
} else if configIndex > 0 {
(*testModifySet).Add(key)
}
attributeValueMap[key] = fmt.Sprintf("%v", value)
parseAttr(configIndex, key, value, attributeValueMap, testMustSet, testModifySet)
}
} else if d, ok := data.([]interface{}); ok {
for _, v := range d {
attributeValueMap[rootName] = fmt.Sprintf("%v", data)
parseAttr(configIndex, rootName, v, attributeValueMap, testMustSet, testModifySet)
}
}
}
var (
commentedRegex = regexp.MustCompile("^[\t]*//")
normalFuncRegex = regexp.MustCompile("^func Test(.*)")
unitFuncRegex = regexp.MustCompile("^func TestUnit(.*)")
standardFuncRegex = regexp.MustCompile("^func TestAccAliCloud(.*)")
configRegex = regexp.MustCompile("(^[\t]*)Config:(.*)")
checkRegex = regexp.MustCompile("(.*)Check:(.*)")
ignoreRegex = regexp.MustCompile("(.*)ImportStateVerifyIgnore:(.*)")
hasNumRegex = regexp.MustCompile(`[0-9]+`)
attrRegex = regexp.MustCompile("^([{]*)\"([a-zA-Z_0-9-.]+)\":(.*)")
symbolRegex = regexp.MustCompile(`\s`)
variableRegex = regexp.MustCompile("(^[a-zA-Z_0-9]+)|(\"[+]\")")
valueFuncRegex = regexp.MustCompile("[(].*[\"].*[\"].*[)]")
valueOnlySymbol = regexp.MustCompile(`.*([^\\\"])(\\)([^\\\"]).*`)
bracket = map[string]string{
"{": "}",
"[": "]",
}
matchMap = map[int][]string{
0: []string{"[]string{", "["},
1: []string{"[]map[string]interface{}{", "["},
2: []string{"[]map[string]string{", "["},
3: []string{"[]interface{}{", "["},
4: []string{"map[string]interface{}{", "{"},
5: []string{"map[string]string{", "{"},
6: []string{"\\n", ""},
7: []string{"`${map(", "["},
8: []string{")}`", "]"},
}
isNameCorrect = true
)
type ResourceTest struct {
resourceName string
funcs map[string]FuncTest
}
type FuncTest struct {
stepStr map[int]string
stepAttributes map[int]map[string]interface{}
}
func checkAttributeSet(resourceName string, fileType string, schemaMustSet, testMustSet,
schemaModifySet, testModifySet, schemaForceNewSet, schemaAllSet, testIgnoreSet mapset.Set) bool {
isFullCover, isIgnoreLegal, isAllModified := true, true, true
notCoverSlice := schemaMustSet.Difference(testMustSet).ToSlice()
if len(notCoverSlice) != 0 {
isFullCover = false
schemaCount := float64(len(schemaMustSet.ToSlice()))
notCoverCount := float64(len(notCoverSlice))
coverageRate := 1 - (notCoverCount / schemaCount)
log.Infof("resource %s attributes has %.2f%% testing coverage rate ", resourceName, coverageRate*100)
notCoverStr, _ := json.Marshal(notCoverSlice)
log.Errorf("resource %s attributes %v missing test cases", resourceName, string(notCoverStr))
} else {
log.Infof("resource %s attributes has 100%% testing coverage rate ", resourceName)
}
forceNewButIgnore := schemaForceNewSet.Intersect(testIgnoreSet).ToSlice()
if len(forceNewButIgnore) != 0 {
isIgnoreLegal = false
forceNewButIgnoreStr, _ := json.Marshal(forceNewButIgnore)
// TODO: 从READ方法区分是否是私有属性,从而区分应该修改ignore数组还是应该修改资源属性
log.Errorf("resource %s [ForceNew] attributes %v are in ImportStateVerifyIgnore array ", resourceName, string(forceNewButIgnoreStr))
}
redundantAttr := testIgnoreSet.Difference(schemaAllSet).ToSlice()
redundantAttrFinal := []string{}
for _, v := range redundantAttr {
vStr := v.(string)
if hasNumRegex.MatchString(vStr) && strings.Contains(vStr, ".") {
parts := strings.Split(vStr, ".")
attrStr := parts[0]
for _, subAttr := range parts[1:] {
if _, err := strconv.Atoi(subAttr); err == nil {
continue
} else {
attrStr += "." + subAttr
}
}
if !schemaAllSet.Contains(attrStr) {
redundantAttrFinal = append(redundantAttrFinal, vStr)
}
} else {
redundantAttrFinal = append(redundantAttrFinal, vStr)
}
}
if len(redundantAttrFinal) != 0 {
isIgnoreLegal = false
redundantAttrFinalStr, _ := json.Marshal(redundantAttrFinal)
log.Errorf("resource %s attributes %v should not in ImportStateVerifyIgnore array", resourceName, string(redundantAttrFinalStr))
}
schemaModifySet = schemaModifySet.Difference(testIgnoreSet)
notModifySlice := schemaModifySet.Difference(testModifySet).ToSlice()
if len(notModifySlice) != 0 {
isAllModified = false
schemaCount := float64(len(schemaModifySet.ToSlice()))
notCoverCount := float64(len(notModifySlice))
coverageRate := 1 - (notCoverCount / schemaCount)
log.Infof("resource %s attributes has %.2f%% modified coverage rate ", resourceName, coverageRate*100)
notModifyStr, _ := json.Marshal(notModifySlice)
log.Errorf("resource %s attributes %v missing modification in test cases", resourceName, string(notModifyStr))
} else {
log.Infof("resource %s attributes has 100%% modified coverage rate ", resourceName)
}
return isFullCover && isIgnoreLegal && isAllModified
}
func testAll(diff *diffparser.Diff) {
file, err := os.Open("filename.txt")
if err != nil {
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
diff.Files = make([]*diffparser.DiffFile, 0, 800)
line := 0
for scanner.Scan() {
text := scanner.Text()
if strings.HasPrefix(text, "resource_alicloud") {
diff.Files = append(diff.Files, &diffparser.DiffFile{NewName: "alicloud/" + text})
}
line++
}
}