Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / testutils.py @ 31d827d1

History | View | Annotate | Download (9.6 kB)

1 a85f23fa Michael Hanselmann
#
2 a85f23fa Michael Hanselmann
#
3 a85f23fa Michael Hanselmann
4 a85f23fa Michael Hanselmann
# Copyright (C) 2012 Google Inc.
5 a85f23fa Michael Hanselmann
#
6 a85f23fa Michael Hanselmann
# This program is free software; you can redistribute it and/or modify
7 a85f23fa Michael Hanselmann
# it under the terms of the GNU General Public License as published by
8 a85f23fa Michael Hanselmann
# the Free Software Foundation; either version 2 of the License, or
9 a85f23fa Michael Hanselmann
# (at your option) any later version.
10 a85f23fa Michael Hanselmann
#
11 a85f23fa Michael Hanselmann
# This program is distributed in the hope that it will be useful, but
12 a85f23fa Michael Hanselmann
# WITHOUT ANY WARRANTY; without even the implied warranty of
13 a85f23fa Michael Hanselmann
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 a85f23fa Michael Hanselmann
# General Public License for more details.
15 a85f23fa Michael Hanselmann
#
16 a85f23fa Michael Hanselmann
# You should have received a copy of the GNU General Public License
17 a85f23fa Michael Hanselmann
# along with this program; if not, write to the Free Software
18 a85f23fa Michael Hanselmann
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 a85f23fa Michael Hanselmann
# 02110-1301, USA.
20 a85f23fa Michael Hanselmann
21 a85f23fa Michael Hanselmann
22 a85f23fa Michael Hanselmann
"""Remote API test utilities.
23 a85f23fa Michael Hanselmann

24 a85f23fa Michael Hanselmann
"""
25 a85f23fa Michael Hanselmann
26 a85f23fa Michael Hanselmann
import logging
27 f90a1ab5 René Nussbaumer
import re
28 d9492490 Michael Hanselmann
import mimetools
29 d9492490 Michael Hanselmann
import base64
30 f90a1ab5 René Nussbaumer
import pycurl
31 d9492490 Michael Hanselmann
from cStringIO import StringIO
32 a85f23fa Michael Hanselmann
33 a85f23fa Michael Hanselmann
from ganeti import errors
34 a85f23fa Michael Hanselmann
from ganeti import opcodes
35 d9492490 Michael Hanselmann
from ganeti import http
36 1afa108c Michael Hanselmann
from ganeti import server
37 1afa108c Michael Hanselmann
from ganeti import utils
38 1afa108c Michael Hanselmann
from ganeti import compat
39 1afa108c Michael Hanselmann
from ganeti import luxi
40 1afa108c Michael Hanselmann
from ganeti import rapi
41 1afa108c Michael Hanselmann
42 1afa108c Michael Hanselmann
import ganeti.http.server # pylint: disable=W0611
43 1afa108c Michael Hanselmann
import ganeti.server.rapi
44 1afa108c Michael Hanselmann
import ganeti.rapi.client
45 a85f23fa Michael Hanselmann
46 a85f23fa Michael Hanselmann
47 f90a1ab5 René Nussbaumer
_URI_RE = re.compile(r"https://(?P<host>.*):(?P<port>\d+)(?P<path>/.*)")
48 f90a1ab5 René Nussbaumer
49 f90a1ab5 René Nussbaumer
50 a85f23fa Michael Hanselmann
class VerificationError(Exception):
51 a85f23fa Michael Hanselmann
  """Dedicated error class for test utilities.
52 a85f23fa Michael Hanselmann

53 a85f23fa Michael Hanselmann
  This class is used to hide all of Ganeti's internal exception, so that
54 a85f23fa Michael Hanselmann
  external users of these utilities don't have to integrate Ganeti's exception
55 a85f23fa Michael Hanselmann
  hierarchy.
56 a85f23fa Michael Hanselmann

57 a85f23fa Michael Hanselmann
  """
58 a85f23fa Michael Hanselmann
59 a85f23fa Michael Hanselmann
60 a85f23fa Michael Hanselmann
def _GetOpById(op_id):
61 a85f23fa Michael Hanselmann
  """Tries to get an opcode class based on its C{OP_ID}.
62 a85f23fa Michael Hanselmann

63 a85f23fa Michael Hanselmann
  """
64 a85f23fa Michael Hanselmann
  try:
