From 1afa108cc28b7b549866007946241fb7714372ab Mon Sep 17 00:00:00 2001 From: Michael Hanselmann Date: Fri, 30 Mar 2012 16:52:30 +0200 Subject: [PATCH] Add more RAPI test utilities This patch adds a mock RAPI client to test input values to methods. All methods either raise an exception if there was a problem or return None. Third-party code can use this to test their input values to the RAPI client. Signed-off-by: Michael Hanselmann Reviewed-by: Iustin Pop --- lib/rapi/testutils.py | 186 +++++++++++++++++++++++++++++++- test/ganeti.rapi.testutils_unittest.py | 95 +++++++++++++++- 2 files changed, 279 insertions(+), 2 deletions(-) diff --git a/lib/rapi/testutils.py b/lib/rapi/testutils.py index 258f379..f960381 100644 --- a/lib/rapi/testutils.py +++ b/lib/rapi/testutils.py @@ -33,6 +33,15 @@ from cStringIO import StringIO from ganeti import errors from ganeti import opcodes from ganeti import http +from ganeti import server +from ganeti import utils +from ganeti import compat +from ganeti import luxi +from ganeti import rapi + +import ganeti.http.server # pylint: disable=W0611 +import ganeti.server.rapi +import ganeti.rapi.client _URI_RE = re.compile(r"https://(?P.*):(?P\d+)(?P/.*)") @@ -65,7 +74,7 @@ def _HideInternalErrors(fn): def wrapper(*args, **kwargs): try: return fn(*args, **kwargs) - except errors.GenericError, err: + except (errors.GenericError, rapi.client.GanetiApiError), err: raise VerificationError("Unhandled Ganeti error: %s" % err) return wrapper @@ -185,3 +194,178 @@ class FakeCurl: self._info[pycurl.RESPONSE_CODE] = code if resp_body is not None: writefn(resp_body) + + +class _RapiMock: + """Mocking out the RAPI server parts. + + """ + def __init__(self, user_fn, luxi_client): + """Initialize this class. + + @type user_fn: callable + @param user_fn: Function to authentication username + @param luxi_client: A LUXI client implementation + + """ + self.handler = \ + server.rapi.RemoteApiHandler(user_fn, _client_cls=luxi_client) + + def FetchResponse(self, path, method, headers, request_body): + """This is a callback method used to fetch a response. + + This method is called by the FakeCurl.perform method + + @type path: string + @param path: Requested path + @type method: string + @param method: HTTP method + @type request_body: string + @param request_body: Request body + @type headers: mimetools.Message + @param headers: Request headers + @return: Tuple containing status code and response body + + """ + req_msg = http.HttpMessage() + req_msg.start_line = \ + http.HttpClientToServerStartLine(method, path, http.HTTP_1_0) + req_msg.headers = headers + req_msg.body = request_body + + (_, _, _, resp_msg) = \ + http.server.HttpResponder(self.handler)(lambda: (req_msg, None)) + + return (resp_msg.start_line.code, resp_msg.body) + + +class _TestLuxiTransport: + """Mocked LUXI transport. + + Raises L{errors.RapiTestResult} for all method calls, no matter the + arguments. + + """ + def __init__(self, record_fn, address, timeouts=None): # pylint: disable=W0613 + """Initializes this class. + + """ + self._record_fn = record_fn + + def Close(self): + pass + + def Call(self, data): + """Calls LUXI method. + + In this test class the method is not actually called, but added to a list + of called methods and then an exception (L{errors.RapiTestResult}) is + raised. There is no return value. + + """ + (method, _, _) = luxi.ParseRequest(data) + + # Take a note of called method + self._record_fn(method) + + # Everything went fine until here, so let's abort the test + raise errors.RapiTestResult + + +class _LuxiCallRecorder: + """Records all called LUXI client methods. + + """ + def __init__(self): + """Initializes this class. + + """ + self._called = set() + + def Record(self, name): + """Records a called function name. + + """ + self._called.add(name) + + def CalledNames(self): + """Returns a list of called LUXI methods. + + """ + return self._called + + def __call__(self): + """Creates an instrumented LUXI client. + + The LUXI client will record all method calls (use L{CalledNames} to + retrieve them). + + """ + return luxi.Client(transport=compat.partial(_TestLuxiTransport, + self.Record)) + + +def _TestWrapper(fn, *args, **kwargs): + """Wrapper for ignoring L{errors.RapiTestResult}. + + """ + try: + return fn(*args, **kwargs) + except errors.RapiTestResult: + # Everything was fine up to the point of sending a LUXI request + return NotImplemented + + +class InputTestClient: + """Test version of RAPI client. + + Instances of this class can be used to test input arguments for RAPI client + calls. See L{rapi.client.GanetiRapiClient} for available methods and their + arguments. Functions can return C{NotImplemented} if all arguments are + acceptable, but a LUXI request would be necessary to provide an actual return + value. In case of an error, L{VerificationError} is raised. + + @see: An example on how to use this class can be found in + C{doc/examples/rapi_testutils.py} + + """ + def __init__(self): + """Initializes this class. + + """ + username = utils.GenerateSecret() + password = utils.GenerateSecret() + + def user_fn(wanted): + """Called to verify user credentials given in HTTP request. + + """ + assert username == wanted + return http.auth.PasswordFileUser(username, password, + [rapi.RAPI_ACCESS_WRITE]) + + self._lcr = _LuxiCallRecorder() + + # Create a mock RAPI server + handler = _RapiMock(user_fn, self._lcr) + + self._client = \ + rapi.client.GanetiRapiClient("master.example.com", + username=username, password=password, + curl_factory=lambda: FakeCurl(handler)) + + def _GetLuxiCalls(self): + """Returns the names of all called LUXI client functions. + + """ + return self._lcr.CalledNames() + + def __getattr__(self, name): + """Finds method by name. + + The method is wrapped using L{_TestWrapper} to produce the actual test + result. + + """ + return _HideInternalErrors(compat.partial(_TestWrapper, + getattr(self._client, name))) diff --git a/test/ganeti.rapi.testutils_unittest.py b/test/ganeti.rapi.testutils_unittest.py index 55c0c9a..d26bd19 100755 --- a/test/ganeti.rapi.testutils_unittest.py +++ b/test/ganeti.rapi.testutils_unittest.py @@ -27,13 +27,32 @@ from ganeti import compat from ganeti import constants from ganeti import errors from ganeti import opcodes +from ganeti import luxi from ganeti import rapi +from ganeti import utils import ganeti.rapi.testutils +import ganeti.rapi.client import testutils +KNOWN_UNUSED_LUXI = frozenset([ + luxi.REQ_SUBMIT_MANY_JOBS, + luxi.REQ_ARCHIVE_JOB, + luxi.REQ_AUTOARCHIVE_JOBS, + luxi.REQ_QUERY_EXPORTS, + luxi.REQ_QUERY_CONFIG_VALUES, + luxi.REQ_QUERY_TAGS, + luxi.REQ_QUEUE_SET_DRAIN_FLAG, + luxi.REQ_SET_WATCHER_PAUSE, + ]) + + +# Global variable for storing used LUXI calls +_used_luxi_calls = None + + class TestHideInternalErrors(unittest.TestCase): def test(self): def inner(): @@ -96,5 +115,79 @@ class TestVerifyOpResult(unittest.TestCase): vor(opcodes.OpTestDummy.OP_ID, None) +class TestInputTestClient(unittest.TestCase): + def setUp(self): + self.cl = rapi.testutils.InputTestClient() + + def tearDown(self): + _used_luxi_calls.update(self.cl._GetLuxiCalls()) + + def testGetInfo(self): + self.assertTrue(self.cl.GetInfo() is NotImplemented) + + def testPrepareExport(self): + result = self.cl.PrepareExport("inst1.example.com", + constants.EXPORT_MODE_LOCAL) + self.assertTrue(result is NotImplemented) + self.assertRaises(rapi.client.GanetiApiError, self.cl.PrepareExport, + "inst1.example.com", "###invalid###") + + def testGetJobs(self): + self.assertTrue(self.cl.GetJobs() is NotImplemented) + + def testQuery(self): + result = self.cl.Query(constants.QR_NODE, ["name"]) + self.assertTrue(result is NotImplemented) + + def testQueryFields(self): + result = self.cl.QueryFields(constants.QR_INSTANCE) + self.assertTrue(result is NotImplemented) + + def testCancelJob(self): + self.assertTrue(self.cl.CancelJob("1") is NotImplemented) + + def testGetNodes(self): + self.assertTrue(self.cl.GetNodes() is NotImplemented) + + def testGetInstances(self): + self.assertTrue(self.cl.GetInstances() is NotImplemented) + + def testGetGroups(self): + self.assertTrue(self.cl.GetGroups() is NotImplemented) + + def testWaitForJobChange(self): + result = self.cl.WaitForJobChange("1", ["id"], None, None) + self.assertTrue(result is NotImplemented) + + +class CustomTestRunner(unittest.TextTestRunner): + def run(self, *args): + global _used_luxi_calls + assert _used_luxi_calls is None + + diff = (KNOWN_UNUSED_LUXI - luxi.REQ_ALL) + assert not diff, "Non-existing LUXI calls listed as unused: %s" % diff + + _used_luxi_calls = set() + try: + # Run actual tests + result = unittest.TextTestRunner.run(self, *args) + + diff = _used_luxi_calls & KNOWN_UNUSED_LUXI + if diff: + raise AssertionError("LUXI methods marked as unused were called: %s" % + utils.CommaJoin(diff)) + + diff = (luxi.REQ_ALL - KNOWN_UNUSED_LUXI - _used_luxi_calls) + if diff: + raise AssertionError("The following LUXI methods were not used: %s" % + utils.CommaJoin(diff)) + finally: + # Reset global variable + _used_luxi_calls = None + + return result + + if __name__ == "__main__": - testutils.GanetiTestProgram() + testutils.GanetiTestProgram(testRunner=CustomTestRunner) -- 1.7.10.4