mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-04 04:08:16 +00:00 
			
		
		
		
	Since we have a Kubernetes-specific fork of go-yaml, use that consistently throughout the project. This doesn't eliminate the dependencies on gopkg.in/yaml, which are still used indirectly; but it ensures that the whole project benefits from fixes or changes to sigs.k8s.io/yaml. Signed-off-by: Stephen Kitt <skitt@redhat.com>
		
			
				
	
	
		
			297 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			297 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
Copyright 2019 The Kubernetes Authors.
 | 
						|
 | 
						|
Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
you may not use this file except in compliance with the License.
 | 
						|
You may obtain a copy of the License at
 | 
						|
 | 
						|
    http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
Unless required by applicable law or agreed to in writing, software
 | 
						|
distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
See the License for the specific language governing permissions and
 | 
						|
limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
package main
 | 
						|
 | 
						|
import (
 | 
						|
	"encoding/json"
 | 
						|
	"flag"
 | 
						|
	"fmt"
 | 
						|
	"go/ast"
 | 
						|
	"go/parser"
 | 
						|
	"go/token"
 | 
						|
	"io"
 | 
						|
	"log"
 | 
						|
	"os"
 | 
						|
	"regexp"
 | 
						|
	"sort"
 | 
						|
	"strings"
 | 
						|
	"text/template"
 | 
						|
 | 
						|
	"github.com/onsi/ginkgo/v2/types"
 | 
						|
 | 
						|
	yaml "sigs.k8s.io/yaml/goyaml.v2"
 | 
						|
)
 | 
						|
 | 
						|
// ConformanceData describes the structure of the conformance.yaml file
 | 
						|
type ConformanceData struct {
 | 
						|
	// A URL to the line of code in the kube src repo for the test. Omitted from the YAML to avoid exposing line number.
 | 
						|
	URL string `yaml:"-"`
 | 
						|
	// Extracted from the "Testname:" comment before the test
 | 
						|
	TestName string
 | 
						|
	// CodeName is taken from the actual ginkgo descriptions, e.g. `[sig-apps] Foo should bar [Conformance]`
 | 
						|
	CodeName string
 | 
						|
	// Extracted from the "Description:" comment before the test
 | 
						|
	Description string
 | 
						|
	// Version when this test is added or modified ex: v1.12, v1.13
 | 
						|
	Release string
 | 
						|
	// File is the filename where the test is defined. We intentionally don't save the line here to avoid meaningless changes.
 | 
						|
	File string
 | 
						|
}
 | 
						|
 | 
						|
var (
 | 
						|
	baseURL = flag.String("url", "https://github.com/kubernetes/kubernetes/tree/master/", "location of the current source")
 | 
						|
	k8sPath = flag.String("source", "", "location of the current source on the current machine")
 | 
						|
	confDoc = flag.Bool("docs", false, "write a conformance document")
 | 
						|
	version = flag.String("version", "v1.9", "version of this conformance document")
 | 
						|
 | 
						|
	// If a test name contains any of these tags, it is ineligible for promotion to conformance
 | 
						|
	regexIneligibleTags = regexp.MustCompile(`\[(Alpha|Feature:[^\]]+|Flaky)\]`)
 | 
						|
 | 
						|
	// Conformance comments should be within this number of lines to the call itself.
 | 
						|
	// Allowing for more than one in case a spare comment or two is below it.
 | 
						|
	conformanceCommentsLineWindow = 5
 | 
						|
 | 
						|
	seenLines map[string]struct{}
 | 
						|
)
 | 
						|
 | 
						|
type frame struct {
 | 
						|
	// File and Line are the file name and line number of the
 | 
						|
	// location in this frame. For non-leaf frames, this will be
 | 
						|
	// the location of a call. These may be the empty string and
 | 
						|
	// zero, respectively, if not known.
 | 
						|
	File string
 | 
						|
	Line int
 | 
						|
}
 | 
						|
 | 
						|
