2019-04-12 18:17:28 +00:00
|
|
|
#!/usr/bin/python3
|
|
|
|
|
2019-05-23 23:16:37 +00:00
|
|
|
import argparse
|
2019-04-12 18:17:28 +00:00
|
|
|
import os
|
|
|
|
import subprocess
|
|
|
|
import sys
|
2019-06-25 08:22:32 +00:00
|
|
|
import traceback
|
2019-04-12 18:17:28 +00:00
|
|
|
import unittest
|
|
|
|
|
|
|
|
# import Cockpit's machinery for test VMs and its browser test API
|
|
|
|
sys.path.append(os.path.join(os.path.dirname(__file__), "../bots/machine"))
|
|
|
|
import testvm # pylint: disable=import-error
|
|
|
|
|
2019-10-01 01:11:46 +00:00
|
|
|
#pylint: disable=subprocess-run-check
|
2019-04-12 18:17:28 +00:00
|
|
|
|
2019-05-24 07:47:01 +00:00
|
|
|
def print_exception(etype, value, tb):
|
|
|
|
# only include relevant lines
|
|
|
|
limit = 0
|
|
|
|
while tb and '__unittest' in tb.tb_frame.f_globals:
|
|
|
|
limit += 1
|
|
|
|
tb = tb.tb_next
|
|
|
|
|
|
|
|
traceback.print_exception(etype, value, tb, limit=limit)
|
|
|
|
|
|
|
|
|
2019-04-12 18:17:28 +00:00
|
|
|
class ComposerTestCase(unittest.TestCase):
|
|
|
|
image = testvm.DEFAULT_IMAGE
|
2019-05-24 07:47:01 +00:00
|
|
|
sit = False
|
2019-04-12 18:17:28 +00:00
|
|
|
|
|
|
|
def setUp(self):
|
2019-05-30 00:14:26 +00:00
|
|
|
self.network = testvm.VirtNetwork(0)
|
|
|
|
self.machine = testvm.VirtMachine(self.image, networking=self.network.host(), memory_mb=2048)
|
2019-04-12 18:17:28 +00:00
|
|
|
|
2019-06-25 08:22:32 +00:00
|
|
|
print("Starting virtual machine '{}'".format(self.image))
|
2019-04-12 18:17:28 +00:00
|
|
|
self.machine.start()
|
|
|
|
self.machine.wait_boot()
|
|
|
|
|
|
|
|
# run a command to force starting the SSH master
|
|
|
|
self.machine.execute("uptime")
|
|
|
|
|
|
|
|
self.ssh_command = ["ssh", "-o", "ControlPath=" + self.machine.ssh_master,
|
|
|
|
"-p", self.machine.ssh_port,
|
|
|
|
self.machine.ssh_user + "@" + self.machine.ssh_address]
|
|
|
|
|
|
|
|
print("Machine is up. Connect to it via:")
|
|
|
|
print(" ".join(self.ssh_command))
|
|
|
|
print()
|
|
|
|
|
|
|
|
print("Waiting for lorax-composer to become ready...")
|
|
|
|
curl_command = ["curl", "--max-time", "360",
|
|
|
|
"--silent",
|
|
|
|
"--unix-socket", "/run/weldr/api.socket",
|
|
|
|
"http://localhost/api/status"]
|
|
|
|
r = subprocess.run(self.ssh_command + curl_command, stdout=subprocess.DEVNULL)
|
|
|
|
self.assertEqual(r.returncode, 0)
|
|
|
|
|
|
|
|
def tearDown(self):
|
2019-05-24 07:47:01 +00:00
|
|
|
# Peek into internal data structure, because there's no way to get the
|
|
|
|
# TestResult at this point. `errors` is a list of tuples (method, error)
|
2019-06-02 17:52:59 +00:00
|
|
|
errors = list(e[1] for e in self._outcome.errors if e[1])
|
|
|
|
|
2019-05-24 07:47:01 +00:00
|
|
|
if errors and self.sit:
|
|
|
|
for e in errors:
|
|
|
|
print_exception(*e)
|
|
|
|
|
|
|
|
print()
|
|
|
|
print(" ".join(self.ssh_command))
|
|
|
|
input("Press RETURN to continue...")
|
|
|
|
|
2019-04-12 18:17:28 +00:00
|
|
|
self.machine.stop()
|
|
|
|
|
|
|
|
def execute(self, command, **args):
|
|
|
|
"""Execute a command on the test machine.
|
|
|
|
|
|
|
|
**args and return value are the same as those for subprocess.run().
|
|
|
|
"""
|
|
|
|
return subprocess.run(self.ssh_command + command, **args)
|
|
|
|
|
|
|
|
def runCliTest(self, script):
|
2019-06-02 13:08:03 +00:00
|
|
|
extra_env = []
|
|
|
|
if self.sit:
|
|
|
|
extra_env.append("COMPOSER_TEST_FAIL_FAST=1")
|
|
|
|
|
2019-05-28 11:17:25 +00:00
|
|
|
r = self.execute(["CLI=/usr/bin/composer-cli",
|
|
|
|
"TEST=" + self.id(),
|
|
|
|
"PACKAGE=composer-cli",
|
2019-06-02 13:08:03 +00:00
|
|
|
*extra_env,
|
2019-05-28 11:17:25 +00:00
|
|
|
"/tests/test_cli.sh", script])
|
2019-04-12 18:17:28 +00:00
|
|
|
self.assertEqual(r.returncode, 0)
|
|
|
|
|
|
|
|
|
2019-06-25 08:22:32 +00:00
|
|
|
class ComposerTestResult(unittest.TestResult):
|
|
|
|
def name(self, test):
|
|
|
|
name = test.id().replace("__main__.", "")
|
|
|
|
if test.shortDescription():
|
|
|
|
name += ": " + test.shortDescription()
|
|
|
|
return name
|
|
|
|
|
|
|
|
def startTest(self, test):
|
|
|
|
super().startTest(test)
|
|
|
|
|
|
|
|
print("# ----------------------------------------------------------------------")
|
|
|
|
print("# ", self.name(test))
|
|
|
|
print("", flush=True)
|
|
|
|
|
|
|
|
def stopTest(self, test):
|
|
|
|
print(flush=True)
|
|
|
|
|
|
|
|
def addSuccess(self, test):
|
|
|
|
super().addSuccess(test)
|
|
|
|
print("ok {} {}".format(self.testsRun, self.name(test)))
|
|
|
|
|
|
|
|
def addError(self, test, err):
|
|
|
|
super().addError(test, err)
|
|
|
|
traceback.print_exception(*err, file=sys.stdout)
|
|
|
|
print("not ok {} {}".format(self.testsRun, self.name(test)))
|
|
|
|
|
|
|
|
def addFailure(self, test, err):
|
|
|
|
super().addError(test, err)
|
|
|
|
traceback.print_exception(*err, file=sys.stdout)
|
|
|
|
print("not ok {} {}".format(self.testsRun, self.name(test)))
|
|
|
|
|
|
|
|
def addSkip(self, test, reason):
|
|
|
|
super().addSkip(test, reason)
|
|
|
|
print("ok {} {} # SKIP {}".format(self.testsRun, self.name(test), reason))
|
|
|
|
|
|
|
|
def addExpectedFailure(self, test, err):
|
|
|
|
super().addExpectedFailure(test, err)
|
|
|
|
print("ok {} {}".format(self.testsRun, self.name(test)))
|
|
|
|
|
|
|
|
def addUnexpectedSuccess(self, test):
|
|
|
|
super().addUnexpectedSuccess(test)
|
|
|
|
print("not ok {} {}".format(self.testsRun, self.name(test)))
|
|
|
|
|
|
|
|
|
|
|
|
class ComposerTestRunner(object):
|
|
|
|
"""A test runner that (in combination with ComposerTestResult) outputs
|
|
|
|
results in a way that cockpit's log.html can read and format them.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, failfast=False):
|
|
|
|
self.failfast = failfast
|
|
|
|
|
|
|
|
def run(self, testable):
|
|
|
|
result = ComposerTestResult()
|
|
|
|
result.failfast = self.failfast
|
|
|
|
result.startTestRun()
|
|
|
|
count = testable.countTestCases()
|
|
|
|
print("1.." + str(count))
|
|
|
|
try:
|
|
|
|
testable(result)
|
|
|
|
finally:
|
|
|
|
result.stopTestRun()
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
2019-05-27 12:20:55 +00:00
|
|
|
def print_tests(tests):
|
|
|
|
for test in tests:
|
|
|
|
if isinstance(test, unittest.TestSuite):
|
|
|
|
print_tests(test)
|
|
|
|
elif isinstance(test, unittest.loader._FailedTest):
|
|
|
|
name = test.id().replace("unittest.loader._FailedTest.", "")
|
2019-06-25 08:22:32 +00:00
|
|
|
print("Error: '{}' does not match a test".format(name), file=sys.stderr)
|
2019-05-27 12:20:55 +00:00
|
|
|
else:
|
|
|
|
print(test.id().replace("__main__.", ""))
|
|
|
|
|
|
|
|
|
2019-04-12 18:17:28 +00:00
|
|
|
def main():
|
2019-05-23 23:16:37 +00:00
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
parser.add_argument("tests", nargs="*", help="List of tests modules, classes, and methods")
|
2019-05-27 12:20:55 +00:00
|
|
|
parser.add_argument("-l", "--list", action="store_true", help="Print the list of tests that would be executed")
|
2019-05-24 07:47:01 +00:00
|
|
|
parser.add_argument("-s", "--sit", action="store_true", help="Halt test execution (but keep VM running) when a test fails")
|
2019-05-23 23:16:37 +00:00
|
|
|
args = parser.parse_args()
|
|
|
|
|
2019-05-24 07:47:01 +00:00
|
|
|
ComposerTestCase.sit = args.sit
|
2019-05-23 23:16:37 +00:00
|
|
|
|
2019-05-24 07:47:01 +00:00
|
|
|
module = __import__("__main__")
|
2019-05-23 23:16:37 +00:00
|
|
|
if args.tests:
|
|
|
|
tests = unittest.defaultTestLoader.loadTestsFromNames(args.tests, module)
|
|
|
|
else:
|
|
|
|
tests = unittest.defaultTestLoader.loadTestsFromModule(module)
|
|
|
|
|
2019-05-27 12:20:55 +00:00
|
|
|
if args.list:
|
|
|
|
print_tests(tests)
|
|
|
|
return 0
|
|
|
|
|
2019-06-25 08:22:32 +00:00
|
|
|
runner = ComposerTestRunner(failfast=args.sit)
|
2019-05-23 23:16:37 +00:00
|
|
|
result = runner.run(tests)
|
2019-05-24 07:47:01 +00:00
|
|
|
|
2019-07-04 10:11:03 +00:00
|
|
|
if tests.countTestCases() != result.testsRun:
|
|
|
|
print("Error: unexpected number of tests were run", file=sys.stderr)
|
|
|
|
sys.exit(1)
|
|
|
|
|
2019-05-23 23:16:37 +00:00
|
|
|
sys.exit(not result.wasSuccessful())
|