Ctrl K
ComunidadGuías…

mock_miss_phrase_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"io/fs"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"testing"
)

// missPhrase is what users and CI scripts grep keploy's output for to fail a
// run on a mock miss. keploy's own lines must use it only for an actual miss.
const missPhrase = "mock miss"

// missLiteral is one string literal in keploy's non-test Go source: the file
// (slash-separated, from the module root), the top-level function or method
// it sits in ("" at package level) and the literal's text.
type missLiteral struct {
	file, fn, text string
}

// missLine says which miss a registered literal reports, and how many times
// the literal is in its function.
type missLine struct {
	n   int
	why string
}

// missLines is the registry of the lines that report an actual miss: every
// string literal in keploy's source that contains missPhrase, in any case,
// must be one of them, as many times as registered.
//
// A line that is not a miss (a rule stated at start, an egress knob, a
// warning about a degraded state, help text, the config template) must say
// what it means without the phrase. A registered line that is reworded or
// removed fails the row too, so a miss line keeps the wording users grep for.
var missLines = map[missLiteral]missLine{
	{"pkg/agent/proxy/dns.go", "(*Proxy).resolveUncachedDNSResponse", "DNS mock miss resolved via upstream forward"}:                                                                                                                {1, "mocking on, a query keploy reports, no DNS mock answered, an in-network or opted-in resolver did"},
	{"pkg/agent/proxy/dns.go", "(*Proxy).resolveUncachedDNSResponse", "DNS mock miss: upstream NODATA (empty NOERROR), relaying as valid negative answer (a genuinely unrecorded record of this type would also relay empty here)"}: {1, "mocking on, a query keploy reports, no DNS mock answered, upstream said NODATA"},
	{"pkg/agent/proxy/dns.go", "(*Proxy).resolveUncachedDNSResponse", "DNS mock miss: upstream NODATA, relaying (already logged for this name/qtype)"}:                                                                              {1, "the same miss, deduplicated"},
	{"pkg/agent/proxy/dns.go", "(*Proxy).resolveUncachedDNSResponse", "DNS mock miss: upstream returned negative answer; falling back to synthetic DNS response"}:                                                                   {1, "mocking on, no DNS mock answered, upstream could not answer (SERVFAIL, REFUSED, ...)"},
	{"pkg/agent/proxy/dns.go", "(*Proxy).resolveUncachedDNSResponse", "DNS mock miss + upstream forward failed; falling back to synthetic response"}:                                                                                {1, "mocking on, no DNS mock answered, and the forward got no answer"},
	{"pkg/agent/proxy/dns.go", "(*Proxy).resolveUncachedDNSResponse", "mock miss"}:                                                                                                                                                  {1, "mocking on, no DNS mock answered: the reported miss"},
	{"pkg/agent/proxy/integrations/http/http.go", "(*HTTP).MockOutgoing", "mock miss — 502 already sent to client"}:                                                                                                                 {1, "no HTTP mock matched the request"},
	{"pkg/agent/proxy/integrations/mysql/replayer/match.go", "matchCommandWith", "mock miss"}:                                                                                                                                       {2, "no MySQL mock matched the command"},
}

