Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / testutils.py @ 0e632cbd

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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