Add gallery app, native window and architecture tests

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 16:45:34 -04:00
1 parent 7e47e3a924
commit d3b4adebb0
16 files changed
+1552 -5

No files matched your search

+222
View File
@@ -0,0 +1,222 @@
// Package archtest checks the rules of AGENTS.md on every package.
package archtest
import (
"go/ast"
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"testing"
)
const module = "git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/"
var tiers = map[string]int{
"pixel": 0, "icon": 1,
"layout": 2, "text": 2, "display": 2,
"input": 3, "feedback": 3,
"navigation": 4, "data": 4, "overlay": 4, "datetime": 4, "chart": 4, "fx": 4,
"game": 5, "chat": 5, "media": 5, "code": 5, "account": 5, "files": 5,
"project": 5, "shop": 5, "agent": 5, "desk": 5, "tabletop": 5,
"gallery": 6, "cmd/gallery": 7, "cmd/web": 7,
}
// Styled ui widgets drawing outside the pixel style.
var bannedFuncs = set("Text Textf RichText Button PrimaryButton Link MenuButton Toggle ToggleGroup Segmented Toolbar " +
"Checkbox CheckboxGroup Switch Radio Select Slider StepSlider RangeSlider Stepper Rating TextInput TextArea " +
"NumberInput SearchField Combobox Autocomplete TokenField EditableText FindBar Calendar DateInput TimeInput " +
"ColorPicker ColorWell Table Tree TreeItem Accordion AccordionItem Collapsible Form Field Fieldset Tabs " +
"Sidebar SidebarItem SidebarSection Breadcrumbs BackButton ForwardButton Modal AlertDialog Popover Progress " +
"Spinner Meter Badge Image Icon Avatar Divider Split SplitVertical")
// Element, Context and Painter methods that blend, ease or draw.
var bannedMethods = set("Background Border BorderColor BorderWidth BorderStyle Radius Shadow Gradient " +
"LinearGradient Opacity Stripes Tooltip Menu ContextMenu Transition Animate AnimateWith Loop Toast " +
"ToastAction AnimationFrame After FillPath StrokePath RichText Glyphs FillGradient Stroke StrokeDashed " +
"StrokePathGradient FillPathGradient Material Icon Image")
func set(s string) map[string]bool {
out := map[string]bool{}
for _, f := range strings.Fields(s) {
out[f] = true
}
return out
}
type goFile struct {
path, pkg string
ast *ast.File
src []byte
}
// files parses every Go file of the module.
func files(t *testing.T) []goFile {
var out []goFile
err := filepath.WalkDir("../..", func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() && p != "../.." && (strings.HasPrefix(d.Name(), ".") || d.Name() == "node_modules") {
return filepath.SkipDir
}
if d.IsDir() || !strings.HasSuffix(p, ".go") {
return nil
}
src, err := os.ReadFile(p)
if err != nil {
return err
}
f, err := parser.ParseFile(token.NewFileSet(), p, src, parser.ParseComments)
if err != nil {
return err
}
rel, _ := filepath.Rel("../..", filepath.Dir(p))
out = append(out, goFile{p, filepath.ToSlash(rel), f, src})
return nil
})
if err != nil {
t.Fatal(err)
}
if len(out) < 20 {
t.Fatalf("found %d Go files; the walk is broken", len(out))
}
return out
}
func TestImportsPointDown(t *testing.T) {
for _, f := range files(t) {
if strings.HasSuffix(f.path, "_test.go") || strings.HasPrefix(f.pkg, "tools/") || strings.HasPrefix(f.pkg, "internal/") {
continue
}
tier, ok := tiers[f.pkg]
if !ok {
t.Errorf("%s: package %q has no tier in AGENTS.md", f.path, f.pkg)
continue
}
for _, im := range f.ast.Imports {
path, _ := strconv.Unquote(im.Path.Value)
if !strings.HasPrefix(path, module) {
continue
}
to := strings.TrimPrefix(path, module)
if got, ok := tiers[to]; !ok || got >= tier {
t.Errorf("%s: %s (tier %d) imports %s (tier %d)", f.path, f.pkg, tier, to, got)
}
}
}
}
func TestNoBannedCalls(t *testing.T) {
for _, f := range files(t) {
if strings.HasSuffix(f.path, "_test.go") || f.pkg == "pixel" || strings.HasPrefix(f.pkg, "tools/") || strings.HasPrefix(f.pkg, "internal/") {
continue
}
imported := map[string]string{}
for _, im := range f.ast.Imports {
path, _ := strconv.Unquote(im.Path.Value)
name := path[strings.LastIndex(path, "/")+1:]
if im.Name != nil {
name = im.Name.Name
}
imported[name] = path
}
painters := map[*ast.Ident]bool{}
var stack []ast.Node
ast.Inspect(f.ast, func(n ast.Node) bool {
if n == nil {
stack = stack[:len(stack)-1]
return true
}
var parent ast.Node
if len(stack) > 0 {
parent = stack[len(stack)-1]
}
stack = append(stack, n)
switch n := n.(type) {
case *ast.Field:
if star, ok := n.Type.(*ast.StarExpr); ok && isUI(star.X, "Painter", imported) {
for _, name := range n.Names {
painters[name] = true
}
}
case *ast.Ident:
if n.Obj != nil && n.Obj.Kind == ast.Var && !painters[n] && isPainter(n, painters) && !intoCanvas(n, parent) {
t.Errorf("%s: painter %s used outside k.Canvas; only pixel draws", f.path, n.Name)
}
case *ast.CallExpr:
sel, ok := n.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
pkg := false
if id, ok := sel.X.(*ast.Ident); ok {
_, pkg = imported[id.Name]
}
if pkg && isUI(sel.X, "", imported) && bannedFuncs[sel.Sel.Name] || !pkg && bannedMethods[sel.Sel.Name] {
t.Errorf("%s: banned call %s", f.path, sel.Sel.Name)
}
}
return true
})
}
}
// isPainter reports whether id refers to a *ui.Painter parameter.
func isPainter(id *ast.Ident, painters map[*ast.Ident]bool) bool {
for p := range painters {
if id.Obj == p.Obj {
return true
}
}
return false
}
// intoCanvas reports whether id is a Canvas call argument.
func intoCanvas(id *ast.Ident, parent ast.Node) bool {
call, ok := parent.(*ast.CallExpr)
if !ok {
return false
}
sel, ok := call.Fun.(*ast.SelectorExpr)
return ok && sel.Sel.Name == "Canvas" && slices.Contains(call.Args, ast.Expr(id))
}
// isUI reports whether x names package ui, or ui.name.
func isUI(x ast.Expr, name string, imported map[string]string) bool {
if sel, ok := x.(*ast.SelectorExpr); ok && name != "" {
return sel.Sel.Name == name && isUI(sel.X, "", imported)
}
id, ok := x.(*ast.Ident)
return ok && name == "" && imported[id.Name] == "github.com/egoist/mygo/ui"
}
var directive = regexp.MustCompile(`^//(go:|line )`)
func TestCommentsAndFileLength(t *testing.T) {
for _, f := range files(t) {
if n := strings.Count(string(f.src), "\n"); n >= 500 {
t.Errorf("%s: %d lines, keep files under 500", f.path, n)
}
for _, g := range f.ast.Comments {
lines := 0
for _, c := range g.List {
if directive.MatchString(c.Text) || c.Text == "//" {
continue
}
lines++
if words := len(strings.Fields(strings.TrimPrefix(c.Text, "//"))); words > 10 || strings.HasPrefix(c.Text, "/*") {
t.Errorf("%s: comment of %d words: %s", f.path, words, c.Text)
}
}
if lines > 1 {
t.Errorf("%s: comment of %d lines near %q", f.path, lines, g.List[0].Text)
}
}
}
}