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  }