65 a85f23fa Michael Hanselmann
    return opcodes.OP_MAPPING[op_id]
66 a85f23fa Michael Hanselmann
  except KeyError:
67 a85f23fa Michael Hanselmann
    raise VerificationError("Unknown opcode ID '%s'" % op_id)
68 a85f23fa Michael Hanselmann
69 a85f23fa Michael Hanselmann
70 a85f23fa Michael Hanselmann
def _HideInternalErrors(fn):
71 a85f23fa Michael Hanselmann
  """Hides Ganeti-internal exceptions, see L{VerificationError}.
72 a85f23fa Michael Hanselmann

73 a85f23fa Michael Hanselmann
  """
74 a85f23fa Michael Hanselmann
  def wrapper(*args, **kwargs):
75 a85f23fa Michael Hanselmann
    try:
76 a85f23fa Michael Hanselmann
      return fn(*args, **kwargs)
77 1afa108c Michael Hanselmann
    except (errors.GenericError, rapi.client.GanetiApiError), err:
78 a85f23fa Michael Hanselmann
      raise VerificationError("Unhandled Ganeti error: %s" % err)
79 a85f23fa Michael Hanselmann
80 a85f23fa Michael Hanselmann
  return wrapper
81 a85f23fa Michael Hanselmann
82 a85f23fa Michael Hanselmann
83 a85f23fa Michael Hanselmann
@_HideInternalErrors
84 a85f23fa Michael Hanselmann
def VerifyOpInput(op_id, data):
85 a85f23fa Michael Hanselmann
  """Verifies opcode parameters according to their definition.
86 a85f23fa Michael Hanselmann

87 a85f23fa Michael Hanselmann
  @type op_id: string
88 a85f23fa Michael Hanselmann
  @param op_id: Opcode ID (C{OP_ID} attribute), e.g. C{OP_CLUSTER_VERIFY}
89 a85f23fa Michael Hanselmann
  @type data: dict
90 a85f23fa Michael Hanselmann
  @param data: Opcode parameter values
91 a85f23fa Michael Hanselmann
  @raise VerificationError: Parameter verification failed
92 a85f23fa Michael Hanselmann

93 a85f23fa Michael Hanselmann
  """
94 a85f23fa Michael Hanselmann
  op_cls = _GetOpById(op_id)
95 a85f23fa Michael Hanselmann
96 a85f23fa Michael Hanselmann
  try:
97 a85f23fa Michael Hanselmann
    op = op_cls(**data) # pylint: disable=W0142
98 a85f23fa Michael Hanselmann
  except TypeError, err:
99 a85f23fa Michael Hanselmann
    raise VerificationError("Unable to create opcode instance: %s" % err)
100 a85f23fa Michael Hanselmann
101 a85f23fa Michael Hanselmann
  try:
102 a85f23fa Michael Hanselmann
    op.Validate(False)
103 a85f23fa Michael Hanselmann
  except errors.OpPrereqError, err:
104 a85f23fa Michael Hanselmann
    raise VerificationError("Parameter validation for opcode '%s' failed: %s" %
105 a85f23fa Michael Hanselmann
                            (op_id, err))
106 a85f23fa Michael Hanselmann
107 a85f23fa Michael Hanselmann
108 a85f23fa Michael Hanselmann
@_HideInternalErrors
109 a85f23fa Michael Hanselmann
def VerifyOpResult(op_id, result):
110 a85f23fa Michael Hanselmann
  """Verifies opcode results used in tests (e.g. in a mock).
111 a85f23fa Michael Hanselmann

112 a85f23fa Michael Hanselmann
  @type op_id: string
113 a85f23fa Michael Hanselmann
  @param op_id: Opcode ID (C{OP_ID} attribute), e.g. C{OP_CLUSTER_VERIFY}
114 a85f23fa Michael Hanselmann
  @param result: Mocked opcode result
115 a85f23fa Michael Hanselmann
  @raise VerificationError: Return value verification failed
116 a85f23fa Michael Hanselmann

117 a85f23fa Michael Hanselmann
  """
118 a85f23fa Michael Hanselmann
  resultcheck_fn = _GetOpById(op_id).OP_RESULT
119 a85f23fa Michael Hanselmann
120 a85f23fa Michael Hanselmann
  if not resultcheck_fn:
121 a85f23fa Michael Hanselmann
    logging.warning("Opcode '%s' has no result type definition", op_id)