// TestMockMissMeansAMiss reads every string literal in keploy's non-test Go
// source and fails on one that contains missPhrase, in any case, and is not a
// registered miss line, and on a registered miss line that is gone.
//
// Limits: it reads literals one by one, so a phrase built at run time ("mock "
// + "miss", a format verb, a value from a file) is not seen, and a literal is
// keyed by where it is written, so a constant holding the phrase that a
// non-miss line logs elsewhere passes; no source in the repo does either.
// Comments are not output and are not read. Test files,
// testdata, hidden directories (.github's sample apps) and nested modules are
// not keploy's own output and are skipped. Whether a registered line is logged
// only on a miss is held, for DNS, by TestDNSForwardLinesSayMockMissOnlyOnAMiss
// and, for HTTP, by TestMockOutgoingSaysMockMissOnlyOnAMiss. MySQL's two lines
// were read from their call site (the branch where no mock matched the
// command), not run.
func TestMockMissMeansAMiss(t *testing.T) {
	found := map[missLiteral][]token.Position{}
	fset := token.NewFileSet()
	err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() {
			if path == "." {
				return nil
			}
			name := d.Name()
			// Hidden directories (.github's sample apps), testdata, vendored and
			// node code, and nested modules are not keploy's own output.
			if strings.HasPrefix(name, ".") || name == "testdata" || name == "vendor" || name == "node_modules" {
				return filepath.SkipDir
			}
			if _, statErr := os.Stat(filepath.Join(path, "go.mod")); statErr == nil {
				return filepath.SkipDir
			}
			return nil
		}
		if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
			return nil
		}
		f, err := parser.ParseFile(fset, path, nil, parser.SkipObjectResolution)
		if err != nil {
			return err
		}
		file := filepath.ToSlash(path)
		for _, decl := range f.Decls {
			fn := ""
			if fd, ok := decl.(*ast.FuncDecl); ok {
				fn = funcName(fd)
			}
			ast.Inspect(decl, func(n ast.Node) bool {
				lit, ok := n.(*ast.BasicLit)
				if !ok || lit.Kind != token.STRING {
					return true
				}
				text, err := strconv.Unquote(lit.Value)
				if err != nil {
					t.Errorf("%s: cannot unquote %s: %v", fset.Position(lit.Pos()), lit.Value, err)
					return true
				}
				if strings.Contains(strings.ToLower(text), missPhrase) {
					key := missLiteral{file, fn, text}
					found[key] = append(found[key], fset.Position(lit.Pos()))
				}
				return true
			})
		}
		return nil
	})
	if err != nil {
		t.Fatalf("walk the module's source: %v", err)
	}

	var extra, stale []string
	for lit, at := range found {
		if want := missLines[lit].n; len(at) > want {
			// Which copy is the new one is not known: name them all.
			where := make([]string, len(at))
			for i, pos := range at {
				where[i] = pos.String()
			}
			extra = append(extra, fmt.Sprintf("%s in %s: %s, %d found, %d registered", strings.Join(where, ", "), lit.fn, snippet(lit.text), len(at), want))
		}
	}
	for lit, reg := range missLines {
		if got := len(found[lit]); got < reg.n {
			stale = append(stale, fmt.Sprintf("%s in %s: %s, %d found, %d registered", lit.file, lit.fn, snippet(lit.text), got, reg.n))
		}
	}
	sort.Strings(extra)
	sort.Strings(stale)
	for _, e := range extra {
		t.Errorf("%q in a line that is not a registered miss: %s\n\tsay what the line means without the phrase, or, if it reports an actual miss, add it to missLines", missPhrase, e)
	}
	for _, s := range stale {
		t.Errorf("registered miss line no longer in the source: %s\n\ta reported miss keeps the wording users grep for; update missLines only if the miss itself moved", s)
	}
}

// snippet quotes text, cut to the words around missPhrase when it is long
// (the config template is one literal of a few hundred lines).
func snippet(text string) string {
	const room = 80
	if len(text) <= 2*room {
		return strconv.Quote(text)
	}
	i := strings.Index(strings.ToLower(text), missPhrase)
	from, to := max(i-room, 0), min(i+len(missPhrase)+room, len(text))
	return "..." + strconv.Quote(text[from:to]) + "..."
}

// funcName names a top-level function, or a method as "(*T).m" / "T.m".
func funcName(fd *ast.FuncDecl) string {
	if fd.Recv == nil || len(fd.Recv.List) == 0 {
		return fd.Name.Name
	}
	recv := fd.Recv.List[0].Type
	star := ""
	if se, ok := recv.(*ast.StarExpr); ok {
		star, recv = "*", se.X
	}
	if ix, ok := recv.(*ast.IndexExpr); ok {
		recv = ix.X
	}
	if ix, ok := recv.(*ast.IndexListExpr); ok {
		recv = ix.X
	}
	name := "?"
	if id, ok := recv.(*ast.Ident); ok {
		name = id.Name
	}
	if star != "" {
		return "(*" + name + ")." + fd.Name.Name
	}
	return name + "." + fd.Name.Name
}