Revision 1afa108c

b/lib/rapi/testutils.py
33 33
from ganeti import errors
34 34
from ganeti import opcodes
35 35
from ganeti import http
36
from ganeti import server
37
from ganeti import utils
38
from ganeti import compat
39
from ganeti import luxi
40
from ganeti import rapi
41

  
42
import ganeti.http.server # pylint: disable=W0611
43
import ganeti.server.rapi
44
import ganeti.rapi.client
36 45

  
37 46

  
38 47
_URI_RE = re.compile(r"https://(?P<host>.*):(?P<port>\d+)(?P<path>/.*)")
......
65 74
  def wrapper(*args, **kwargs):
66 75
    try:
67 76
      return fn(*args, **kwargs)
68
    except errors.GenericError, err:
77
    except (errors.GenericError, rapi.client.GanetiApiError), err:
69 78
      raise VerificationError("Unhandled Ganeti error: %s" % err)
70 79

  
71 80
  return wrapper
......
185 194
    self._info[pycurl.RESPONSE_CODE] = code
186 195
    if resp_body is not None:
187 196
      writefn(resp_body)
197

  
198

  
199
class _RapiMock:
200
  """Mocking out the RAPI server parts.
201

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

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

  
210
    """
211
    self.handler = \
212
      server.rapi.RemoteApiHandler(user_fn, _client_cls=luxi_client)
213

  
214
  def FetchResponse(self, path, method, headers, request_body):
215
    """This is a callback method used to fetch a response.
216

  
217
    This method is called by the FakeCurl.perform method
218

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

  
229
    """
230
    req_msg = http.HttpMessage()
231
    req_msg.start_line = \
232
      http.HttpClientToServerStartLine(method, path, http.HTTP_1_0)
233
    req_msg.headers = headers
234
    req_msg.body = request_body
235

  
236
    (_, _, _, resp_msg) = \
237
      http.server.HttpResponder(self.handler)(lambda: (req_msg, None))
238

  
239
    return (resp_msg.start_line.code, resp_msg.body)
240

  
241

  
242
class _TestLuxiTransport:
243
  """Mocked LUXI transport.
244

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

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

  
252
    """
253
    self._record_fn = record_fn
254

  
255
  def Close(self):
256
    pass
257

  
258
  def Call(self, data):
259
    """Calls LUXI method.
260

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

  
265
    """
266
    (method, _, _) = luxi.ParseRequest(data)
267

  
268
    # Take a note of called method
269
    self._record_fn(method)
270

  
271
    # Everything went fine until here, so let's abort the test
272
    raise errors.RapiTestResult
273

  
274

  
275
class _LuxiCallRecorder:
276
  """Records all called LUXI client methods.
277

  
278
  """
279
  def __init__(self):
280
    """Initializes this class.
281

  
282
    """
283
    self._called = set()
284

  
285
  def Record(self, name):
286
    """Records a called function name.
287

  
288
    """
289
    self._called.add(name)
290

  
291
  def CalledNames(self):
292
    """Returns a list of called LUXI methods.
293

  
294
    """
295
    return self._called
296

  
297
  def __call__(self):
298
    """Creates an instrumented LUXI client.
299

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

  
303
    """
304
    return luxi.Client(transport=compat.partial(_TestLuxiTransport,
305
                                                self.Record))
306

  
307

  
308
def _TestWrapper(fn, *args, **kwargs):
309
  """Wrapper for ignoring L{errors.RapiTestResult}.
310

  
311
  """
312
  try:
313
    return fn(*args, **kwargs)
314
  except errors.RapiTestResult:
315
    # Everything was fine up to the point of sending a LUXI request
316
    return NotImplemented
317

  
318

  
319
class InputTestClient:
320
  """Test version of RAPI client.
321

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

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

  
331
  """
332
  def __init__(self):
333
    """Initializes this class.
334

  
335
    """
336
    username = utils.GenerateSecret()
337
    password = utils.GenerateSecret()
338

  
339
    def user_fn(wanted):
340
      """Called to verify user credentials given in HTTP request.
341

  
342
      """
343
      assert username == wanted
344
      return http.auth.PasswordFileUser(username, password,
345
                                        [rapi.RAPI_ACCESS_WRITE])
346

  
347
    self._lcr = _LuxiCallRecorder()
348

  
349
    # Create a mock RAPI server
350
    handler = _RapiMock(user_fn, self._lcr)
351

  
352
    self._client = \
353
      rapi.client.GanetiRapiClient("master.example.com",
354
                                   username=username, password=password,
355
                                   curl_factory=lambda: FakeCurl(handler))
356

  
357
  def _GetLuxiCalls(self):
358
    """Returns the names of all called LUXI client functions.
359

  
360
    """
361
    return self._lcr.CalledNames()
362

  
363
  def __getattr__(self, name):
