github.com/abayer/test-infra@v0.0.5/experiment/coverage/apicoverage.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "bufio" 21 "encoding/csv" 22 "flag" 23 "fmt" 24 "io" 25 "io/ioutil" 26 "log" 27 "net/http" 28 "net/url" 29 "os" 30 "regexp" 31 "strconv" 32 "strings" 33 34 "github.com/go-openapi/spec" 35 "github.com/golang/glog" 36 ) 37 38 var ( 39 openAPIFile = flag.String("openapi", "https://raw.githubusercontent.com/kubernetes/kubernetes/master/api/openapi-spec/swagger.json", "URL to openapi-spec of Kubernetes") 40 outputCoveredAPIs = flag.Bool("output-covered-apis", false, "Output the list of covered APIs") 41 minCoverage = flag.Int("minimum-coverage", 0, "This command fails if the number of covered APIs is less than this option ratio(percent)") 42 restLog = flag.String("restlog", "", "File path to REST API operation log of Kubernetes") 43 logType = flag.String("logtype", "e2e", "Type of REST API operation log of Kubernetes(e2e, apiserver)") 44 ) 45 46 type apiData struct { 47 Method string 48 URL string 49 } 50 51 type apiArray []apiData 52 53 func parseOpenAPI(rawdata []byte) apiArray { 54 var swaggerSpec spec.Swagger 55 var apisOpenapi apiArray 56 57 err := swaggerSpec.UnmarshalJSON(rawdata) 58 if err != nil { 59 log.Fatal(err) 60 } 61 62 for path, pathItem := range swaggerSpec.Paths.Paths { 63 // Some paths contain "/" at the end of swagger spec, here removes "/" for comparing them easily later. 64 path = strings.TrimRight(path, "/") 65 66 // Standard HTTP methods: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#path-item-object 67 methods := []string{"get", "put", "post", "delete", "options", "head", "patch"} 68 for _, method := range methods { 69 methodSpec, err := pathItem.JSONLookup(method) 70 if err != nil { 71 log.Fatal(err) 72 } 73 t, ok := methodSpec.(*spec.Operation) 74 if ok == false { 75 log.Fatal("Failed to convert methodSpec.") 76 } 77 if t == nil { 78 continue 79 } 80 method := strings.ToUpper(method) 81 api := apiData{ 82 Method: method, 83 URL: path, 84 } 85 apisOpenapi = append(apisOpenapi, api) 86 } 87 } 88 return apisOpenapi 89 } 90 91 func getOpenAPISpec(url string) apiArray { 92 resp, err := http.Get(url) 93 if err != nil { 94 log.Fatal(err) 95 } 96 bytes, err := ioutil.ReadAll(resp.Body) 97 if err != nil { 98 log.Fatal(err) 99 } 100 return parseOpenAPI(bytes) 101 } 102 103 // I0919 15:34:14.943642 6611 round_trippers.go:414] GET https://172.27.138.63:6443/api/v1/namespaces/kube-system/replicationcontrollers 104 var reE2eAPILog = regexp.MustCompile(`round_trippers.go:\d+\] (GET|PUT|POST|DELETE|OPTIONS|HEAD|PATCH) (\S+)`) 105 106 func parseE2eAPILog(fp io.Reader) apiArray { 107 var apisLog apiArray 108 var err error 109 110 reader := bufio.NewReaderSize(fp, 4096) 111 for line := ""; err == nil; line, err = reader.ReadString('\n') { 112 result := reE2eAPILog.FindSubmatch([]byte(line)) 113 if len(result) == 0 { 114 continue 115 } 116 method := strings.ToUpper(string(result[1])) 117 rawurl := string(result[2]) 118 parsedURL, err := url.Parse(rawurl) 119 if err != nil { 120 log.Fatal(err) 121 } 122 api := apiData{ 123 Method: method, 124 URL: parsedURL.Path, 125 } 126 apisLog = append(apisLog, api) 127 } 128 return apisLog 129 } 130 131 // I0413 12:10:56.612005 1 wrap.go:42] PUT /apis/apiregistration.k8s.io/v1/apiservices/v1.apps/status: (1.671974ms) 200 [[kube-apiserver/v1.11.0 (linux/amd64) kubernetes/7297c1c] 127.0.0.1:44356] 132 var reAPIServerLog = regexp.MustCompile(`wrap.go:\d+\] (GET|PUT|POST|DELETE|OPTIONS|HEAD|PATCH) (\S+)`) 133 134 func parseAPIServerLog(fp io.Reader) apiArray { 135 var apisLog apiArray 136 var err error 137 138 reader := bufio.NewReaderSize(fp, 4096) 139 for line := ""; err == nil; line, err = reader.ReadString('\n') { 140 result := reAPIServerLog.FindSubmatch([]byte(line)) 141 if len(result) == 0 { 142 continue 143 } 144 method := strings.ToUpper(string(result[1])) 145 rawurl := strings.Replace(string(result[2]), ":", "", 1) 146 parsedURL, err := url.Parse(rawurl) 147 if err != nil { 148 log.Fatal(err) 149 } 150 api := apiData{ 151 Method: method, 152 URL: parsedURL.Path, 153 } 154 apisLog = append(apisLog, api) 155 } 156 return apisLog 157 } 158 159 func getAPILog(restlog string) apiArray { 160 var fp *os.File 161 var err error 162 163 fp, err = os.Open(restlog) 164 if err != nil { 165 log.Fatal(err) 166 } 167 defer fp.Close() 168 169 if *logType == "e2e" { 170 return parseE2eAPILog(fp) 171 } 172 return parseAPIServerLog(fp) 173 } 174 175 var reOpenapi = regexp.MustCompile(`({\S+?})`) 176 177 func getTestedAPIs(apisOpenapi, apisLogs apiArray) apiArray { 178 var found bool 179 var apisTested apiArray 180 181 for _, openapi := range apisOpenapi { 182 regURL := reOpenapi.ReplaceAllLiteralString(openapi.URL, `[^/\s]+`) + `$` 183 reg := regexp.MustCompile(regURL) 184 found = false 185 for _, log := range apisLogs { 186 if openapi.Method != log.Method { 187 continue 188 } 189 if !reg.MatchString(log.URL) { 190 continue 191 } 192 found = true 193 apisTested = append(apisTested, openapi) 194 break 195 } 196 if found { 197 continue 198 } 199 } 200 return apisTested 201 } 202 203 func getTestedAPIsByLevel(negative bool, reg *regexp.Regexp, apisOpenapi, apisTested apiArray) (apiArray, apiArray) { 204 var apisTestedByLevel apiArray 205 var apisAllByLevel apiArray 206 207 for _, openapi := range apisTested { 208 if (negative == false && reg.MatchString(openapi.URL)) || 209 (negative == true && !reg.MatchString(openapi.URL)) { 210 apisTestedByLevel = append(apisTestedByLevel, openapi) 211 } 212 } 213 for _, openapi := range apisOpenapi { 214 if (negative == false && reg.MatchString(openapi.URL)) || 215 (negative == true && !reg.MatchString(openapi.URL)) { 216 apisAllByLevel = append(apisAllByLevel, openapi) 217 } 218 } 219 return apisTestedByLevel, apisAllByLevel 220 } 221 222 type coverageData struct { 223 Total string 224 Tested string 225 Untested string 226 Coverage string 227 } 228 229 func getCoverageByLevel(apisTested, apisAll apiArray) coverageData { 230 var coverage coverageData 231 232 coverage.Total = fmt.Sprint(len(apisAll)) 233 coverage.Tested = fmt.Sprint(len(apisTested)) 234 coverage.Untested = fmt.Sprint(len(apisAll) - len(apisTested)) 235 coverage.Coverage = fmt.Sprint(100 * len(apisTested) / len(apisAll)) 236 237 return coverage 238 } 239 240 //NOTE: This is messy, but the regex doesn't support negative lookahead(?!) on golang. 241 //This is just a workaround. 242 var reNotStableAPI = regexp.MustCompile(`\S+(alpha|beta)\S+`) 243 var reAlphaAPI = regexp.MustCompile(`\S+alpha\S+`) 244 var reBetaAPI = regexp.MustCompile(`\S+beta\S+`) 245 246 func outputCoverage(apisOpenapi, apisTested apiArray) { 247 apisTestedByStable, apisAllByStable := getTestedAPIsByLevel(true, reNotStableAPI, apisOpenapi, apisTested) 248 apisTestedByAlpha, apisAllByAlpha := getTestedAPIsByLevel(false, reAlphaAPI, apisOpenapi, apisTested) 249 apisTestedByBeta, apisAllByBeta := getTestedAPIsByLevel(false, reBetaAPI, apisOpenapi, apisTested) 250 251 coverageAll := getCoverageByLevel(apisTested, apisOpenapi) 252 coverageStable := getCoverageByLevel(apisTestedByStable, apisAllByStable) 253 coverageAlpha := getCoverageByLevel(apisTestedByAlpha, apisAllByAlpha) 254 coverageBeta := getCoverageByLevel(apisTestedByBeta, apisAllByBeta) 255 256 records := [][]string{ 257 {"API", "TOTAL", "TESTED", "UNTESTED", "COVERAGE(%)"}, 258 {"ALL", coverageAll.Total, coverageAll.Tested, coverageAll.Untested, coverageAll.Coverage}, 259 {"STABLE", coverageStable.Total, coverageStable.Tested, coverageStable.Untested, coverageStable.Coverage}, 260 {"Alpha", coverageAlpha.Total, coverageAlpha.Tested, coverageAlpha.Untested, coverageAlpha.Coverage}, 261 {"Beta", coverageBeta.Total, coverageBeta.Tested, coverageBeta.Untested, coverageBeta.Coverage}, 262 } 263 w := csv.NewWriter(os.Stdout) 264 w.WriteAll(records) 265 266 actualCoverage, _ := strconv.Atoi(coverageAll.Coverage) 267 if *minCoverage > int(actualCoverage) { 268 log.Fatalf("The API coverage(%d) is lower than the specified one(%d).", actualCoverage, *minCoverage) 269 } 270 } 271 272 func main() { 273 flag.Parse() 274 if len(*restLog) == 0 { 275 glog.Fatal("need to set '--restlog'") 276 } 277 if *logType != "e2e" && *logType != "apiserver" { 278 glog.Fatal("need to specify e2e or apiserver with '--logtype'") 279 } 280 281 apisOpenapi := getOpenAPISpec(*openAPIFile) 282 apisLogs := getAPILog(*restLog) 283 apisTested := getTestedAPIs(apisOpenapi, apisLogs) 284 outputCoverage(apisOpenapi, apisTested) 285 if *outputCoveredAPIs { 286 for _, openapi := range apisTested { 287 fmt.Printf("%s %s\n", openapi.Method, openapi.URL) 288 } 289 } 290 }