diff options
Diffstat (limited to 'src/third_party/wiredtiger/test/suite/wthooks.py')
-rwxr-xr-x | src/third_party/wiredtiger/test/suite/wthooks.py | 259 |
1 files changed, 259 insertions, 0 deletions
diff --git a/src/third_party/wiredtiger/test/suite/wthooks.py b/src/third_party/wiredtiger/test/suite/wthooks.py new file mode 100755 index 00000000000..56827350e29 --- /dev/null +++ b/src/third_party/wiredtiger/test/suite/wthooks.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python +# +# Public Domain 2014-present MongoDB, Inc. +# Public Domain 2008-2014 WiredTiger, Inc. +# +# This is free and unencumbered software released into the public domain. +# +# Anyone is free to copy, modify, publish, use, compile, sell, or +# distribute this software, either in source code form or as a compiled +# binary, for any purpose, commercial or non-commercial, and by any +# means. +# +# In jurisdictions that recognize copyright laws, the author or authors +# of this software dedicate any and all copyright interest in the +# software to the public domain. We make this dedication for the benefit +# of the public at large and to the detriment of our heirs and +# successors. We intend this dedication to be an overt act of +# relinquishment in perpetuity of all present and future rights to this +# software under copyright law. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# [TEST_TAGS] +# ignored_file +# [END_TAGS] +# +# WiredTigerHookManager +# Manage running of hooks +# +from __future__ import print_function + +import os, sys +from importlib import import_module +from abc import ABC, abstractmethod +import wiredtiger + +# Three kinds of hooks available: +HOOK_REPLACE = 1 # replace the call with the hook function +HOOK_NOTIFY = 2 # call the hook function after the function +HOOK_ARGS = 3 # transform the arg list before the call + +# Print to /dev/tty for debugging, since anything extraneous to stdout/stderr will +# cause a test error. +def tty(message): + from wttest import WiredTigerTestCase + WiredTigerTestCase.tty(message) + +################ +# Hooks Overview +# +# Here are some useful terms to know, with some commentary for each. +# +# API functions +# potentially any WiredTiger API functions that a hook creator wishes to modify (like +# Session.rename). In Python most everything is an object. Of course an instance of +# "Session" is an object, but also the "Session" class itself is an object. The Session.rename +# function is also an object (of a certain form that can be called). Also in Python, +# attributes on an object don't have to be "pre-declared", they can be created at any time. +# So it's easy to imagine assigning Session._rename_orig to be (the original value of) +# Session.rename, and then assigning Session.rename to be some other function object, that +# knows how to do something and then perhaps calls Session._rename_orig . This is the +# essence of the hook concept. +# +# Hook Creator: +# A way to attach a set of "behavior modifications" to various API functions. More precisely, +# a hook creator derives from WiredTigerHookCreator and sets up a number of "hook functions", +# that are actions that are done either just before, after, or instead of, an API function. +# A XxxxHookCreator lives in a hook_xxxx.py file. When a HookCreator is loaded, it may be +# given an optional argument. This argument comes from the original python command line. +# For example, "python run.py --hook abc" loads hook_abc.py (where it expects to find a hook). +# "python run.py --hook abc=123" loads hook_abc.py with an argument "123". +# +# Hook Function: +# One function that will be called before, after or instead of, an API function. A hook +# function will be bound to an API function. It is the job of the HookCreator to set up that +# binding. It is possible to have multiple hook functions bound to the same API function. +# A hook function that replaces an API function will have the same args as the function +# it replaces (but there is a trick to give it additional context if needed - +# see session_create_replace in hook_demo.py). + +# For every API function altered, there is one of these objects +# stashed in the <class>._<api_name>_hooks attribute. +class WiredTigerHookInfo(object): + def __init__(self): + self.arg_funcs = [] # The set of hook functions for manipulating arguments + self.notify_funcs = [] # The set of hook functions for manipulating arguments + # At the moment, we can only replace a method once. + # If needed, we can think about removing this restriction. + self.replace_func = None + +# hooked_function - +# A helper function for the hook manager. +def hooked_function(self, orig_func, hook_info_name, *args): + hook_info = getattr(self, hook_info_name) + + notifies = [] + replace_func = None + + # The three kinds of hooks are acted upon at different times. + # Before we call the function, we modify the args as indicated + # by hooks. Then we call the function, possibly with a replacement. + # Finally, we'll call any notify hooks. + # + # We only walk through the hook list once, and process the config + # hooks while we're doing that, and copy any other hooks needed. + for hook_func in hook_info.arg_funcs: + args = hook_func(self, args) + call_func = hook_info.replace_func + if call_func == None: + call_func = orig_func + if self == wiredtiger: + ret = call_func(*args) + else: + ret = call_func(self, *args) + for hook_func in hook_info.notify_funcs: + hook_func(ret, self, *args) + return ret + +# WiredTigerHookManager - +# The hook manager class. There is only one hook manager. It is responsible for finding all the +# HookCreators at the beginning of the run, and calling setup_hooks() for each one, to have it bind +# hook functions to API functions. The hook manager is initialized with a list of hook names. Each +# name is expanded, for example, "demo" causes the hook manager to load hook_demo.py, and to call +# the "initialize" global function in that file. We expect "initialize" to return a list of objects +# (hooks) derived from WiredTigerHook (class defined below). Generally, "initialize" returns a +# single object (setting up some number of "hook functions") but to allow flexibility for different +# sorts of packaging, we allow any number of hooks to be returned. +# +# A hook can set up any number of "hook functions". See hook_demo.py for a sample hook class. +class WiredTigerHookManager(object): + def __init__(self, hooknames = []): + self.hooks = [] + names_seen = [] + for name in hooknames: + # The hooks are indicated as "somename=arg" or simply "somename". + # hook_somename.py will be imported, and initialized with the arg. + # Names must be unique, as we stash some info into extra fields + # on the connection/session/cursor, these are named using the + # unique name of the hook. + if '=' in name: + name,arg = name.split('=', 1) + else: + arg = None + if name in names_seen: + raise Exception(name + ': hook name cannot be used multiple times') + names_seen.append(name) + + modname = 'hook_' + name + try: + imported = import_module(modname) + for hook in imported.initialize(arg): + hook._initialize(name, self) + self.hooks.append(hook) + except: + print('Cannot import hook: ' + name + ', check file ' + modname + '.py') + raise + for hook in self.hooks: + hook.setup_hooks() + + def add_hook(self, clazz, method_name, hook_type, hook_func): + if not hasattr(clazz, method_name): + raise Exception('Cannot find method ' + method_name + ' on class ' + str(clazz)) + + # We need to set up some extra attributes on the Connection class. + # Given that the method name is XXXX, and class is Connection, here's what we're doing: + # orig = wiredtiger.Connection.XXXX + # wiredtiger.Connection._XXXX_hooks = WiredTigerHookInfo() + # wiredtiger.Connection._XXXX_orig = wiredtiger.Connection.XXXX + # wiredtiger.Connection.XXXX = lambda self, *args: + # hooked_function(self, orig, '_XXXX_hooks', *args) + hook_info_name = '_' + method_name + '_hooks' + orig_name = '_' + method_name + '_orig' + if not hasattr(clazz, hook_info_name): + #tty('Setting up hook on ' + str(clazz) + '.' + method_name) + orig_func = getattr(clazz, method_name) + if orig_func == None: + raise Exception('method ' + method_name + ' hook setup: method does not exist') + setattr(clazz, hook_info_name, WiredTigerHookInfo()) + + # If we're using the wiredtiger module and not a class, we need a slightly different + # style of hooked_function, since there is no self. What would be the "self" argument + # is in fact the class. + if clazz == wiredtiger: + f = lambda *args: hooked_function(wiredtiger, orig_func, hook_info_name, *args) + else: + f = lambda self, *args: hooked_function(self, orig_func, hook_info_name, *args) + setattr(clazz, method_name, f) + setattr(clazz, orig_name, orig_func) + + # Now add to the list of hook functions + # If it's a replace hook, we only allow one of them for a given method name + hook_info = getattr(clazz, hook_info_name) + if hook_type == HOOK_ARGS: + hook_info.arg_funcs.append(hook_func) + elif hook_type == HOOK_NOTIFY: + hook_info.notify_funcs.append(hook_func) + elif hook_type == HOOK_REPLACE: + if hook_info.replace_func == None: + hook_info.replace_func = hook_func + else: + raise Exception('method ' + method_name + ' hook setup: trying to replace the same method with two hooks') + #tty('Setting up hooks list in ' + str(clazz) + '.' + hook_info_name) + + def get_function(self, clazz, method_name): + orig_name = '_' + method_name + '_orig' + if hasattr(clazz, orig_name): + orig_func = getattr(clazz, orig_name) + else: + orig_func = getattr(clazz, method_name) + return orig_func + + def filter_tests(self, tests): + for hook in self.hooks: + tests = hook.filter_tests(tests) + return tests + +class HookCreatorProxy(object): + def __init__(self, hookmgr, clazz): + self.hookmgr = hookmgr + self.clazz = clazz + + # Get the original function/method before any hooks applied + def __getitem__(self, name): + return self.hookmgr.get_function(self.clazz, name) + + # Get the original function/method before any hooks applied + def __setitem__(self, name, value): + try: + hooktype = int(value[0]) + fcn = value[1] + except: + raise ValueError('value must be (HOOK_xxxx, function)') + self.hookmgr.add_hook(self.clazz, name, hooktype, fcn) + +# Hooks must derive from this class +class WiredTigerHookCreator(ABC): + # This is called right after creation and should not be overridden. + def _initialize(self, name, hookmgr): + self.name = name + self.hookmgr = hookmgr + self.wiredtiger = HookCreatorProxy(self.hookmgr, wiredtiger) + self.Connection = HookCreatorProxy(self.hookmgr, wiredtiger.Connection) + self.Session = HookCreatorProxy(self.hookmgr, wiredtiger.Session) + self.Cursor = HookCreatorProxy(self.hookmgr, wiredtiger.Cursor) + + # default version of filter_tests, can be overridden + def filter_tests(self, tests): + return tests + + @abstractmethod + def setup_hooks(self): + """Set up all hooks using add_*_hook methods.""" + return |