122 a85f23fa Michael Hanselmann
  elif not resultcheck_fn(result):
123 a85f23fa Michael Hanselmann
    raise VerificationError("Given result does not match result description"
124 a85f23fa Michael Hanselmann
                            " for opcode '%s': %s" % (op_id, resultcheck_fn))
125 f90a1ab5 René Nussbaumer
126 f90a1ab5 René Nussbaumer
127 f90a1ab5 René Nussbaumer
def _GetPathFromUri(uri):
128 f90a1ab5 René Nussbaumer
  """Gets the path and query from a URI.
129 f90a1ab5 René Nussbaumer

130 f90a1ab5 René Nussbaumer
  """
131 f90a1ab5 René Nussbaumer
  match = _URI_RE.match(uri)
132 f90a1ab5 René Nussbaumer
  if match:
133 f90a1ab5 René Nussbaumer
    return match.groupdict()["path"]
134 f90a1ab5 René Nussbaumer
  else:
135 f90a1ab5 René Nussbaumer
    return None
136 f90a1ab5 René Nussbaumer
137 f90a1ab5 René Nussbaumer
138 f90a1ab5 René Nussbaumer
class FakeCurl:
139 f90a1ab5 René Nussbaumer
  """Fake cURL object.
140 f90a1ab5 René Nussbaumer

141 f90a1ab5 René Nussbaumer
  """
142 f90a1ab5 René Nussbaumer
  def __init__(self, handler):
143 f90a1ab5 René Nussbaumer
    """Initialize this class
144 f90a1ab5 René Nussbaumer

145 f90a1ab5 René Nussbaumer
    @param handler: Request handler instance
146 f90a1ab5 René Nussbaumer

147 f90a1ab5 René Nussbaumer
    """
148 f90a1ab5 René Nussbaumer
    self._handler = handler
149 f90a1ab5 René Nussbaumer
    self._opts = {}
150 f90a1ab5 René Nussbaumer
    self._info = {}
151 f90a1ab5 René Nussbaumer
152 f90a1ab5 René Nussbaumer
  def setopt(self, opt, value):
153 f90a1ab5 René Nussbaumer
    self._opts[opt] = value
154 f90a1ab5 René Nussbaumer
155 f90a1ab5 René Nussbaumer
  def getopt(self, opt):
156 f90a1ab5 René Nussbaumer
    return self._opts.get(opt)
157 f90a1ab5 René Nussbaumer
158 f90a1ab5 René Nussbaumer
  def unsetopt(self, opt):
159 f90a1ab5 René Nussbaumer
    self._opts.pop(opt, None)
160 f90a1ab5 René Nussbaumer
161 f90a1ab5 René Nussbaumer
  def getinfo(self, info):
162 f90a1ab5 René Nussbaumer
    return self._info[info]
163 f90a1ab5 René Nussbaumer
164 f90a1ab5 René Nussbaumer
  def perform(self):
165 f90a1ab5 René Nussbaumer
    method = self._opts[pycurl.CUSTOMREQUEST]
166 f90a1ab5 René Nussbaumer
    url = self._opts[pycurl.URL]
167 f90a1ab5 René Nussbaumer
    request_body = self._opts[pycurl.POSTFIELDS]
168 f90a1ab5 René Nussbaumer
    writefn = self._opts[pycurl.WRITEFUNCTION]
169 f90a1ab5 René Nussbaumer
170 d9492490 Michael Hanselmann
    if pycurl.HTTPHEADER in self._opts:
171 d9492490 Michael Hanselmann
      baseheaders = "\n".join(self._opts[pycurl.HTTPHEADER])
172 d9492490 Michael Hanselmann
    else:
173 d9492490 Michael Hanselmann
      baseheaders = ""
174 d9492490 Michael Hanselmann
175 d9492490 Michael Hanselmann
    headers = mimetools.Message(StringIO(baseheaders), 0)
176 d9492490 Michael Hanselmann
177 d9492490 Michael Hanselmann
    if request_body:
178 d9492490 Michael Hanselmann
      headers[http.HTTP_CONTENT_LENGTH] = str(len(request_body))
179 d9492490 Michael Hanselmann
180 d9492490 Michael Hanselmann
    if self._opts.get(pycurl.HTTPAUTH, 0) & pycurl.HTTPAUTH_BASIC:
