4 # Copyright (C) 2011, 2013 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22 """Script for testing ganeti.hypervisor.hv_lxc"""
24 import string # pylint: disable=W0402
31 from ganeti import constants
32 from ganeti import objects
33 from ganeti import hypervisor
34 from ganeti import utils
35 from ganeti import errors
36 from ganeti import compat
38 from ganeti.hypervisor import hv_xen
43 # Map from hypervisor class to hypervisor name
44 HVCLASS_TO_HVNAME = utils.InvertDict(hypervisor._HYPERVISOR_MAP)
47 class TestConsole(unittest.TestCase):
49 for cls in [hv_xen.XenPvmHypervisor, hv_xen.XenHvmHypervisor]:
50 instance = objects.Instance(name="xen.example.com",
51 primary_node="node24828")
52 cons = cls.GetInstanceConsole(instance, {}, {})
53 self.assertTrue(cons.Validate())
54 self.assertEqual(cons.kind, constants.CONS_SSH)
55 self.assertEqual(cons.host, instance.primary_node)
56 self.assertEqual(cons.command[-1], instance.name)
59 class TestCreateConfigCpus(unittest.TestCase):
61 for cpu_mask in [None, ""]:
62 self.assertEqual(hv_xen._CreateConfigCpus(cpu_mask),
66 self.assertEqual(hv_xen._CreateConfigCpus(constants.CPU_PINNING_ALL),
70 self.assertEqual(hv_xen._CreateConfigCpus("9"), "cpu = \"9\"")
72 def testMultiple(self):
73 self.assertEqual(hv_xen._CreateConfigCpus("0-2,4,5-5:3:all"),
74 ("cpus = [ \"0,1,2,4,5\", \"3\", \"%s\" ]" %
75 constants.CPU_PINNING_ALL_XEN))
78 class TestParseXmList(testutils.GanetiTestCase):
80 data = testutils.ReadTestData("xen-xm-list-4.0.1-dom0-only.txt")
83 self.assertEqual(hv_xen._ParseXmList(data.splitlines(), False), [])
86 result = hv_xen._ParseXmList(data.splitlines(), True)
87 self.assertEqual(len(result), 1)
88 self.assertEqual(len(result[0]), 6)
91 self.assertEqual(result[0][0], hv_xen._DOM0_NAME)
94 self.assertEqual(result[0][1], 0)
97 self.assertEqual(result[0][2], 1023)
100 self.assertEqual(result[0][3], 1)
103 self.assertEqual(result[0][4], "r-----")
106 self.assertAlmostEqual(result[0][5], 121152.6)
108 def testWrongLineFormat(self):
110 ["three fields only"],
111 ["name InvalidID 128 1 r----- 12345"],
116 hv_xen._ParseXmList(["Header would be here"] + lines, False)
117 except errors.HypervisorError, err:
118 self.assertTrue("Can't parse output of xm list" in str(err))
120 self.fail("Exception was not raised")
123 class TestGetXmList(testutils.GanetiTestCase):
125 return utils.RunResult(constants.EXIT_FAILURE, None,
126 "stdout", "stderr", None,
127 NotImplemented, NotImplemented)
129 def testTimeout(self):
130 fn = testutils.CallCounter(self._Fail)
132 hv_xen._GetXmList(fn, False, _timeout=0.1)
133 except errors.HypervisorError, err:
134 self.assertTrue("timeout exceeded" in str(err))
136 self.fail("Exception was not raised")
138 self.assertTrue(fn.Count() < 10,
139 msg="'xm list' was called too many times")
141 def _Success(self, stdout):
142 return utils.RunResult(constants.EXIT_SUCCESS, None, stdout, "", None,
143 NotImplemented, NotImplemented)
145 def testSuccess(self):
146 data = testutils.ReadTestData("xen-xm-list-4.0.1-four-instances.txt")
148 fn = testutils.CallCounter(compat.partial(self._Success, data))
150 result = hv_xen._GetXmList(fn, True, _timeout=0.1)
152 self.assertEqual(len(result), 4)
154 self.assertEqual(map(compat.fst, result), [
156 "server01.example.com",
157 "web3106215069.example.com",
158 "testinstance.example.com",
161 self.assertEqual(fn.Count(), 1)
164 class TestParseNodeInfo(testutils.GanetiTestCase):
166 self.assertEqual(hv_xen._ParseNodeInfo(""), {})
168 def testUnknownInput(self):
171 "something else goes",
174 self.assertEqual(hv_xen._ParseNodeInfo(data), {})
176 def testBasicInfo(self):
177 data = testutils.ReadTestData("xen-xm-info-4.0.1.txt")
178 result = hv_xen._ParseNodeInfo(data)
179 self.assertEqual(result, {
183 "hv_version": (4, 0),
185 "memory_total": 16378,
189 class TestMergeInstanceInfo(testutils.GanetiTestCase):
191 self.assertEqual(hv_xen._MergeInstanceInfo({}, lambda _: []), {})
193 def _FakeXmList(self, include_node):
194 self.assertTrue(include_node)
196 (hv_xen._DOM0_NAME, NotImplemented, 4096, 7, NotImplemented,
198 ("inst1.example.com", NotImplemented, 2048, 4, NotImplemented,
202 def testMissingNodeInfo(self):
203 result = hv_xen._MergeInstanceInfo({}, self._FakeXmList)
204 self.assertEqual(result, {
209 def testWithNodeInfo(self):
210 info = testutils.ReadTestData("xen-xm-info-4.0.1.txt")
211 result = hv_xen._GetNodeInfo(info, self._FakeXmList)
212 self.assertEqual(result, {
217 "hv_version": (4, 0),
221 "memory_total": 16378,
225 class TestGetConfigFileDiskData(unittest.TestCase):
226 def testLetterCount(self):
227 self.assertEqual(len(hv_xen._DISK_LETTERS), 26)
229 def testNoDisks(self):
230 self.assertEqual(hv_xen._GetConfigFileDiskData([], "hd"), [])
232 def testManyDisks(self):
233 for offset in [0, 1, 10]:
234 disks = [(objects.Disk(dev_type=constants.LD_LV), "/tmp/disk/%s" % idx)
235 for idx in range(len(hv_xen._DISK_LETTERS) + offset)]
238 result = hv_xen._GetConfigFileDiskData(disks, "hd")
239 self.assertEqual(result, [
240 "'phy:/tmp/disk/%s,hd%s,r'" % (idx, string.ascii_lowercase[idx])
241 for idx in range(len(hv_xen._DISK_LETTERS) + offset)
245 hv_xen._GetConfigFileDiskData(disks, "hd")
246 except errors.HypervisorError, err:
247 self.assertEqual(str(err), "Too many disks")
249 self.fail("Exception was not raised")
251 def testTwoLvDisksWithMode(self):
253 (objects.Disk(dev_type=constants.LD_LV, mode=constants.DISK_RDWR),
255 (objects.Disk(dev_type=constants.LD_LV, mode=constants.DISK_RDONLY),
259 result = hv_xen._GetConfigFileDiskData(disks, "hd")
260 self.assertEqual(result, [
261 "'phy:/tmp/diskFirst,hda,w'",
262 "'phy:/tmp/diskLast,hdb,r'",
265 def testFileDisks(self):
267 (objects.Disk(dev_type=constants.LD_FILE, mode=constants.DISK_RDWR,
268 physical_id=[constants.FD_LOOP]),
270 (objects.Disk(dev_type=constants.LD_FILE, mode=constants.DISK_RDONLY,
271 physical_id=[constants.FD_BLKTAP]),
273 (objects.Disk(dev_type=constants.LD_FILE, mode=constants.DISK_RDWR,
274 physical_id=[constants.FD_LOOP]),
276 (objects.Disk(dev_type=constants.LD_FILE, mode=constants.DISK_RDWR,
277 physical_id=[constants.FD_BLKTAP]),
281 result = hv_xen._GetConfigFileDiskData(disks, "sd")
282 self.assertEqual(result, [
283 "'file:/tmp/diskFirst,sda,w'",
284 "'tap:aio:/tmp/diskTwo,sdb,r'",
285 "'file:/tmp/diskThree,sdc,w'",
286 "'tap:aio:/tmp/diskLast,sdd,w'",
289 def testInvalidFileDisk(self):
291 (objects.Disk(dev_type=constants.LD_FILE, mode=constants.DISK_RDWR,
292 physical_id=["#unknown#"]),
296 self.assertRaises(KeyError, hv_xen._GetConfigFileDiskData, disks, "sd")
299 class TestXenHypervisorUnknownCommand(unittest.TestCase):
301 cmd = "#unknown command#"
302 self.assertFalse(cmd in constants.KNOWN_XEN_COMMANDS)
303 hv = hv_xen.XenHypervisor(_cfgdir=NotImplemented,
304 _run_cmd_fn=NotImplemented,
306 self.assertRaises(errors.ProgrammerError, hv._RunXen, [])
309 class TestXenHypervisorWriteConfigFile(unittest.TestCase):
311 self.tmpdir = tempfile.mkdtemp()
314 shutil.rmtree(self.tmpdir)
316 def testWriteError(self):
317 cfgdir = utils.PathJoin(self.tmpdir, "foobar")
319 hv = hv_xen.XenHypervisor(_cfgdir=cfgdir,
320 _run_cmd_fn=NotImplemented,
323 self.assertFalse(os.path.exists(cfgdir))
326 hv._WriteConfigFile("name", "data")
327 except errors.HypervisorError, err:
328 self.assertTrue(str(err).startswith("Cannot write Xen instance"))
330 self.fail("Exception was not raised")
333 class _TestXenHypervisor(object):
334 TARGET = NotImplemented
336 HVNAME = NotImplemented
339 super(_TestXenHypervisor, self).setUp()
341 self.tmpdir = tempfile.mkdtemp()
343 self.vncpw = "".join(random.sample(string.ascii_letters, 10))
345 self.vncpw_path = utils.PathJoin(self.tmpdir, "vncpw")
346 utils.WriteFile(self.vncpw_path, data=self.vncpw)
349 super(_TestXenHypervisor, self).tearDown()
351 shutil.rmtree(self.tmpdir)
353 def _GetHv(self, run_cmd=NotImplemented):
354 return self.TARGET(_cfgdir=self.tmpdir, _run_cmd_fn=run_cmd, _cmd=self.CMD)
356 def _SuccessCommand(self, stdout, cmd):
357 self.assertEqual(cmd[0], self.CMD)
359 return utils.RunResult(constants.EXIT_SUCCESS, None, stdout, "", None,
360 NotImplemented, NotImplemented)
362 def _FailingCommand(self, cmd):
363 self.assertEqual(cmd[0], self.CMD)
365 return utils.RunResult(constants.EXIT_FAILURE, None,
366 "", "This command failed", None,
367 NotImplemented, NotImplemented)
369 def _FakeTcpPing(self, expected, result, target, port, **kwargs):
370 self.assertEqual((target, port), expected)
373 def testReadingNonExistentConfigFile(self):
377 hv._ReadConfigFile("inst15780.example.com")
378 except errors.HypervisorError, err:
379 self.assertTrue(str(err).startswith("Failed to load Xen config file:"))
381 self.fail("Exception was not raised")
383 def testRemovingAutoConfigFile(self):
384 name = "inst8206.example.com"
385 cfgfile = utils.PathJoin(self.tmpdir, name)
386 autodir = utils.PathJoin(self.tmpdir, "auto")
387 autocfgfile = utils.PathJoin(autodir, name)
391 utils.WriteFile(autocfgfile, data="")
395 self.assertTrue(os.path.isfile(autocfgfile))
396 hv._WriteConfigFile(name, "content")
397 self.assertFalse(os.path.exists(autocfgfile))
398 self.assertEqual(utils.ReadFile(cfgfile), "content")
400 def _XenList(self, cmd):
401 self.assertEqual(cmd, [self.CMD, "list"])
403 # TODO: Use actual data from "xl" command
404 output = testutils.ReadTestData("xen-xm-list-4.0.1-four-instances.txt")
406 return self._SuccessCommand(output, cmd)
408 def testGetInstanceInfo(self):
409 hv = self._GetHv(run_cmd=self._XenList)
411 (name, instid, memory, vcpus, state, runtime) = \
412 hv.GetInstanceInfo("server01.example.com")
414 self.assertEqual(name, "server01.example.com")
415 self.assertEqual(instid, 1)
416 self.assertEqual(memory, 1024)
417 self.assertEqual(vcpus, 1)
418 self.assertEqual(state, "-b----")
419 self.assertAlmostEqual(runtime, 167643.2)
421 def testGetInstanceInfoDom0(self):
422 hv = self._GetHv(run_cmd=self._XenList)
424 # TODO: Not sure if this is actually used anywhere (can't find it), but the
425 # code supports querying for Dom0
426 (name, instid, memory, vcpus, state, runtime) = \
427 hv.GetInstanceInfo(hv_xen._DOM0_NAME)
429 self.assertEqual(name, "Domain-0")
430 self.assertEqual(instid, 0)
431 self.assertEqual(memory, 1023)
432 self.assertEqual(vcpus, 1)
433 self.assertEqual(state, "r-----")
434 self.assertAlmostEqual(runtime, 154706.1)
436 def testGetInstanceInfoUnknown(self):
437 hv = self._GetHv(run_cmd=self._XenList)
439 result = hv.GetInstanceInfo("unknown.example.com")
440 self.assertTrue(result is None)
442 def testGetAllInstancesInfo(self):
443 hv = self._GetHv(run_cmd=self._XenList)
445 result = hv.GetAllInstancesInfo()
447 self.assertEqual(map(compat.fst, result), [
448 "server01.example.com",
449 "web3106215069.example.com",
450 "testinstance.example.com",
453 def testListInstances(self):
454 hv = self._GetHv(run_cmd=self._XenList)
456 self.assertEqual(hv.ListInstances(), [
457 "server01.example.com",
458 "web3106215069.example.com",
459 "testinstance.example.com",
462 def testVerify(self):
463 output = testutils.ReadTestData("xen-xm-info-4.0.1.txt")
464 hv = self._GetHv(run_cmd=compat.partial(self._SuccessCommand,
466 self.assertTrue(hv.Verify() is None)
468 def testVerifyFailing(self):
469 hv = self._GetHv(run_cmd=self._FailingCommand)
470 self.assertTrue("failed:" in hv.Verify())
472 def _StartInstanceCommand(self, inst, paused, failcreate, cmd):
473 if cmd == [self.CMD, "info"]:
474 output = testutils.ReadTestData("xen-xm-info-4.0.1.txt")
475 elif cmd == [self.CMD, "list"]:
476 output = testutils.ReadTestData("xen-xm-list-4.0.1-dom0-only.txt")
477 elif cmd[:2] == [self.CMD, "create"]:
479 cfgfile = utils.PathJoin(self.tmpdir, inst.name)
482 self.assertEqual(args, ["-p", cfgfile])
484 self.assertEqual(args, [cfgfile])
487 return self._FailingCommand(cmd)
491 self.fail("Unhandled command: %s" % (cmd, ))
493 return self._SuccessCommand(output, cmd)
494 #return self._FailingCommand(cmd)
496 def _MakeInstance(self):
497 # Copy default parameters
498 bep = objects.FillDict(constants.BEC_DEFAULTS, {})
499 hvp = objects.FillDict(constants.HVC_DEFAULTS[self.HVNAME], {})
501 # Override default VNC password file path
502 if constants.HV_VNC_PASSWORD_FILE in hvp:
503 hvp[constants.HV_VNC_PASSWORD_FILE] = self.vncpw_path
506 (objects.Disk(dev_type=constants.LD_LV, mode=constants.DISK_RDWR),
507 utils.PathJoin(self.tmpdir, "disk0")),
508 (objects.Disk(dev_type=constants.LD_LV, mode=constants.DISK_RDONLY),
509 utils.PathJoin(self.tmpdir, "disk1")),
512 inst = objects.Instance(name="server01.example.com",
513 hvparams=hvp, beparams=bep,
514 osparams={}, nics=[], os="deb1",
515 disks=map(compat.fst, disks))
520 def testStartInstance(self):
521 (inst, disks) = self._MakeInstance()
523 for failcreate in [False, True]:
524 for paused in [False, True]:
525 run_cmd = compat.partial(self._StartInstanceCommand,
526 inst, paused, failcreate)
528 hv = self._GetHv(run_cmd=run_cmd)
530 # Ensure instance is not listed
531 self.assertTrue(inst.name not in hv.ListInstances())
533 # Remove configuration
534 cfgfile = utils.PathJoin(self.tmpdir, inst.name)
535 utils.RemoveFile(cfgfile)
538 self.assertRaises(errors.HypervisorError, hv.StartInstance,
541 hv.StartInstance(inst, disks, paused)
543 # Check if configuration was updated
544 lines = utils.ReadFile(cfgfile).splitlines()
546 if constants.HV_VNC_PASSWORD_FILE in inst.hvparams:
547 self.assertTrue(("vncpasswd = '%s'" % self.vncpw) in lines)
549 extra = inst.hvparams[constants.HV_KERNEL_ARGS]
550 self.assertTrue(("extra = '%s'" % extra) in lines)
552 def _StopInstanceCommand(self, instance_name, force, fail, cmd):
553 if ((force and cmd[:2] == [self.CMD, "destroy"]) or
554 (not force and cmd[:2] == [self.CMD, "shutdown"])):
555 self.assertEqual(cmd[2:], [instance_name])
558 self.fail("Unhandled command: %s" % (cmd, ))
561 # Simulate a failing command
562 return self._FailingCommand(cmd)
564 return self._SuccessCommand(output, cmd)
566 def testStopInstance(self):
567 name = "inst4284.example.com"
568 cfgfile = utils.PathJoin(self.tmpdir, name)
569 cfgdata = "config file content\n"
571 for force in [False, True]:
572 for fail in [False, True]:
573 utils.WriteFile(cfgfile, data=cfgdata)
575 run_cmd = compat.partial(self._StopInstanceCommand, name, force, fail)
577 hv = self._GetHv(run_cmd=run_cmd)
579 self.assertTrue(os.path.isfile(cfgfile))
583 hv._StopInstance(name, force)
584 except errors.HypervisorError, err:
585 self.assertTrue(str(err).startswith("Failed to stop instance"))
587 self.fail("Exception was not raised")
588 self.assertEqual(utils.ReadFile(cfgfile), cfgdata,
589 msg=("Configuration was removed when stopping"
592 hv._StopInstance(name, force)
593 self.assertFalse(os.path.exists(cfgfile))
595 def _MigrateNonRunningInstCmd(self, cmd):
596 if cmd == [self.CMD, "list"]:
597 output = testutils.ReadTestData("xen-xm-list-4.0.1-dom0-only.txt")
599 self.fail("Unhandled command: %s" % (cmd, ))
601 return self._SuccessCommand(output, cmd)
603 def testMigrateInstanceNotRunning(self):
604 name = "nonexistinginstance.example.com"
605 target = constants.IP4_ADDRESS_LOCALHOST
608 hv = self._GetHv(run_cmd=self._MigrateNonRunningInstCmd)
610 for live in [False, True]:
612 hv._MigrateInstance(NotImplemented, name, target, port, live,
613 _ping_fn=NotImplemented)
614 except errors.HypervisorError, err:
615 self.assertEqual(str(err), "Instance not running, cannot migrate")
617 self.fail("Exception was not raised")
619 def _MigrateInstTargetUnreachCmd(self, cmd):
620 if cmd == [self.CMD, "list"]:
621 output = testutils.ReadTestData("xen-xm-list-4.0.1-four-instances.txt")
623 self.fail("Unhandled command: %s" % (cmd, ))
625 return self._SuccessCommand(output, cmd)
627 def testMigrateTargetUnreachable(self):
628 name = "server01.example.com"
629 target = constants.IP4_ADDRESS_LOCALHOST
632 hv = self._GetHv(run_cmd=self._MigrateInstTargetUnreachCmd)
634 for live in [False, True]:
635 if self.CMD == constants.XEN_CMD_XL:
636 # TODO: Detect unreachable targets
640 hv._MigrateInstance(NotImplemented, name, target, port, live,
641 _ping_fn=compat.partial(self._FakeTcpPing,
642 (target, port), False))
643 except errors.HypervisorError, err:
644 wanted = "Remote host %s not" % target
645 self.assertTrue(str(err).startswith(wanted))
647 self.fail("Exception was not raised")
649 def _MigrateInstanceCmd(self, cluster_name, instance_name, target, port,
651 if cmd == [self.CMD, "list"]:
652 output = testutils.ReadTestData("xen-xm-list-4.0.1-four-instances.txt")
653 elif cmd[:2] == [self.CMD, "migrate"]:
654 if self.CMD == constants.XEN_CMD_XM:
655 args = ["-p", str(port)]
660 elif self.CMD == constants.XEN_CMD_XL:
662 "-s", constants.XL_SSH_CMD % cluster_name,
663 "-C", utils.PathJoin(self.tmpdir, instance_name),
667 self.fail("Unknown Xen command '%s'" % self.CMD)
669 args.extend([instance_name, target])
670 self.assertEqual(cmd[2:], args)
673 return self._FailingCommand(cmd)
677 self.fail("Unhandled command: %s" % (cmd, ))
679 return self._SuccessCommand(output, cmd)
681 def testMigrateInstance(self):
682 clustername = "cluster.example.com"
683 instname = "server01.example.com"
684 target = constants.IP4_ADDRESS_LOCALHOST
687 for live in [False, True]:
688 for fail in [False, True]:
690 testutils.CallCounter(compat.partial(self._FakeTcpPing,
691 (target, port), True))
694 compat.partial(self._MigrateInstanceCmd,
695 clustername, instname, target, port, live,
698 hv = self._GetHv(run_cmd=run_cmd)
702 hv._MigrateInstance(clustername, instname, target, port, live,
704 except errors.HypervisorError, err:
705 self.assertTrue(str(err).startswith("Failed to migrate instance"))
707 self.fail("Exception was not raised")
709 hv._MigrateInstance(clustername, instname, target, port, live,
712 if self.CMD == constants.XEN_CMD_XM:
717 self.assertEqual(ping_fn.Count(), expected_pings)
719 def _GetNodeInfoCmd(self, fail, cmd):
720 if cmd == [self.CMD, "info"]:
722 return self._FailingCommand(cmd)
724 output = testutils.ReadTestData("xen-xm-info-4.0.1.txt")
725 elif cmd == [self.CMD, "list"]:
727 self.fail("'xm list' shouldn't be called when 'xm info' failed")
729 output = testutils.ReadTestData("xen-xm-list-4.0.1-four-instances.txt")
731 self.fail("Unhandled command: %s" % (cmd, ))
733 return self._SuccessCommand(output, cmd)
735 def testGetNodeInfo(self):
736 run_cmd = compat.partial(self._GetNodeInfoCmd, False)
737 hv = self._GetHv(run_cmd=run_cmd)
738 result = hv.GetNodeInfo()
740 self.assertEqual(result["hv_version"], (4, 0))
741 self.assertEqual(result["memory_free"], 8004)
743 def testGetNodeInfoFailing(self):
744 run_cmd = compat.partial(self._GetNodeInfoCmd, True)
745 hv = self._GetHv(run_cmd=run_cmd)
746 self.assertTrue(hv.GetNodeInfo() is None)
749 def _MakeTestClass(cls, cmd):
750 """Makes a class for testing.
752 The returned class has structure as shown in the following pseudo code:
754 class Test{cls.__name__}{cmd}(_TestXenHypervisor, unittest.TestCase):
757 HVNAME = {Hypervisor name retrieved using class}
760 @param cls: Hypervisor class to be tested
762 @param cmd: Hypervisor command
764 @return: Class name and class object (not instance)
767 name = "Test%sCmd%s" % (cls.__name__, cmd.title())
768 bases = (_TestXenHypervisor, unittest.TestCase)
769 hvname = HVCLASS_TO_HVNAME[cls]
771 return (name, type(name, bases, dict(TARGET=cls, CMD=cmd, HVNAME=hvname)))
774 # Create test classes programmatically instead of manually to reduce the risk
775 # of forgetting some combinations
776 for cls in [hv_xen.XenPvmHypervisor, hv_xen.XenHvmHypervisor]:
777 for cmd in constants.KNOWN_XEN_COMMANDS:
778 (name, testcls) = _MakeTestClass(cls, cmd)
780 assert name not in locals()
782 locals()[name] = testcls
785 if __name__ == "__main__":
786 testutils.GanetiTestProgram()