github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/spyglass/lenses/restcoverage/restcoverage.go (about) 1 /* 2 Copyright 2019 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 restcoverage 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "html/template" 24 "path/filepath" 25 26 "github.com/sirupsen/logrus" 27 28 "sigs.k8s.io/prow/pkg/config" 29 "sigs.k8s.io/prow/pkg/spyglass/api" 30 "sigs.k8s.io/prow/pkg/spyglass/lenses" 31 ) 32 33 const ( 34 // DefaultWarningThreshold returns default threshold for warning class 35 DefaultWarningThreshold = 40.0 36 // DefaultErrorThreshold returns default threshold for error class 37 DefaultErrorThreshold = 10.0 38 ) 39 40 type Lens struct{} 41 42 // Coverage represents a REST API statistics 43 type Coverage struct { 44 UniqueHits int `json:"uniqueHits"` 45 ExpectedUniqueHits int `json:"expectedUniqueHits"` 46 Percent float64 `json:"percent"` 47 Endpoints map[string]map[string]*Endpoint `json:"endpoints"` 48 *Thresholds 49 } 50 51 // Endpoint represents a basic statistics structure which is used to calculate REST API coverage 52 type Endpoint struct { 53 Params `json:"params"` 54 UniqueHits int `json:"uniqueHits"` 55 ExpectedUniqueHits int `json:"expectedUniqueHits"` 56 Percent float64 `json:"percent"` 57 MethodCalled bool `json:"methodCalled"` 58 } 59 60 // Params represents body and query parameters 61 type Params struct { 62 Body Trie `json:"body"` 63 Query Trie `json:"query"` 64 } 65 66 // Trie represents a coverage data 67 type Trie struct { 68 Root Node `json:"root"` 69 UniqueHits int `json:"uniqueHits"` 70 ExpectedUniqueHits int `json:"expectedUniqueHits"` 71 Size int `json:"size"` 72 Height int `json:"height"` 73 } 74 75 // Node represents a single data unit for coverage report 76 type Node struct { 77 Hits int `json:"hits"` 78 Items map[string]*Node `json:"items,omitempty"` 79 } 80 81 // Thresholds sets color (yellow or red) to highlight coverage percent 82 type Thresholds struct { 83 Warning float64 `json:"threshold_warning"` 84 Error float64 `json:"threshold_error"` 85 } 86 87 func init() { 88 lenses.RegisterLens(Lens{}) 89 } 90 91 // Config returns the lens's configuration. 92 func (lens Lens) Config() lenses.LensConfig { 93 return lenses.LensConfig{ 94 Title: "REST API coverage report", 95 Name: "restcoverage", 96 Priority: 0, 97 } 98 } 99 100 // Header returns the content of <head> 101 func (lens Lens) Header(artifacts []api.Artifact, resourceDir string, config json.RawMessage, spyglassConfig config.Spyglass) string { 102 t, err := template.ParseFiles(filepath.Join(resourceDir, "template.html")) 103 if err != nil { 104 return fmt.Sprintf("<!-- FAILED LOADING HEADER: %v -->", err) 105 } 106 var buf bytes.Buffer 107 if err := t.ExecuteTemplate(&buf, "header", nil); err != nil { 108 return fmt.Sprintf("<!-- FAILED EXECUTING HEADER TEMPLATE: %v -->", err) 109 } 110 return buf.String() 111 } 112 113 func (lens Lens) Callback(artifacts []api.Artifact, resourceDir string, data string, config json.RawMessage, spyglassConfig config.Spyglass) string { 114 return "" 115 } 116 117 // Body returns the displayed HTML for the <body> 118 func (lens Lens) Body(artifacts []api.Artifact, resourceDir string, data string, config json.RawMessage, spyglassConfig config.Spyglass) string { 119 var ( 120 cov Coverage 121 err error 122 ) 123 124 cov.Thresholds, err = getThresholds(config) 125 if err != nil { 126 logrus.Errorf("Invalid config: %v", err) 127 return fmt.Sprintf("Invalid config: %v", err) 128 } 129 130 if len(artifacts) != 1 { 131 logrus.Errorf("Invalid artifacts: %v", artifacts) 132 return "Either artifact is not passed or it is too many of them! Let's call it an error :)" 133 } 134 135 covJSON, err := artifacts[0].ReadAll() 136 if err != nil { 137 logrus.Errorf("Failed to read artifact: %v", err) 138 return fmt.Sprintf("Failed to read artifact: %v", err) 139 } 140 141 err = json.Unmarshal(covJSON, &cov) 142 if err != nil { 143 logrus.Errorf("Failed to unmarshall coverage report: %v", err) 144 return fmt.Sprintf("Failed to unmarshall coverage report: %v", err) 145 } 146 147 restcoverageTemplate, err := template.ParseFiles(filepath.Join(resourceDir, "template.html")) 148 if err != nil { 149 logrus.WithError(err).Error("Error executing template.") 150 return fmt.Sprintf("Failed to load template file: %v", err) 151 } 152 153 var buf bytes.Buffer 154 if err := restcoverageTemplate.ExecuteTemplate(&buf, "body", cov); err != nil { 155 logrus.WithError(err).Error("Error executing template.") 156 } 157 158 return buf.String() 159 } 160 161 func getThresholds(config json.RawMessage) (*Thresholds, error) { 162 var thresholds Thresholds 163 164 if len(config) == 0 { 165 thresholds.Error = DefaultErrorThreshold 166 thresholds.Warning = DefaultWarningThreshold 167 return &thresholds, nil 168 } 169 170 if err := json.Unmarshal(config, &thresholds); err != nil { 171 return nil, err 172 } 173 174 if thresholds.Error > thresholds.Warning { 175 return nil, fmt.Errorf("errorThreshold %.2f is bigger than warningThreshold %.2f", thresholds.Error, thresholds.Warning) 176 } 177 178 return &thresholds, nil 179 }