181 d9492490 Michael Hanselmann
      try:
182 d9492490 Michael Hanselmann
        userpwd = self._opts[pycurl.USERPWD]
183 d9492490 Michael Hanselmann
      except KeyError:
184 d9492490 Michael Hanselmann
        raise errors.ProgrammerError("Basic authentication requires username"
185 d9492490 Michael Hanselmann
                                     " and password")
186 d9492490 Michael Hanselmann
187 d9492490 Michael Hanselmann
      headers[http.HTTP_AUTHORIZATION] = \
188 d9492490 Michael Hanselmann
        "%s %s" % (http.auth.HTTP_BASIC_AUTH, base64.b64encode(userpwd))
189 d9492490 Michael Hanselmann
190 f90a1ab5 René Nussbaumer
    path = _GetPathFromUri(url)
191 d9492490 Michael Hanselmann
    (code, resp_body) = \
192 d9492490 Michael Hanselmann
      self._handler.FetchResponse(path, method, headers, request_body)
193 f90a1ab5 René Nussbaumer
194 f90a1ab5 René Nussbaumer
    self._info[pycurl.RESPONSE_CODE] = code
195 f90a1ab5 René Nussbaumer
    if resp_body is not None:
196 f90a1ab5 René Nussbaumer
      writefn(resp_body)
197 1afa108c Michael Hanselmann
198 1afa108c Michael Hanselmann
199 1afa108c Michael Hanselmann
class _RapiMock:
200 1afa108c Michael Hanselmann
  """Mocking out the RAPI server parts.
201 1afa108c Michael Hanselmann

202 1afa108c Michael Hanselmann
  """
203 1afa108c Michael Hanselmann
  def __init__(self, user_fn, luxi_client):
204 1afa108c Michael Hanselmann
    """Initialize this class.
205 1afa108c Michael Hanselmann

206 1afa108c Michael Hanselmann
    @type user_fn: callable
207 1afa108c Michael Hanselmann
    @param user_fn: Function to authentication username
208 1afa108c Michael Hanselmann
    @param luxi_client: A LUXI client implementation
209 1afa108c Michael Hanselmann

210 1afa108c Michael Hanselmann
    """
211 1afa108c Michael Hanselmann
    self.handler = \
212 1afa108c Michael Hanselmann
      server.rapi.RemoteApiHandler(user_fn, _client_cls=luxi_client)
213 1afa108c Michael Hanselmann
214 1afa108c Michael Hanselmann
  def FetchResponse(self, path, method, headers, request_body):
215 1afa108c Michael Hanselmann
    """This is a callback method used to fetch a response.
216 1afa108c Michael Hanselmann

217 1afa108c Michael Hanselmann
    This method is called by the FakeCurl.perform method
218 1afa108c Michael Hanselmann

219 1afa108c Michael Hanselmann
    @type path: string
220 1afa108c Michael Hanselmann
    @param path: Requested path
221 1afa108c Michael Hanselmann
    @type method: string
222 1afa108c Michael Hanselmann
    @param method: HTTP method
223 1afa108c Michael Hanselmann
    @type request_body: string
224 1afa108c Michael Hanselmann
    @param request_body: Request body
225 1afa108c Michael Hanselmann
    @type headers: mimetools.Message
226 1afa108c Michael Hanselmann
    @param headers: Request headers
227 1afa108c Michael Hanselmann
    @return: Tuple containing status code and response body
228 1afa108c Michael Hanselmann

229 1afa108c Michael Hanselmann
    """
230 1afa108c Michael Hanselmann
    req_msg = http.HttpMessage()
231 1afa108c Michael Hanselmann
    req_msg.start_line = \
232 1afa108c Michael Hanselmann
      http.HttpClientToServerStartLine(method, path, http.HTTP_1_0)
233 1afa108c Michael Hanselmann
    req_msg.headers = headers
234 1afa108c Michael Hanselmann
    req_msg.body = request_body
235 1afa108c Michael Hanselmann
236 1afa108c Michael Hanselmann
    (_, _, _, resp_msg) = \
237 1afa108c Michael Hanselmann
      http.server.HttpResponder(self.handler)(lambda: (req_msg, None))
238 1afa108c Michael Hanselmann
239 1afa108c Michael Hanselmann
    return (resp_msg.start_line.code, resp_msg.body)
