forked from go-mup/mup
/
tester.go
274 lines (249 loc) · 7.7 KB
/
tester.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
package mup
import (
"fmt"
"sync"
"time"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"gopkg.in/mup.v0/ldap"
"gopkg.in/mup.v0/schema"
"strings"
)
// NewPluginTester interacts with an internally managed instance of a
// registered plugin for testing purposes.
type PluginTester struct {
mu sync.Mutex
cond sync.Cond
stopped bool
state pluginState
replies []string
ldaps map[string]ldap.Conn
}
// NewPluginTester creates a new tester for interacting with an internally
// managed instance of the named plugin.
func NewPluginTester(pluginName string) *PluginTester {
spec, ok := registeredPlugins[pluginKey(pluginName)]
if !ok {
panic(fmt.Sprintf("plugin not registered: %q", pluginKey(pluginName)))
}
t := &PluginTester{}
t.cond.L = &t.mu
t.ldaps = make(map[string]ldap.Conn)
t.state.spec = spec
t.state.plugger = newPlugger(pluginName, t.appendMessage, t.ldap)
return t
}
func (t *PluginTester) appendMessage(msg *Message) error {
t.mu.Lock()
defer t.mu.Unlock()
if t.stopped {
panic("plugin attempted to send message after being stopped")
}
msgstr := msg.String()
if msg.Account != "test" {
msgstr = "[@" + msg.Account + "] " + msgstr
}
t.replies = append(t.replies, msgstr)
t.cond.Signal()
t.state.handle(msg, "")
return nil
}
func (t *PluginTester) ldap(name string) (ldap.Conn, error) {
t.mu.Lock()
conn, ok := t.ldaps[name]
t.mu.Unlock()
if ok {
return conn, nil
}
return nil, fmt.Errorf("LDAP connection %q not found", name)
}
// Plugger returns the plugger that is provided to the plugin.
func (t *PluginTester) Plugger() *Plugger {
return t.state.plugger
}
// Start starts the plugin being tested.
func (t *PluginTester) Start() error {
t.mu.Lock()
defer t.mu.Unlock()
if t.state.plugin != nil {
panic("PluginTester.Start called more than once")
}
var err error
t.state.plugin = t.state.spec.Start(t.state.plugger)
return err
}
// SetDatabase sets the database to offer the plugin being tested.
func (t *PluginTester) SetDatabase(db *mgo.Database) {
t.mu.Lock()
defer t.mu.Unlock()
if t.state.plugin != nil {
panic("PluginTester.SetDatabase called after Start")
}
// Ensure tests run with capped collections properly created.
err := createCollections(db)
if err != nil {
panic("PluginTester.SetDatabase cannot create default collections: " + err.Error())
}
t.state.plugger.setDatabase(db)
}
// SetConfig changes the configuration of the plugin being tested.
func (t *PluginTester) SetConfig(value interface{}) {
t.mu.Lock()
defer t.mu.Unlock()
if t.state.plugin != nil {
panic("PluginTester.SetConfig called after Start")
}
t.state.plugger.setConfig(marshalRaw(value))
}
// SetTargets changes the targets of the plugin being tested.
//
// These targets affect message broadcasts performed by the plugin,
// and also the list of targets that the plugin may observe by
// explicitly querying the provided Plugger about them. Changing
// targets does not prevent the tester's Sendf and SendAll functions
// from delivering messages to the plugin, though, as it doesn't
// make sense to feed the plugin with test messages that it cannot
// observe.
func (t *PluginTester) SetTargets(value interface{}) {
t.mu.Lock()
defer t.mu.Unlock()
if t.state.plugin != nil {
panic("PluginTester.SetTargets called after Start")
}
t.state.plugger.setTargets(marshalRaw(value))
}
// SetLDAP makes the provided LDAP connection available to the plugin.
func (t *PluginTester) SetLDAP(name string, conn ldap.Conn) {
t.mu.Lock()
t.ldaps[name] = conn
t.mu.Unlock()
}
func marshalRaw(value interface{}) bson.Raw {
if value == nil {
return emptyDoc
}
data, err := bson.Marshal(bson.D{{"value", value}})
if err != nil {
panic("cannot marshal provided value: " + err.Error())
}
var raw struct{ Value bson.Raw }
err = bson.Unmarshal(data, &raw)
if err != nil {
panic("cannot unmarshal provided value: " + err.Error())
}
return raw.Value
}
// Stop stops the tester and the plugin being tested.
func (t *PluginTester) Stop() error {
err := t.state.plugin.Stop()
t.mu.Lock()
t.stopped = true
t.cond.Broadcast()
t.mu.Unlock()
return err
}
// Recv receives the next message dispatched by the plugin being tested. If no
// message is currently pending, Recv waits up to a few seconds for a message
// to arrive. If no messages arrive even then, an empty string is returned.
//
// The message is formatted as a raw IRC protocol message, and optionally prefixed
// by the account name under brackets ("[@<account>] ") if the message is delivered
// to any other account besides the default "test" one.
//
// Recv may be used after the tester is stopped.
func (t *PluginTester) Recv() string {
t.mu.Lock()
defer t.mu.Unlock()
timeout := time.Now().Add(3 * time.Second)
for !t.stopped && len(t.replies) == 0 && time.Now().Before(timeout) {
t.cond.Wait()
}
if len(t.replies) == 0 {
return ""
}
reply := t.replies[0]
copy(t.replies, t.replies[1:])
t.replies = t.replies[0 : len(t.replies)-1]
return reply
}
// RecvAll receives all currently pending messages dispatched by the plugin being tested.
//
// All messages are formatted as raw IRC protocol messages, and optionally prefixed
// by the account name under brackets ("[@<account>] ") if the message is delivered
// to any other account besides the default "test" one.
//
// RecvAll may be used after the tester is stopped.
func (t *PluginTester) RecvAll() []string {
t.mu.Lock()
replies := t.replies
t.replies = nil
t.mu.Unlock()
return replies
}
// Sendf formats a PRIVMSG coming from "nick!~user@host" and delivers to the plugin
// being tested for handling as a message, as a command, or both, depending on the
// plugin specification and implementation.
//
// The formatted message may be prefixed by "[<target>@<account>,<option>] " to define
// the channel or bot nick the message was addressed to, the account name it was
// observed on, and a list of comma-separated options. All fields are optional, and
// default to "[mup@test] ". The only supported option at the moment is "raw", which
// causes the message text to be taken as a raw IRC protocol message. When providing
// a target without an account the "@" may be omitted, and the comma may be omitted
// if there are no options.
//
// Sendf always delivers the message to the plugin, irrespective of which targets
// are currently setup, as it doesn't make sense to test the plugin with a message
// that it cannot observe.
func (t *PluginTester) Sendf(format string, args ...interface{}) {
account, message := parseSendfText(fmt.Sprintf(format, args...))
msg := ParseIncoming(account, "mup", "!", message)
t.state.handle(msg, schema.CommandName(msg.BotText))
}
func parseSendfText(text string) (account, message string) {
account = "test"
close := strings.Index(text, "] ")
if !strings.HasPrefix(text, "[") || close < 0 {
return account, ":nick!~user@host PRIVMSG mup :" + text
}
prefix := text[1:close]
text = text[close+2:]
raw := false
comma := strings.Index(prefix, ",")
if comma >= 0 {
for _, option := range strings.Split(prefix[comma+1:], ",") {
if option == "raw" {
raw = true
} else if option != "" {
panic("unknown option for Tester.Sendf: " + option)
}
}
prefix = prefix[:comma]
}
at := strings.Index(prefix, "@")
if at >= 0 {
if at < len(prefix)-1 {
account = prefix[at+1:]
}
prefix = prefix[:at]
}
if raw {
if prefix != "" {
panic("Sendf prefix cannot contain both a target and the raw option")
}
return account, text
}
target := "mup"
if prefix != "" {
target = prefix
}
return account, ":nick!~user@host PRIVMSG " + target + " :" + text
}
// SendAll sends each entry in text as an individual message to the bot.
//
// See Sendf for more details.
func (t *PluginTester) SendAll(text []string) {
for _, texti := range text {
t.Sendf("%s", texti)
}
}