Skip to content
quarry Artifact [f8646b10c6]
Login

Artifact [f8646b10c6]

Artifact f8646b10c62a1f8cdbcd1c30c6861738699079ca8d151729aa7349ba977a50c4:


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, "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,
	"MergeProbe": true, "StackProbe": true,
	"Settings": true, "ExportConfig": 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]:
				if !firstParamIsToken(fn) {
					t.Errorf("FAIL %s writes to the store but does not take lease.Token first", n)
				} else {
					t.Logf("ok %s takes lease.Token", 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)
	}
}

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")