240 1afa108c Michael Hanselmann
241 1afa108c Michael Hanselmann
242 1afa108c Michael Hanselmann
class _TestLuxiTransport:
243 1afa108c Michael Hanselmann
  """Mocked LUXI transport.
244 1afa108c Michael Hanselmann

245 1afa108c Michael Hanselmann
  Raises L{errors.RapiTestResult} for all method calls, no matter the
246 1afa108c Michael Hanselmann
  arguments.
247 1afa108c Michael Hanselmann

248 1afa108c Michael Hanselmann
  """
249 1afa108c Michael Hanselmann
  def __init__(self, record_fn, address, timeouts=None): # pylint: disable=W0613
250 1afa108c Michael Hanselmann
    """Initializes this class.
251 1afa108c Michael Hanselmann

252 1afa108c Michael Hanselmann
    """
253 1afa108c Michael Hanselmann
    self._record_fn = record_fn
254 1afa108c Michael Hanselmann
255 1afa108c Michael Hanselmann
  def Close(self):
256 1afa108c Michael Hanselmann
    pass
257 1afa108c Michael Hanselmann
258 1afa108c Michael Hanselmann
  def Call(self, data):
259 1afa108c Michael Hanselmann
    """Calls LUXI method.
260 1afa108c Michael Hanselmann

261 1afa108c Michael Hanselmann
    In this test class the method is not actually called, but added to a list
262 1afa108c Michael Hanselmann
    of called methods and then an exception (L{errors.RapiTestResult}) is
263 1afa108c Michael Hanselmann
    raised. There is no return value.
264 1afa108c Michael Hanselmann

265 1afa108c Michael Hanselmann
    """
266 1afa108c Michael Hanselmann
    (method, _, _) = luxi.ParseRequest(data)
267 1afa108c Michael Hanselmann
268 1afa108c Michael Hanselmann
    # Take a note of called method
269 1afa108c Michael Hanselmann
    self._record_fn(method)
270 1afa108c Michael Hanselmann
271 1afa108c Michael Hanselmann
    # Everything went fine until here, so let's abort the test
272 1afa108c Michael Hanselmann
    raise errors.RapiTestResult
273 1afa108c Michael Hanselmann
274 1afa108c Michael Hanselmann
275 1afa108c Michael Hanselmann
class _LuxiCallRecorder:
276 1afa108c Michael Hanselmann
  """Records all called LUXI client methods.
277 1afa108c Michael Hanselmann

278 1afa108c Michael Hanselmann
  """
279 1afa108c Michael Hanselmann
  def __init__(self):
280 1afa108c Michael Hanselmann
    """Initializes this class.
281 1afa108c Michael Hanselmann

282 1afa108c Michael Hanselmann
    """
283 1afa108c Michael Hanselmann
    self._called = set()
284 1afa108c Michael Hanselmann
285 1afa108c Michael Hanselmann
  def Record(self, name):
286 1afa108c Michael Hanselmann
    """Records a called function name.
287 1afa108c Michael Hanselmann

288 1afa108c Michael Hanselmann
    """
289 1afa108c Michael Hanselmann
    self._called.add(name)
290 1afa108c Michael Hanselmann
291 1afa108c Michael Hanselmann
  def CalledNames(self):
292 1afa108c Michael Hanselmann
    """Returns a list of called LUXI methods.
293 1afa108c Michael Hanselmann

294 1afa108c Michael Hanselmann
    """
295 1afa108c Michael Hanselmann
    return self._called
296 1afa108c Michael Hanselmann
297 1afa108c Michael Hanselmann
  def __call__(self):
298 1afa108c Michael Hanselmann
    """Creates an instrumented LUXI client.
299 1afa108c Michael Hanselmann

300 1afa108c Michael Hanselmann
    The LUXI client will record all method calls (use L{CalledNames} to
301 1afa108c Michael Hanselmann
    retrieve them).
302 1afa108c Michael Hanselmann

303 1afa108c Michael Hanselmann
    """
304 1afa108c Michael Hanselmann
    return luxi.Client(transport=compat.partial(_TestLuxiTransport,
305 1afa108c Michael Hanselmann
                                                self.Record))
306 1afa108c Michael Hanselmann
307 1afa108c Michael Hanselmann
308 1afa108c Michael Hanselmann
def _TestWrapper(fn, *args, **kwargs):
309 1afa108c Michael Hanselmann
  """Wrapper for ignoring L{errors.RapiTestResult}.
310 1afa108c Michael Hanselmann

311 1afa108c Michael Hanselmann
  """
