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