364
    """Finds method by name.
365

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

  
369
    """
370
    return _HideInternalErrors(compat.partial(_TestWrapper,
371
                                              getattr(self._client, name)))
b/test/ganeti.rapi.testutils_unittest.py
27 27
from ganeti import constants
28 28
from ganeti import errors
29 29
from ganeti import opcodes
30
from ganeti import luxi
30 31
from ganeti import rapi
32
from ganeti import utils
31 33

  
32 34
import ganeti.rapi.testutils
35
import ganeti.rapi.client
33 36

  
34 37
import testutils
35 38

  
36 39

  
40
KNOWN_UNUSED_LUXI = frozenset([
41
  luxi.REQ_SUBMIT_MANY_JOBS,
42
  luxi.REQ_ARCHIVE_JOB,
43
  luxi.REQ_AUTOARCHIVE_JOBS,
44
  luxi.REQ_QUERY_EXPORTS,
45
  luxi.REQ_QUERY_CONFIG_VALUES,
46
  luxi.REQ_QUERY_TAGS,
47
  luxi.REQ_QUEUE_SET_DRAIN_FLAG,
48
  luxi.REQ_SET_WATCHER_PAUSE,
49
  ])
50

  
51

  
52
# Global variable for storing used LUXI calls
53
_used_luxi_calls = None
54

  
55

  
37 56
class TestHideInternalErrors(unittest.TestCase):
38 57
  def test(self):
39 58
    def inner():
......
96 115
    vor(opcodes.OpTestDummy.OP_ID, None)
97 116

  
98 117

  
118
class TestInputTestClient(unittest.TestCase):
119
  def setUp(self):
120
    self.cl = rapi.testutils.InputTestClient()
121

  
122
  def tearDown(self):
123
    _used_luxi_calls.update(self.cl._GetLuxiCalls())
124

  
125
  def testGetInfo(self):
126
    self.assertTrue(self.cl.GetInfo() is NotImplemented)
127

  
128
  def testPrepareExport(self):
129
    result = self.cl.PrepareExport("inst1.example.com",
130
                                   constants.EXPORT_MODE_LOCAL)
131
    self.assertTrue(result is NotImplemented)
132
    self.assertRaises(rapi.client.GanetiApiError, self.cl.PrepareExport,
133
                      "inst1.example.com", "###invalid###")
134

  
135
  def testGetJobs(self):
136
    self.assertTrue(self.cl.GetJobs() is NotImplemented)
137

  
138
  def testQuery(self):
139
    result = self.cl.Query(constants.QR_NODE, ["name"])
140
    self.assertTrue(result is NotImplemented)
141

  
142
  def testQueryFields(self):
143
    result = self.cl.QueryFields(constants.QR_INSTANCE)
144
    self.assertTrue(result is NotImplemented)
145

  
146
  def testCancelJob(self):
147
    self.assertTrue(self.cl.CancelJob("1") is NotImplemented)
148

  
149
  def testGetNodes(self):
150
    self.assertTrue(self.cl.GetNodes() is NotImplemented)
151

  
152
  def testGetInstances(self):
153
    self.assertTrue(self.cl.GetInstances() is NotImplemented)
154

  
155
  def testGetGroups(self):
156
    self.assertTrue(self.cl.GetGroups() is NotImplemented)
157

  
158
  def testWaitForJobChange(self):
159
    result = self.cl.WaitForJobChange("1", ["id"], None, None)
160
    self.assertTrue(result is NotImplemented)
161

  
162

  
163
class CustomTestRunner(unittest.TextTestRunner):
164
  def run(self, *args):
165
    global _used_luxi_calls
166
    assert _used_luxi_calls is None
167

  
168
    diff = (KNOWN_UNUSED_LUXI - luxi.REQ_ALL)
169
    assert not diff, "Non-existing LUXI calls listed as unused: %s" % diff
170

  
171
    _used_luxi_calls = set()
172
    try:
173
      # Run actual tests
174
      result = unittest.TextTestRunner.run(self, *args)
175

  
176
      diff = _used_luxi_calls & KNOWN_UNUSED_LUXI
177
      if diff:
178
        raise AssertionError("LUXI methods marked as unused were called: %s" %
179
                             utils.CommaJoin(diff))
180

  
181
      diff = (luxi.REQ_ALL - KNOWN_UNUSED_LUXI - _used_luxi_calls)
182
      if diff:
183
        raise AssertionError("The following LUXI methods were not used: %s" %
184
                             utils.CommaJoin(diff))
185
    finally:
186
      # Reset global variable
187
      _used_luxi_calls = None
188

  
189
    return result
190

  
191

  
99 192
if __name__ == "__main__":
100
  testutils.GanetiTestProgram()
193
  testutils.GanetiTestProgram(testRunner=CustomTestRunner)

Also available in: Unified diff