312 1afa108c Michael Hanselmann
  try:
313 1afa108c Michael Hanselmann
    return fn(*args, **kwargs)
314 1afa108c Michael Hanselmann
  except errors.RapiTestResult:
315 1afa108c Michael Hanselmann
    # Everything was fine up to the point of sending a LUXI request
316 1afa108c Michael Hanselmann
    return NotImplemented
317 1afa108c Michael Hanselmann
318 1afa108c Michael Hanselmann
319 1afa108c Michael Hanselmann
class InputTestClient:
320 1afa108c Michael Hanselmann
  """Test version of RAPI client.
321 1afa108c Michael Hanselmann

322 1afa108c Michael Hanselmann
  Instances of this class can be used to test input arguments for RAPI client
323 1afa108c Michael Hanselmann
  calls. See L{rapi.client.GanetiRapiClient} for available methods and their
324 1afa108c Michael Hanselmann
  arguments. Functions can return C{NotImplemented} if all arguments are
325 1afa108c Michael Hanselmann
  acceptable, but a LUXI request would be necessary to provide an actual return
326 1afa108c Michael Hanselmann
  value. In case of an error, L{VerificationError} is raised.
327 1afa108c Michael Hanselmann

328 1afa108c Michael Hanselmann
  @see: An example on how to use this class can be found in
329 1afa108c Michael Hanselmann
    C{doc/examples/rapi_testutils.py}
330 1afa108c Michael Hanselmann

331 1afa108c Michael Hanselmann
  """
332 1afa108c Michael Hanselmann
  def __init__(self):
333 1afa108c Michael Hanselmann
    """Initializes this class.
334 1afa108c Michael Hanselmann

335 1afa108c Michael Hanselmann
    """
336 1afa108c Michael Hanselmann
    username = utils.GenerateSecret()
337 1afa108c Michael Hanselmann
    password = utils.GenerateSecret()
338 1afa108c Michael Hanselmann
339 1afa108c Michael Hanselmann
    def user_fn(wanted):
340 1afa108c Michael Hanselmann
      """Called to verify user credentials given in HTTP request.
341 1afa108c Michael Hanselmann

342 1afa108c Michael Hanselmann
      """
343 1afa108c Michael Hanselmann
      assert username == wanted
344 1afa108c Michael Hanselmann
      return http.auth.PasswordFileUser(username, password,
345 1afa108c Michael Hanselmann
                                        [rapi.RAPI_ACCESS_WRITE])
346 1afa108c Michael Hanselmann
347 1afa108c Michael Hanselmann
    self._lcr = _LuxiCallRecorder()
348 1afa108c Michael Hanselmann
349 1afa108c Michael Hanselmann
    # Create a mock RAPI server
350 1afa108c Michael Hanselmann
    handler = _RapiMock(user_fn, self._lcr)
351 1afa108c Michael Hanselmann
352 1afa108c Michael Hanselmann
    self._client = \
353 1afa108c Michael Hanselmann
      rapi.client.GanetiRapiClient("master.example.com",
354 1afa108c Michael Hanselmann
                                   username=username, password=password,
355 1afa108c Michael Hanselmann
                                   curl_factory=lambda: FakeCurl(handler))
356 1afa108c Michael Hanselmann
357 1afa108c Michael Hanselmann
  def _GetLuxiCalls(self):
358 1afa108c Michael Hanselmann
    """Returns the names of all called LUXI client functions.
359 1afa108c Michael Hanselmann

360 1afa108c Michael Hanselmann
    """
361 1afa108c Michael Hanselmann
    return self._lcr.CalledNames()
362 1afa108c Michael Hanselmann
363 1afa108c Michael Hanselmann
  def __getattr__(self, name):
364 1afa108c Michael Hanselmann
    """Finds method by name.
365 1afa108c Michael Hanselmann

366 1afa108c Michael Hanselmann
    The method is wrapped using L{_TestWrapper} to produce the actual test
367 1afa108c Michael Hanselmann
    result.
368 1afa108c Michael Hanselmann

369 1afa108c Michael Hanselmann
    """
370 1afa108c Michael Hanselmann
    return _HideInternalErrors(compat.partial(_TestWrapper,
371 1afa108c Michael Hanselmann
                                              getattr(self._client, name)))