1#!/usr/bin/env python
2
3import os
4import sys
5
6import breakdancer
7from breakdancer import Condition, Effect, Action, Driver
8
9TESTKEY = 'testkey'
10
11######################################################################
12# Conditions
13######################################################################
14
15class ExistsCondition(Condition):
16
17    def __call__(self, state):
18        return TESTKEY in state
19
20class ExistsAsNumber(Condition):
21
22    def __call__(self, state):
23        try:
24            int(state[TESTKEY])
25            return True
26        except:
27            return False
28
29class MaybeExistsAsNumber(ExistsAsNumber):
30
31    def __call__(self, state):
32        return TESTKEY not in state or ExistsAsNumber.__call__(self, state)
33
34class DoesNotExistCondition(Condition):
35
36    def __call__(self, state):
37        return TESTKEY not in state
38
39class NothingExistsCondition(Condition):
40
41    def __call__(self, state):
42        return not bool(state)
43
44class IsLockedCondition(Condition):
45
46    def __call__(self, state):
47        return (TESTKEY + '.lock') in state
48
49class IsUnlockedCondition(Condition):
50
51    def __call__(self, state):
52        return (TESTKEY + '.lock') not in state
53
54class KnowsCAS(Condition):
55
56    def __call__(self, state):
57        return (TESTKEY + '.cas') in state
58
59######################################################################
60# Effects
61######################################################################
62
63class NoFX(Effect):
64
65    def __call__(self, state):
66        pass
67
68class MutationEffect(Effect):
69
70    def forgetCAS(self, state):
71        k = TESTKEY + '.cas'
72        if k in state:
73            del state[k]
74
75class StoreEffect(MutationEffect):
76
77    def __init__(self, rememberCAS=False):
78        self.rememberCAS = rememberCAS
79
80    def __call__(self, state):
81        state[TESTKEY] = '0'
82        if self.rememberCAS:
83            state[TESTKEY + '.cas'] = 1
84        else:
85            self.forgetCAS(state)
86
87class LockEffect(MutationEffect):
88
89    def __call__(self, state):
90        state[TESTKEY + '.lock'] = True
91        self.forgetCAS(state)
92
93class UnlockEffect(Effect):
94
95    def __call__(self, state):
96        klock = TESTKEY + '.lock'
97        if klock in state:
98            del state[klock]
99
100class DeleteEffect(MutationEffect):
101
102    def __call__(self, state):
103        del state[TESTKEY]
104        klock = TESTKEY + '.lock'
105        if klock in state:
106            del state[klock]
107        self.forgetCAS(state)
108
109class FlushEffect(Effect):
110
111    def __call__(self, state):
112        state.clear()
113
114class AppendEffect(MutationEffect):
115
116    suffix = '-suffix'
117
118    def __call__(self, state):
119        state[TESTKEY] = state[TESTKEY] + self.suffix
120        self.forgetCAS(state)
121
122class PrependEffect(MutationEffect):
123
124    prefix = 'prefix-'
125
126    def __call__(self, state):
127        state[TESTKEY] = self.prefix + state[TESTKEY]
128        self.forgetCAS(state)
129
130class ArithmeticEffect(MutationEffect):
131
132    default = '0'
133
134    def __init__(self, by=1):
135        self.by = by
136
137    def __call__(self, state):
138        if TESTKEY in state:
139            state[TESTKEY] = str(max(0, int(state[TESTKEY]) + self.by))
140        else:
141            state[TESTKEY] = self.default
142        self.forgetCAS(state)
143
144######################################################################
145# Actions
146######################################################################
147
148class Get(Action):
149
150    preconditions = [ExistsCondition()]
151    effect = NoFX()
152
153class Set(Action):
154
155    preconditions = [IsUnlockedCondition()]
156    effect = StoreEffect()
157    postconditions = [ExistsCondition()]
158
159class setUsingCAS(Action):
160
161    preconditions = [KnowsCAS(), IsUnlockedCondition()]
162    effect = StoreEffect()
163    postconditions = [ExistsCondition()]
164
165class SetRetainCAS(Action):
166
167    preconditions = [IsUnlockedCondition()]
168    effect = StoreEffect(True)
169    postconditions = [ExistsCondition(), KnowsCAS()]
170
171class Add(Action):
172
173    preconditions = [DoesNotExistCondition(), IsUnlockedCondition()]
174    effect = StoreEffect()
175    postconditions = [ExistsCondition()]
176
177class Delete(Action):
178
179    preconditions = [ExistsCondition(), IsUnlockedCondition()]
180    effect = DeleteEffect()
181    postconditions = [DoesNotExistCondition()]
182
183class DeleteUsingCAS(Action):
184
185    preconditions = [KnowsCAS(), ExistsCondition(), IsUnlockedCondition()]
186    effect = DeleteEffect()
187    postconditions = [DoesNotExistCondition()]
188
189class Flush(Action):
190
191    effect = FlushEffect()
192    postconditions = [NothingExistsCondition()]
193
194class WaitForLock(Action):
195
196    effect = UnlockEffect()
197
198class GetLock(Action):
199
200    preconditions = [ExistsCondition(), IsUnlockedCondition()]
201    effect = LockEffect()
202    postconditions = [IsLockedCondition()]
203
204class Delay(Flush):
205    pass
206
207class Append(Action):
208
209    preconditions = [ExistsCondition(), IsUnlockedCondition()]
210    effect = AppendEffect()
211    postconditions = [ExistsCondition()]
212
213class Prepend(Action):
214
215    preconditions = [ExistsCondition(), IsUnlockedCondition()]
216    effect = PrependEffect()
217    postconditions = [ExistsCondition()]
218
219class AppendUsingCAS(Action):
220
221    preconditions = [KnowsCAS(), ExistsCondition(), IsUnlockedCondition()]
222    effect = AppendEffect()
223    postconditions = [ExistsCondition()]
224
225class PrependUsingCAS(Action):
226
227    preconditions = [KnowsCAS(), ExistsCondition(), IsUnlockedCondition()]
228    effect = PrependEffect()
229    postconditions = [ExistsCondition()]
230
231class Incr(Action):
232
233    preconditions = [ExistsAsNumber(), IsUnlockedCondition()]
234    effect = ArithmeticEffect(1)
235    postconditions = [ExistsAsNumber()]
236
237class Decr(Action):
238
239    preconditions = [ExistsAsNumber(), IsUnlockedCondition()]
240    effect = ArithmeticEffect(-1)
241    postconditions = [ExistsAsNumber()]
242
243class IncrWithDefault(Action):
244
245    preconditions = [MaybeExistsAsNumber(), IsUnlockedCondition()]
246    effect = ArithmeticEffect(1)
247    postconditions = [ExistsAsNumber()]
248
249class DecrWithDefault(Action):
250
251    preconditions = [MaybeExistsAsNumber(), IsUnlockedCondition()]
252    effect = ArithmeticEffect(-1)
253    postconditions = [ExistsAsNumber()]
254
255######################################################################
256# Driver
257######################################################################
258
259class TestFile(object):
260
261    def __init__(self, path, n=10):
262        self.tmpfilenames = ["%s_%d.c.tmp" % (path, i) for i in range(n)]
263        self.files = [open(tfn, "w") for tfn in self.tmpfilenames]
264        self.seq = [list() for f in self.files]
265        self.index = 0
266
267    def finish(self):
268        for f in self.files:
269            f.close()
270
271        for tfn in self.tmpfilenames:
272            nfn = tfn[:-4]
273            assert (nfn + '.tmp') == tfn
274            os.rename(tfn, nfn)
275
276    def nextfile(self):
277        self.index = self.index + 1
278        if self.index >= len(self.files):
279            self.index = 0
280
281    def write(self, s):
282        self.files[self.index].write(s)
283
284    def addseq(self, seq):
285        self.seq[self.index].append(seq)
286
287class EngineTestAppDriver(Driver):
288
289    def __init__(self, writer=sys.stdout):
290        self.writer = writer
291
292    def output(self, s):
293        self.writer.write(s)
294
295    def preSuite(self, seq):
296        files = [self.writer]
297        if isinstance(self.writer, TestFile):
298            files = self.writer.files
299        for f in files:
300            f.write('#include "tests/suite_stubs.h"\n\n')
301
302    def testName(self, seq):
303        return 'test_' + '_'.join(a.name for a in seq)
304
305    def shouldRunSequence(self, seq):
306        # Skip any sequence that leads to known failures
307        ok = True
308        sclasses = [type(a) for a in seq]
309        # A list of lists of classes such that any test that includes
310        # the inner list in order should be skipped.
311        bads = []
312        for b in bads:
313            try:
314                nextIdx = 0
315                for c in b:
316                    nextIdx = sclasses.index(c, nextIdx) + 1
317                ok = False
318            except ValueError:
319                pass # Didn't find it, move in
320
321        return ok
322
323    def startSequence(self, seq):
324        if isinstance(self.writer, TestFile):
325            self.writer.nextfile()
326            self.writer.addseq(seq)
327
328        f = "static enum test_result %s" % self.testName(seq)
329        self.output(("%s(ENGINE_HANDLE *h,\n%sENGINE_HANDLE_V1 *h1) {\n"
330                     % (f, " " * (len(f) + 1))))
331
332        self.handled = self.shouldRunSequence(seq)
333
334        if not self.handled:
335            self.output("    (void)h;\n")
336            self.output("    (void)h1;\n")
337            self.output("    return PENDING;\n")
338            self.output("}\n\n")
339
340    def startAction(self, action):
341        if not self.handled:
342            return
343
344        if isinstance(action, Delay):
345            s = "    delay(expiry+1);"
346        elif isinstance(action, WaitForLock):
347            s = "    delay(locktime+1);"
348        elif isinstance(action, Flush):
349            s = "    flush(h, h1);"
350        elif isinstance(action, Delete):
351            s = '    del(h, h1);'
352        else:
353            s = '    %s(h, h1);' % (action.name)
354        self.output(s + "\n")
355
356    def _writeList(self, writer, fname, seq):
357        writer.write("""MEMCACHED_PUBLIC_API
358engine_test_t* %s(void) {
359
360    static engine_test_t tests[]  = {
361
362""" % fname)
363        for seq in sorted(seq):
364            writer.write('        {"%s",\n         %s,\n         NULL, teardown, NULL, NULL, NULL},\n' % (
365                    ', '.join(a.name for a in seq),
366                    self.testName(seq)))
367
368        writer.write("""        {NULL, NULL, NULL, NULL, NULL, NULL, NULL}
369    };
370    return tests;
371}
372""")
373
374    def postSuite(self, seq):
375        if isinstance(self.writer, TestFile):
376            for i in range(len(self.writer.files)):
377                self._writeList(self.writer.files[i],
378                                'get_tests_%d' % i,
379                                self.writer.seq[i])
380        else:
381            self._writeList(self.writer, 'get_tests', seq)
382
383    def endSequence(self, seq, state):
384        if not self.handled:
385            return
386
387        val = state.get(TESTKEY)
388        if val:
389            self.output('    checkValue(h, h1, "%s");\n' % val)
390        else:
391            self.output('    assertNotExists(h, h1);\n')
392        self.output("    return SUCCESS;\n")
393        self.output("}\n\n")
394
395    def endAction(self, action, state, errored):
396        if not self.handled:
397            return
398
399        value = state.get(TESTKEY)
400        if value:
401            vs = ' /* value is "%s" */\n' % value
402        else:
403            vs = ' /* value is not defined */\n'
404
405        if errored:
406            self.output("    assertHasError();" + vs)
407        else:
408            self.output("    assertHasNoError();" + vs)
409
410if __name__ == '__main__':
411    w = TestFile('generated_suite')
412    breakdancer.runTest(breakdancer.findActions(globals().values()),
413                        EngineTestAppDriver(w))
414    w.finish()
415