func main() {
 | 
						|
	flag.Parse()
 | 
						|
 | 
						|
	if len(flag.Args()) < 1 {
 | 
						|
		log.Fatalln("Requires the name of the test details file as first and only argument.")
 | 
						|
	}
 | 
						|
	testDetailsFile := flag.Args()[0]
 | 
						|
	f, err := os.Open(testDetailsFile)
 | 
						|
	if err != nil {
 | 
						|
		log.Fatalf("Failed to open file %v: %v", testDetailsFile, err)
 | 
						|
	}
 | 
						|
	defer f.Close()
 | 
						|
 | 
						|
	seenLines = map[string]struct{}{}
 | 
						|
	dec := json.NewDecoder(f)
 | 
						|
	testInfos := []*ConformanceData{}
 | 
						|
	for {
 | 
						|
		var spec *types.SpecReport
 | 
						|
		if err := dec.Decode(&spec); err == io.EOF {
 | 
						|
			break
 | 
						|
		} else if err != nil {
 | 
						|
			log.Fatal(err)
 | 
						|
		}
 | 
						|
 | 
						|
		if isConformance(spec) {
 | 
						|
			testInfo := getTestInfo(spec)
 | 
						|
			if testInfo != nil {
 | 
						|
				testInfos = append(testInfos, testInfo)
 | 
						|
				if err := validateTestName(testInfo.CodeName); err != nil {
 | 
						|
					log.Fatal(err)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	sort.Slice(testInfos, func(i, j int) bool { return testInfos[i].CodeName < testInfos[j].CodeName })
 | 
						|
	saveAllTestInfo(testInfos)
 | 
						|
}
 | 
						|
 | 
						|
func isConformance(spec *types.SpecReport) bool {
 | 
						|
	return strings.Contains(getTestName(spec), "[Conformance]")
 | 
						|
}
 | 
						|
 | 
						|
func getTestInfo(spec *types.SpecReport) *ConformanceData {
 | 
						|
	var c *ConformanceData
 | 
						|
	var err error
 | 
						|
	// The key to this working is that we don't need to parse every file or walk
 | 
						|
	// every types.CodeLocation. The LeafNodeLocation is going to be file:line which
 | 
						|
	// attached to the comment that we want.
 | 
						|
	leafNodeLocation := spec.LeafNodeLocation
 | 
						|
	frame := frame{
 | 
						|
		File: leafNodeLocation.FileName,
 | 
						|
		Line: leafNodeLocation.LineNumber,
 | 
						|
	}
 | 
						|
	c, err = getConformanceData(frame)
 | 
						|
	if err != nil {
 | 
						|
		log.Printf("Error looking for conformance data: %v", err)
 | 
						|
	}
 | 
						|
	if c == nil {
 | 
						|
		log.Printf("Did not find test info for spec: %#v\n", getTestName(spec))
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	c.CodeName = getTestName(spec)
 | 
						|
	return c
 | 
						|
}
 | 
						|
 | 
						|
func getTestName(spec *types.SpecReport) string {
 | 
						|
	return strings.Join(spec.ContainerHierarchyTexts[0:], " ") + " " + spec.LeafNodeText
 | 
						|
}
 | 
						|
 | 
						|
func saveAllTestInfo(dataSet []*ConformanceData) {
 | 
						|
	if *confDoc {
 | 
						|
		// Note: this assumes that you're running from the root of the kube src repo
 | 
						|
		templ, err := template.ParseFiles("./test/conformance/cf_header.md")
 | 
						|
		if err != nil {
 | 
						|
			fmt.Printf("Error reading the Header file information: %s\n\n", err)
 | 
						|
		}
 | 
						|
		data := struct {
 | 
						|
			Version string
 | 
						|
		}{
 | 
						|
			Version: *version,
 | 
						|
		}
 | 
						|
		templ.Execute(os.Stdout, data)
 | 
						|
 | 
						|
		for _, data := range dataSet {
 | 
						|
			fmt.Printf("## [%s](%s)\n\n", data.TestName, data.URL)
 | 
						|
			fmt.Printf("- Added to conformance in release %s\n", data.Release)
 | 
						|
			fmt.Printf("- Defined in code as: %s\n\n", data.CodeName)
 | 
						|
			fmt.Printf("%s\n\n", data.Description)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// Serialize the list as a whole. Generally meant to end up as conformance.txt which tracks the set of tests.
 | 
						|
	b, err := yaml.Marshal(dataSet)
 | 
						|
	if err != nil {
 | 
						|
		log.Printf("Error marshalling data into YAML: %v", err)
 | 
						|
	}
 | 
						|
	fmt.Println(string(b))
 | 
						|
}
 | 
						|
 | 
						|
func getConformanceData(targetFrame frame) (*ConformanceData, error) {
 | 
						|
	root := *k8sPath
 | 
						|
	if !strings.HasSuffix(root, string(os.PathSeparator)) {
 | 
						|
		root += string(os.PathSeparator)
 | 
						|
	}
 | 
						|
 | 
						|
	trimmedFile := strings.TrimPrefix(targetFrame.File, root)
 | 
						|
	targetFrame.File = trimmedFile
 | 
						|
 | 
						|
	freader, err := os.Open(targetFrame.File)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	defer freader.Close()
 | 
						|
 | 
						|
	cd, err := scanFileForFrame(targetFrame.File, freader, targetFrame)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	if cd != nil {
 | 
						|
		return cd, nil
 | 
						|
	}
 | 
						|
 | 
						|
	return nil, nil
 | 
						|
}
 | 
						|
 | 
						|
// scanFileForFrame will scan the target and look for a conformance comment attached to the function
 | 
						|
// described by the target frame. If the comment can't be found then nil, nil is returned.
 | 
						|
func scanFileForFrame(filename string, src interface{}, targetFrame frame) (*ConformanceData, error) {
 | 
						|
	fset := token.NewFileSet() // positions are relative to fset
 | 
						|
	f, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	cmap := ast.NewCommentMap(fset, f, f.Comments)
 | 
						|
	for _, cs := range cmap {
 | 
						|
		for _, c := range cs {
 | 
						|
			if cd := tryCommentGroupAndFrame(fset, c, targetFrame); cd != nil {
 | 
						|
				return cd, nil
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil, nil
 | 
						|
}
 | 
						|
 | 
						|
func validateTestName(s string) error {
 | 
						|
	matches := regexIneligibleTags.FindAllString(s, -1)
 | 
						|
	if matches != nil {
 | 
						|
		return fmt.Errorf("'%s' cannot have invalid tags %v", s, strings.Join(matches, ","))
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func tryCommentGroupAndFrame(fset *token.FileSet, cg *ast.CommentGroup, f frame) *ConformanceData {
 | 
						|
	if !shouldProcessCommentGroup(fset, cg, f) {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	// Each file/line will either be some helper function (not a conformance comment) or apply to just a single test. Don't revisit.
 | 
						|
	if seenLines != nil {
 | 
						|
		seenLines[fmt.Sprintf("%v:%v", f.File, f.Line)] = struct{}{}
 | 
						|
	}
 | 
						|
	cd := commentToConformanceData(cg.Text())
 | 
						|
	if cd == nil {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	cd.URL = fmt.Sprintf("%s%s#L%d", *baseURL, f.File, f.Line)
 | 
						|
	cd.File = f.File
 | 
						|
	return cd
 | 
						|
}
 | 
						|
 | 
						|
func shouldProcessCommentGroup(fset *token.FileSet, cg *ast.CommentGroup, f frame) bool {
 | 
						|
	lineDiff := f.Line - fset.Position(cg.End()).Line
 | 
						|
	return lineDiff > 0 && lineDiff <= conformanceCommentsLineWindow
 | 
						|
}
 | 
						|
 | 
						|
func commentToConformanceData(comment string) *ConformanceData {
 | 
						|
	lines := strings.Split(comment, "\n")
 | 
						|
	descLines := []string{}
 | 
						|
	cd := &ConformanceData{}
 | 
						|
	var curLine string
 | 
						|
	for _, line := range lines {
 | 
						|
		line = strings.TrimSpace(line)
 | 
						|
		if len(line) == 0 {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if sline := regexp.MustCompile("^Testname\\s*:\\s*").Split(line, -1); len(sline) == 2 {
 | 
						|
			curLine = "Testname"
 | 
						|
			cd.TestName = sline[1]
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if sline := regexp.MustCompile("^Release\\s*:\\s*").Split(line, -1); len(sline) == 2 {
 | 
						|
			curLine = "Release"
 | 
						|
			cd.Release = sline[1]
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if sline := regexp.MustCompile("^Description\\s*:\\s*").Split(line, -1); len(sline) == 2 {
 | 
						|
			curLine = "Description"
 | 
						|
			descLines = append(descLines, sline[1])
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		// Line has no header
 | 
						|
		if curLine == "Description" {
 | 
						|
			descLines = append(descLines, line)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if cd.TestName == "" {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	cd.Description = strings.Join(descLines, " ")
 | 
						|
	return cd
 | 
						|
}
 |