settings_test.go

  1package plugin
  2
  3import (
  4	"testing"
  5
  6	lua "github.com/yuin/gopher-lua"
  7)
  8
  9// declareForTest mimics what loadPlugin does so we can drive declareSettings
 10// without writing a real plugin file to disk.
 11func declareForTest(t *testing.T, m *Manager, name, src string) {
 12	t.Helper()
 13	prev := m.currentPlugin
 14	m.currentPlugin = name
 15	defer func() { m.currentPlugin = prev }()
 16	if err := m.state.DoString(src); err != nil {
 17		t.Fatalf("Lua error loading %q: %v", name, err)
 18	}
 19}
 20
 21func TestPluginSettingsSchemaAndProxy(t *testing.T) {
 22	m := NewManager()
 23	defer m.Close()
 24
 25	src := `
 26		local matcha = require("matcha")
 27		cfg = matcha.settings({
 28			enabled = {type = "boolean", default = true, label = "Enabled"},
 29			limit   = {type = "number",  default = 5,    label = "Limit"},
 30			suffix  = {type = "string",  default = "!"},
 31		})
 32	`
 33	declareForTest(t, m, "demo", src)
 34
 35	defs := m.Schema("demo")
 36	if len(defs) != 3 {
 37		t.Fatalf("expected 3 defs, got %d", len(defs))
 38	}
 39
 40	v, ok := m.GetSettingValue("demo", "enabled")
 41	if !ok || v.(bool) != true {
 42		t.Fatalf("expected default enabled=true, got %v ok=%v", v, ok)
 43	}
 44	v, _ = m.GetSettingValue("demo", "limit")
 45	if v.(float64) != 5 {
 46		t.Fatalf("expected default limit=5, got %v", v)
 47	}
 48
 49	// proxy table should reflect live values
 50	check := `
 51		assert(cfg.enabled == true,  "enabled default")
 52		assert(cfg.limit == 5,        "limit default")
 53		assert(cfg.suffix == "!",     "suffix default")
 54	`
 55	if err := m.state.DoString(check); err != nil {
 56		t.Fatalf("proxy check failed: %v", err)
 57	}
 58
 59	// Override via SetSettingValue and re-check via proxy
 60	if !m.SetSettingValue("demo", "enabled", false) {
 61		t.Fatal("SetSettingValue rejected known key")
 62	}
 63	if !m.SetSettingValue("demo", "limit", float64(42)) {
 64		t.Fatal("SetSettingValue rejected number")
 65	}
 66	if !m.SetSettingValue("demo", "suffix", "?!") {
 67		t.Fatal("SetSettingValue rejected string")
 68	}
 69
 70	check = `
 71		assert(cfg.enabled == false, "enabled override")
 72		assert(cfg.limit == 42,       "limit override")
 73		assert(cfg.suffix == "?!",    "suffix override")
 74	`
 75	if err := m.state.DoString(check); err != nil {
 76		t.Fatalf("proxy override check failed: %v", err)
 77	}
 78}
 79
 80func TestPluginSettingsLoadValues(t *testing.T) {
 81	m := NewManager()
 82	defer m.Close()
 83
 84	declareForTest(t, m, "demo", `
 85		local matcha = require("matcha")
 86		cfg = matcha.settings({
 87			enabled = {type = "boolean", default = true},
 88			limit   = {type = "number",  default = 5},
 89		})
 90	`)
 91
 92	// Simulate loading values from config (JSON unmarshals booleans as bool,
 93	// numbers as float64).
 94	m.LoadSettingValues(map[string]map[string]interface{}{
 95		"demo": {
 96			"enabled": false,
 97			"limit":   float64(99),
 98		},
 99	})
100
101	v, _ := m.GetSettingValue("demo", "enabled")
102	if v.(bool) != false {
103		t.Fatalf("expected enabled=false after load, got %v", v)
104	}
105	v, _ = m.GetSettingValue("demo", "limit")
106	if v.(float64) != 99 {
107		t.Fatalf("expected limit=99 after load, got %v", v)
108	}
109
110	// AllSettingValues should round-trip through JSON-friendly types.
111	all := m.AllSettingValues()
112	if all["demo"]["enabled"] != false || all["demo"]["limit"].(float64) != 99 {
113		t.Fatalf("AllSettingValues mismatch: %#v", all)
114	}
115}
116
117func TestPluginSettingsProxyReadOnly(t *testing.T) {
118	m := NewManager()
119	defer m.Close()
120
121	declareForTest(t, m, "demo", `
122		local matcha = require("matcha")
123		cfg = matcha.settings({enabled = {type = "boolean", default = true}})
124	`)
125
126	err := m.state.DoString(`cfg.enabled = false`)
127	if err == nil {
128		t.Fatal("expected error writing to read-only proxy")
129	}
130}
131
132func TestPluginSettingsRequiresLoadingPlugin(t *testing.T) {
133	m := NewManager()
134	defer m.Close()
135
136	// currentPlugin is empty (no loadPlugin in flight)
137	err := m.state.DoString(`require("matcha").settings({foo = {type = "boolean", default = false}})`)
138	if err == nil {
139		t.Fatal("expected error when calling matcha.settings outside plugin load")
140	}
141}
142
143func TestPluginSettingsCoercion(t *testing.T) {
144	m := NewManager()
145	defer m.Close()
146
147	declareForTest(t, m, "demo", `
148		local matcha = require("matcha")
149		matcha.settings({
150			flag = {type = "boolean", default = false},
151			n    = {type = "number",  default = 0},
152		})
153	`)
154
155	// Strings from a JSON file or older config should coerce.
156	m.LoadSettingValues(map[string]map[string]interface{}{
157		"demo": {
158			"flag": "true",
159			"n":    "ignored",
160		},
161	})
162
163	v, _ := m.GetSettingValue("demo", "flag")
164	if v.(bool) != true {
165		t.Fatalf("expected flag=true after coercion, got %v", v)
166	}
167	v, _ = m.GetSettingValue("demo", "n")
168	if _, ok := v.(float64); !ok {
169		t.Fatalf("expected n coerced to float64, got %T %v", v, v)
170	}
171}
172
173// Verify that hook callbacks see live values (regression test for the closure
174// capture pattern).
175func TestPluginSettingsAccessibleFromHook(t *testing.T) {
176	m := NewManager()
177	defer m.Close()
178
179	declareForTest(t, m, "demo", `
180		local matcha = require("matcha")
181		local cfg = matcha.settings({n = {type = "number", default = 1}})
182		seen = nil
183		matcha.on("custom", function() seen = cfg.n end)
184	`)
185
186	// Fire hook before any override
187	m.CallHook("custom")
188	v := m.state.GetGlobal("seen")
189	if n, ok := v.(lua.LNumber); !ok || float64(n) != 1 {
190		t.Fatalf("expected seen=1, got %v", v)
191	}
192
193	// Override and fire again
194	m.SetSettingValue("demo", "n", float64(7))
195	m.CallHook("custom")
196	v = m.state.GetGlobal("seen")
197	if n, ok := v.(lua.LNumber); !ok || float64(n) != 7 {
198		t.Fatalf("expected seen=7 after override, got %v", v)
199	}
200}