carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "context"
5 "go/ast"
6 "go/parser"
7 "go/token"
8 "testing"
9
10 "github.com/carlosframework/platform/internal/alerting"
11 "github.com/carlosframework/platform/internal/console"
12 "github.com/carlosframework/platform/internal/moves"
13)
14
15// The Phase 1 moves switch reaches console.Config.MovesEnabled through
16// applyMovesSwitch, and only the on-values turn it on.
17func TestApplyMovesSwitchSetsTheConfigField(t *testing.T) {
18 for _, tc := range []struct {
19 val string
20 want bool
21 }{
22 {"", false}, {"on", true}, {"1", true}, {"true", true}, {"off", false}, {"yes", false},
23 } {
24 var cfg console.Config
25 applyMovesSwitch(&cfg, func(k string) string {
26 if k != moves.SwitchEnv {
27 t.Fatalf("read %q, want only %s", k, moves.SwitchEnv)
28 }
29 return tc.val
30 })
31 if cfg.MovesEnabled != tc.want {
32 t.Errorf("%s=%q: MovesEnabled = %v, want %v", moves.SwitchEnv, tc.val, cfg.MovesEnabled, tc.want)
33 }
34 }
35}
36
37// serve() is not callable from a test (it opens a database, a bucket and a
38// listener), so this parses main.go and checks three things: serve() calls
39// applyMovesSwitch(&X, os.Getenv); the same X is the config handed to
40// console.NewServer; and the switch call comes before the NewServer call.
41func TestServeAppliesTheMovesSwitchFromTheEnvironment(t *testing.T) {
42 f, err := parser.ParseFile(token.NewFileSet(), "main.go", nil, 0)
43 if err != nil {
44 t.Fatal(err)
45 }
46 var serve *ast.FuncDecl
47 for _, d := range f.Decls {
48 if fn, ok := d.(*ast.FuncDecl); ok && fn.Name.Name == "serve" {
49 serve = fn
50 }
51 }
52 if serve == nil {
53 t.Fatal("no serve() in main.go")
54 }
55 var applies, news []*ast.CallExpr
56 ast.Inspect(serve.Body, func(n ast.Node) bool {
57 call, ok := n.(*ast.CallExpr)
58 if !ok {
59 return true
60 }
61 switch fun := call.Fun.(type) {
62 case *ast.Ident:
63 if fun.Name == "applyMovesSwitch" {
64 applies = append(applies, call)
65 }
66 case *ast.SelectorExpr:
67 if pkg, ok := fun.X.(*ast.Ident); ok && pkg.Name == "console" && fun.Sel.Name == "NewServer" {
68 news = append(news, call)
69 }
70 }
71 return true
72 })
73 if len(applies) != 1 || len(news) != 1 {
74 t.Fatalf("serve() has %d applyMovesSwitch calls and %d console.NewServer calls, want one of each", len(applies), len(news))
75 }
76 apply, ns := applies[0], news[0]
77 if len(apply.Args) != 2 || len(ns.Args) < 1 {
78 t.Fatal("applyMovesSwitch wants (&cfg, os.Getenv) and console.NewServer wants (cfg, store)")
79 }
80 if sel, ok := apply.Args[1].(*ast.SelectorExpr); !ok || !isIdent(sel.X, "os") || sel.Sel.Name != "Getenv" {
81 t.Fatal("applyMovesSwitch is not given os.Getenv; the switch would not read the real environment")
82 }
83 addr, ok := apply.Args[0].(*ast.UnaryExpr)
84 if !ok || addr.Op != token.AND {
85 t.Fatal("applyMovesSwitch's first argument is not &X")
86 }
87 x, ok := addr.X.(*ast.Ident)
88 if !ok {
89 t.Fatal("applyMovesSwitch's first argument is not the address of a plain variable")
90 }
91 if !isIdent(ns.Args[0], x.Name) {
92 t.Fatalf("console.NewServer is not handed %s, the config applyMovesSwitch changed", x.Name)
93 }
94 if apply.Pos() >= ns.Pos() {
95 t.Fatal("applyMovesSwitch runs after console.NewServer; the server would be built with the switch off")
96 }
97}
98
99func isIdent(e ast.Expr, name string) bool {
100 id, ok := e.(*ast.Ident)
101 return ok && id.Name == name
102}
103
104// The console pages from its move pass (moves Plan 5 Task 7, delta M14):
105// armMovesPaging arms alerting's sinks from the agent's variables on a
106// host-claim console, with or without moves; serve()
107// calls it with os.Getenv before ConfigureClaimRuntime, so the recovery
108// pass never runs unarmed.
109func TestArmMovesPagingWithClaims(t *testing.T) {
110 env := func(k string) string {
111 if k == "CARLOS_ALERT_WEBHOOK" {
112 return "http://example.test/hook"
113 }
114 return ""
115 }
116 for _, tc := range []struct{ claim, moves, want bool }{{false, false, false}, {true, false, true}, {false, true, false}, {true, true, true}} {
117 cfg := console.Config{HostClaim: tc.claim, MovesEnabled: tc.moves}
118 if got := armMovesPaging(context.Background(), cfg, env); got != tc.want {
119 t.Errorf("claim %v moves %v: armed %v, want %v", tc.claim, tc.moves, got, tc.want)
120 }
121 }
122 t.Cleanup(func() { alerting.Arm(nil) })
123
124 f, err := parser.ParseFile(token.NewFileSet(), "main.go", nil, 0)
125 if err != nil {
126 t.Fatal(err)
127 }
128 var armed, configured token.Pos
129 for _, d := range f.Decls {
130 fn, ok := d.(*ast.FuncDecl)
131 if !ok || fn.Name.Name != "serve" {
132 continue
133 }
134 ast.Inspect(fn.Body, func(n ast.Node) bool {
135 call, ok := n.(*ast.CallExpr)
136 if !ok {
137 return true
138 }
139 switch fun := call.Fun.(type) {
140 case *ast.Ident:
141 if fun.Name == "armMovesPaging" && len(call.Args) == 3 {
142 if sel, ok := call.Args[2].(*ast.SelectorExpr); ok && isIdent(sel.X, "os") && sel.Sel.Name == "Getenv" {
143 armed = call.Pos()
144 }
145 }
146 case *ast.SelectorExpr:
147 if fun.Sel.Name == "ConfigureClaimRuntime" {
148 configured = call.Pos()
149 }
150 }
151 return true
152 })
153 }
154 if armed == 0 || configured == 0 || armed > configured {
155 t.Fatalf("serve() must call armMovesPaging(ctx, cfg, os.Getenv) before ConfigureClaimRuntime (armed %v, configured %v)", armed, configured)
156 }
157}
158