1#!/usr/bin/env python
2#
3# Public Domain 2014-2018 MongoDB, Inc.
4# Public Domain 2008-2014 WiredTiger, Inc.
5#
6# This is free and unencumbered software released into the public domain.
7#
8# Anyone is free to copy, modify, publish, use, compile, sell, or
9# distribute this software, either in source code form or as a compiled
10# binary, for any purpose, commercial or non-commercial, and by any
11# means.
12#
13# In jurisdictions that recognize copyright laws, the author or authors
14# of this software dedicate any and all copyright interest in the
15# software to the public domain. We make this dedication for the benefit
16# of the public at large and to the detriment of our heirs and
17# successors. We intend this dedication to be an overt act of
18# relinquishment in perpetuity of all present and future rights to this
19# software under copyright law.
20#
21# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
24# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
25# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
26# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27# OTHER DEALINGS IN THE SOFTWARE.
28
29import testscenarios
30import suite_random
31
32# wtscenarios.py
33#    Support scenarios based testing
34def powerrange(start, stop, mult):
35    """
36    Like xrange, generates a range from start to stop.
37    Unlike xrange, the range is inclusive of stop,
38    each step is multiplicative, and as a special case,
39    the stop value is returned as the last item.
40    """
41    val = start
42    while val <= stop:
43        yield val
44        newval = val * mult
45        if val < stop and newval > stop:
46            val = stop
47        else:
48            val = newval
49
50def log2chr(val):
51    """
52    For the log-base 2 of val, return the numeral or letter
53    corresponding to val (which is < 36).  Hence, 1 return '0',
54    2 return '1', 2*15 returns 'f', 2*16 returns 'g', etc.
55    """
56    p = 0
57    while val >= 2:
58        p += 1
59        val /= 2
60    if p < 10:
61        return chr(ord('0') + p)
62    else:
63        return chr(ord('a') + p - 10)
64
65megabyte = 1024 * 1024
66
67def make_scenarios(*args, **kwargs):
68    """
69    The standard way to create scenarios for WT tests.
70    Scenarios can be combined by listing them all as arguments.
71    If some scenario combinations should not be included,
72    a include= argument function may be listed, which given a name and
73    dictionary argument, returns True if the scenario should be included.
74    A final prune= and/or prunelong= argument may be given that
75    forces the list of entries in the scenario to be pruned.
76    The result is a (combined) scenario that has been checked
77    for name duplicates and has been given names and numbers.
78    """
79    scenes = multiply_scenarios('.', *args)
80    pruneval = None
81    prunelong = None
82    includefunc = None
83    for key in kwargs:
84        if key == 'prune':
85            pruneval = kwargs[key]
86        elif key == 'prunelong':
87            prunelong = kwargs[key]
88        elif key == 'include':
89            includefunc = kwargs[key]
90        else:
91            raise AssertionError(
92                'make_scenarios: unexpected named arg: ' + key)
93    if includefunc:
94        scenes = [(name, d) for (name, d) in scenes if includefunc(name, d)]
95    if pruneval != None or prunelong != None:
96        pruneval = pruneval if pruneval != None else -1
97        prunelong = prunelong if prunelong != None else -1
98        scenes = prune_scenarios(scenes, pruneval, prunelong)
99    return number_scenarios(scenes)
100
101def check_scenarios(scenes):
102    """
103    Make sure all scenarios have unique case insensitive names
104    """
105    assert len(scenes) == len(dict((k.lower(), v) for k, v in scenes))
106    return scenes
107
108def multiply_scenarios(sep, *args):
109    """
110    Create the cross product of two lists of scenarios
111    """
112    result = None
113    for scenes in args:
114        if result == None:
115            result = scenes
116        else:
117            total = []
118            for scena in result:
119                for scenb in scenes:
120                    # Create a merged scenario with a concatenated name
121                    name = scena[0] + sep + scenb[0]
122                    tdict = {}
123                    tdict.update(scena[1])
124                    tdict.update(scenb[1])
125
126                    # If there is a 'P' value, it represents the
127                    # probability that we want to use this scenario
128                    # If both scenarios list a probability, multiply them.
129                    if 'P' in scena[1] and 'P' in scenb[1]:
130                        P = scena[1]['P'] * scenb[1]['P']
131                        tdict['P'] = P
132                    total.append((name, tdict))
133            result = total
134    return check_scenarios(result)
135
136def prune_sorter_key(scene):
137    """
138    Used by prune_scenerios to extract key for sorting.
139    The key is the saved random value multiplied by
140    the probability of choosing.
141    """
142    p = 1.0
143    if 'P' in scene[1]:
144        p = scene[1]['P']
145    return p * scene[1]['_rand']
146
147def prune_resort_key(scene):
148    """
149    Used by prune_scenerios to extract the original ordering key for sorting.
150    """
151    return scene[1]['_order']
152
153def set_long_run(islong):
154    global _is_long_run
155    _is_long_run = islong
156
157def prune_scenarios(scenes, default_count = -1, long_count = -1):
158    """
159    Use listed probabilities for pruning the list of scenarios.
160    That is, the highest probability (value of P in the scendario)
161    are chosen more often.  With just one argument, only scenarios
162    with P > .5 are returned half the time, etc. A second argument
163    limits the number of scenarios. When a third argument is present,
164    it is a separate limit for a long run.
165    """
166    global _is_long_run
167    r = suite_random.suite_random()
168    result = []
169    if default_count == -1:
170        # Missing second arg - return those with P == .3 at
171        # 30% probability, for example.
172        for scene in scenes:
173            if 'P' in scene[1]:
174                p = scene[1]['P']
175                if p < r.rand_float():
176                    continue
177            result.append(scene)
178        return result
179    else:
180        # With at least a second arg present, we'll want a specific count
181        # of items returned.  So we'll sort them all and choose
182        # the top number.  Not the most efficient solution,
183        # but it's easy.
184        if _is_long_run and long_count != -1:
185            count = long_count
186        else:
187            count = default_count
188
189        l = len(scenes)
190        if l <= count:
191            return scenes
192        if count == 0:
193            return []
194        order = 0
195        for scene in scenes:
196            scene[1]['_rand'] = r.rand_float()
197            scene[1]['_order'] = order
198            order += 1
199        scenes = sorted(scenes, key=prune_sorter_key) # random sort driven by P
200        scenes = scenes[l-count:l]                    # truncate to get best
201        scenes = sorted(scenes, key=prune_resort_key) # original order
202        for scene in scenes:
203            del scene[1]['_rand']
204            del scene[1]['_order']
205        return check_scenarios(scenes)
206
207def filter_scenarios(scenes, pred):
208    """
209    Filter scenarios that match a predicate
210    """
211    return [s for s in scenes if pred(*s)]
212
213def number_scenarios(scenes):
214    """
215    Add a 'scenario_number' and 'scenario_name' variable to each scenario.
216    The hash table for each scenario is altered!
217    """
218    count = 0
219    for scene in scenes:
220        scene[1]['scenario_name'] = scene[0]
221        scene[1]['scenario_number'] = count
222        count += 1
223    return check_scenarios(scenes)
224
225def quick_scenarios(fieldname, values, probabilities):
226    """
227    Quickly build common scenarios, like:
228       [('foo', dict(somefieldname='foo')),
229       ('bar', dict(somefieldname='bar')),
230       ('boo', dict(somefieldname='boo'))]
231    via a call to:
232       quick_scenario('somefieldname', ['foo', 'bar', 'boo'])
233    """
234    result = []
235    if probabilities == None:
236        plen = 0
237    else:
238        plen = len(probabilities)
239    ppos = 0
240    for value in values:
241        if ppos >= plen:
242            d = dict([[fieldname, value]])
243        else:
244            p = probabilities[ppos]
245            ppos += 1
246            d = dict([[fieldname, value],['P', p]])
247        result.append((str(value), d))
248    return result
249
250class wtscenario:
251    """
252    A set of generators for different test scenarios
253    """
254
255    @staticmethod
256    def session_create_scenario():
257        """
258        Return a set of scenarios with the name of this method
259        'session_create_scenario' as the name of instance
260        variable containing a wtscenario object.  The wtscenario
261        object can be queried to get a config string.
262        Each scenario is named according to the shortName() method.
263        """
264        s = [
265            ('default', dict(session_create_scenario=wtscenario())) ]
266        for imin in powerrange(512, 512*megabyte, 1024):
267            for imax in powerrange(imin, 512*megabyte, 1024):
268                for lmin in powerrange(512, 512*megabyte, 1024):
269                    for lmax in powerrange(lmin, 512*megabyte, 1024):
270                        for cache in [megabyte, 32*megabyte, 1000*megabyte]:
271                            scen = wtscenario()
272                            scen.ioverflow = max(imin / 40, 40)
273                            scen.imax = imax
274                            scen.loverflow = max(lmin / 40, 40)
275                            scen.lmax = lmax
276                            scen.cache_size = cache
277                            s.append((scen.shortName(), dict(session_create_scenario=scen)))
278        return make_scenarios(s)
279
280    def shortName(self):
281        """
282        Return a name of a scenario, based on the 'log2chr-ed numerals'
283        representing the four values for {internal,leaf} {minimum, maximum}
284        page size.
285        """
286        return 'scen_' + log2chr(self.ioverflow) + log2chr(self.imax) + log2chr(self.loverflow) + log2chr(self.lmax) + log2chr(self.cache_size)
287
288    def configString(self):
289        """
290        Return the associated configuration string
291        """
292        res = ''
293        if hasattr(self, 'ioverflow'):
294            res += ',internal_item_max=' + str(self.ioverflow)
295        if hasattr(self, 'imax'):
296            res += ',internal_page_max=' + str(self.imax)
297            if self.imax < 4*1024:
298                res += ',allocation_size=512'
299        if hasattr(self, 'loverflow'):
300            res += ',leaf_item_max=' + str(self.loverflow)
301        if hasattr(self, 'lmax'):
302            res += ',leaf_page_max=' + str(self.lmax)
303            if self.lmax < 4*1024:
304                res += ',allocation_size=512'
305        return res
306