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  }