package zitcli
import (
"context"
"errors"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"sort"
"strings"
"testing"
"craftdesign.group/quarry/internal/lease"
)
// writes are the calls that change the store. Each must take a
// lease.Token first, so a write outside the lease guard does not
// compile (ticket 8e6ecbb15c).
var writes = map[string]bool{
"WikiCommit": true, "TicketNew": true, "TicketSet": true,
"ForumPost": true, "ForumReply": true, "TechnoteAdd": true,
"AttachAdd": true, "AttachRm": true,
"Pull": true,
"TagAdd": true, "TagCancel": true,
"CommitFile": true,
"LandingOpen": true, "LandingSay": true, "LandingAccept": true, "LandingRetarget": true,
"SetSetting": true, "ImportConfig": true,
}
// notWrites are the exported calls that do not change an existing
// store. Init, Clone and SeedFiles make a store that has no project
// code yet, so there is no lease to hold for it; the probes merge in
// memory and write nothing.
var notWrites = map[string]bool{
"Init": true, "Clone": true, "SeedFiles": true,
"New": true, "StoreLane": true, "SetParallel": true, "Parallel": true, "LeafKey": true,
"ArtifactHead": true, "ArtifactSize": true, "ArtifactRaw": true, "Crosslink": true,
"Frames": true, "FramesEach": true, "FramesEachStructural": true,
"Executable": true, "Symlink": true, "Error": true,
"Ls": true, "LsNames": true, "LsUnder": true, "Cat": true,
"InfoBranch": true, "Info": true, "Diff": true, "Users": true,
"AttachList": true, "FileHistory": true, "ArchiveTo": true,
"SeenSubmanifest": true, "Leaves": true, "ActiveTags": true,
"MergeProbe": true, "StackProbe": true,
"Settings": true, "ExportConfig": true, "VersionAtLeast": true, "WarnTooOld": true, "ProbeVersion": true,
}
// Every exported call in this package is classified, and every write
// takes the lease token as its first parameter. A new exported call
// fails here until someone decides which it is.
func TestEveryStoreWriteTakesALeaseToken(t *testing.T) {
fset := token.NewFileSet()
entries, err := os.ReadDir(".")
if err != nil {
t.Fatal(err)
}
seen := map[string]bool{}
var unclassified []string
for _, e := range entries {
name := e.Name()
if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
continue
}
f, err := parser.ParseFile(fset, name, nil, 0)
if err != nil {
t.Fatal(err)
}
for _, decl := range f.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok || !fn.Name.IsExported() {
continue
}
n := fn.Name.Name
seen[n] = true
switch {
case writes[n]:
switch {
case !firstParamIsToken(fn):
t.Errorf("FAIL %s writes to the store but does not take lease.Token first", n)
case !checksTokenFirst(fn):
t.Errorf("FAIL %s takes lease.Token but its first statement is not the token check", n)
default:
t.Logf("ok %s takes lease.Token and checks it first", n)
}
case notWrites[n]:
if firstParamIsToken(fn) {
t.Errorf("FAIL %s is classified as not writing but takes lease.Token", n)
}
default:
unclassified = append(unclassified, n)
}
}
}
sort.Strings(unclassified)
for _, n := range unclassified {
t.Errorf("FAIL %s is exported but classified as neither a write nor a read; add it to one list", n)
}
for n := range writes {
if !seen[n] {
t.Errorf("FAIL %s is listed as a write but no longer exists", n)
}
}
}
// The zero Token is the one any package can spell, so the write refuses
// it before zit runs: the repository path here does not exist, and an
// answer other than ErrNoToken would mean a subprocess was spawned.
func TestAZeroTokenWritesNothing(t *testing.T) {
c := New(filepath.Join(t.TempDir(), "absent"))
if err := c.TagAdd(lease.Token{}, "ci-pass", "trunk", "", false); !errors.Is(err, lease.ErrNoToken) {
t.Errorf("FAIL TagAdd with a zero token: %v, want ErrNoToken", err)
}
if _, err := CommitFile(lease.Token{}, c.repo, "trunk", "b", "dan", "m", "p", nil); !errors.Is(err, lease.ErrNoToken) {
t.Errorf("FAIL CommitFile with a zero token: %v, want ErrNoToken", err)
}
if err := c.SetSetting(lease.Token{}, context.Background(), "autosync", "off"); !errors.Is(err, lease.ErrNoToken) {
t.Errorf("FAIL SetSetting with a zero token: %v, want ErrNoToken", err)
}
}
// checksTokenFirst reports whether fn's body opens with
// `if err := tok.Err(); err != nil { return ... }`, naming its first
// parameter. Taking the token without checking it would let the zero
// value through to zit.
func checksTokenFirst(fn *ast.FuncDecl) bool {
names := fn.Type.Params.List[0].Names
if len(names) != 1 || fn.Body == nil || len(fn.Body.List) == 0 {
return false
}
ifs, ok := fn.Body.List[0].(*ast.IfStmt)
if !ok || ifs.Init == nil || len(ifs.Body.List) == 0 {
return false
}
if _, ok := ifs.Body.List[len(ifs.Body.List)-1].(*ast.ReturnStmt); !ok {
return false
}
init, ok := ifs.Init.(*ast.AssignStmt)
if !ok || len(init.Rhs) != 1 {
return false
}
call, ok := init.Rhs[0].(*ast.CallExpr)
if !ok {
return false
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok || sel.Sel.Name != "Err" {
return false
}
recv, ok := sel.X.(*ast.Ident)
return ok && recv.Name == names[0].Name
}
func firstParamIsToken(fn *ast.FuncDecl) bool {
if fn.Type.Params == nil || len(fn.Type.Params.List) == 0 {
return false
}
sel, ok := fn.Type.Params.List[0].Type.(*ast.SelectorExpr)
if !ok {
return false
}
pkg, ok := sel.X.(*ast.Ident)
return ok && pkg.Name == "lease" && sel.Sel.Name == "Token"
}
// testToken stands in for the lease in this package's tests, which
// run against a scratch store with no coordinator.
var testToken = lease.Unguarded("test: scratch store, no coordinator")