test-install : first draft that works

This commit is contained in:
alalazo 2016-04-27 13:56:32 +02:00
parent 99b52e6e71
commit 12dbd65f4c

View File

@ -24,10 +24,9 @@
############################################################################## ##############################################################################
import argparse import argparse
import codecs import codecs
import itertools
import os import os
import re
import time import time
import xml.dom.minidom
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import llnl.util.tty as tty import llnl.util.tty as tty
@ -58,54 +57,72 @@ def setup_parser(subparser):
subparser.add_argument('package', nargs=argparse.REMAINDER, help="spec of package to install") subparser.add_argument('package', nargs=argparse.REMAINDER, help="spec of package to install")
class JunitResultFormat(object):
def __init__(self):
self.root = ET.Element('testsuite')
self.tests = []
def add_test(self, buildId, testResult, buildInfo=None):
self.tests.append((buildId, testResult, buildInfo))
def write_to(self, stream):
self.root.set('tests', '{0}'.format(len(self.tests)))
for buildId, testResult, buildInfo in self.tests:
testcase = ET.SubElement(self.root, 'testcase')
testcase.set('classname', buildId.name)
testcase.set('name', buildId.stringId())
if testResult == TestResult.FAILED:
failure = ET.SubElement(testcase, 'failure')
failure.set('type', "Build Error")
failure.text = buildInfo
elif testResult == TestResult.SKIPPED:
skipped = ET.SubElement(testcase, 'skipped')
skipped.set('type', "Skipped Build")
skipped.text = buildInfo
ET.ElementTree(self.root).write(stream)
class TestResult(object): class TestResult(object):
PASSED = 0 PASSED = 0
FAILED = 1 FAILED = 1
SKIPPED = 2 SKIPPED = 2
ERRORED = 3
class BuildId(object): class TestSuite(object):
def __init__(self, spec): def __init__(self, filename):
self.name = spec.name self.filename = filename
self.version = spec.version self.root = ET.Element('testsuite')
self.hashId = spec.dag_hash() self.tests = []
def stringId(self): def __enter__(self):
return "-".join(str(x) for x in (self.name, self.version, self.hashId)) return self
def __hash__(self): def append(self, item):
return hash((self.name, self.version, self.hashId)) if not isinstance(item, TestCase):
raise TypeError('only TestCase instances may be appended to a TestSuite instance')
self.tests.append(item) # Append the item to the list of tests
def __eq__(self, other): def __exit__(self, exc_type, exc_val, exc_tb):
if not isinstance(other, BuildId): # Prepare the header for the entire test suite
return False number_of_errors = sum(x.result_type == TestResult.ERRORED for x in self.tests)
self.root.set('errors', str(number_of_errors))
number_of_failures = sum(x.result_type == TestResult.FAILED for x in self.tests)
self.root.set('failures', str(number_of_failures))
self.root.set('tests', str(len(self.tests)))
return ((self.name, self.version, self.hashId) == (other.name, other.version, other.hashId)) for item in self.tests:
self.root.append(item.element)
with open(self.filename, 'wb') as file:
xml_string = ET.tostring(self.root)
xml_string = xml.dom.minidom.parseString(xml_string).toprettyxml()
file.write(xml_string)
class TestCase(object):
results = {
TestResult.PASSED: None,
TestResult.SKIPPED: 'skipped',
TestResult.FAILED: 'failure',
TestResult.ERRORED: 'error',
}
def __init__(self, classname, name, time=None):
self.element = ET.Element('testcase')
self.element.set('classname', str(classname))
self.element.set('name', str(name))
if time is not None:
self.element.set('time', str(time))
self.result_type = None
def set_result(self, result_type, message=None, error_type=None, text=None):
self.result_type = result_type
result = TestCase.results[self.result_type]
if result is not None:
subelement = ET.SubElement(self.element, result)
if error_type is not None:
subelement.set('type', error_type)
if message is not None:
subelement.set('message', str(message))
if text is not None:
subelement.text = text
def fetch_log(path): def fetch_log(path):
@ -116,39 +133,7 @@ def fetch_log(path):
def failed_dependencies(spec): def failed_dependencies(spec):
return set(childSpec for childSpec in spec.dependencies.itervalues() if not spack.repo.get(childSpec).installed) return set(item for item in spec.dependencies.itervalues() if not spack.repo.get(item).installed)
def create_test_output(top_spec, newInstalls, output, getLogFunc=fetch_log):
# Post-order traversal is not strictly required but it makes sense to output
# tests for dependencies first.
for spec in top_spec.traverse(order='post'):
if spec not in newInstalls:
continue
failedDeps = failed_dependencies(spec)
package = spack.repo.get(spec)
if failedDeps:
result = TestResult.SKIPPED
dep = iter(failedDeps).next()
depBID = BuildId(dep)
errOutput = "Skipped due to failed dependency: {0}".format(depBID.stringId())
elif (not package.installed) and (not package.stage.source_path):
result = TestResult.FAILED
errOutput = "Failure to fetch package resources."
elif not package.installed:
result = TestResult.FAILED
lines = getLogFunc(package.build_log_path)
errMessages = list(line for line in lines if re.search('error:', line, re.IGNORECASE))
errOutput = errMessages if errMessages else lines[-10:]
errOutput = '\n'.join(itertools.chain([spec.to_yaml(), "Errors:"], errOutput, ["Build Log:",
package.build_log_path]))
else:
result = TestResult.PASSED
errOutput = None
bId = BuildId(spec)
output.add_test(bId, result, errOutput)
def get_top_spec_or_die(args): def get_top_spec_or_die(args):
@ -159,6 +144,62 @@ def get_top_spec_or_die(args):
return top_spec return top_spec
def install_single_spec(spec, number_of_jobs):
package = spack.repo.get(spec)
# If it is already installed, skip the test
if spack.repo.get(spec).installed:
testcase = TestCase(package.name, package.spec.short_spec, time=0.0)
testcase.set_result(TestResult.SKIPPED, message='Skipped [already installed]', error_type='already_installed')
return testcase
# If it relies on dependencies that did not install, skip
if failed_dependencies(spec):
testcase = TestCase(package.name, package.spec.short_spec, time=0.0)
testcase.set_result(TestResult.SKIPPED, message='Skipped [failed dependencies]', error_type='dep_failed')
return testcase
# Otherwise try to install the spec
try:
start_time = time.time()
package.do_install(keep_prefix=False,
keep_stage=True,
ignore_deps=False,
make_jobs=number_of_jobs,
verbose=True,
fake=False)
duration = time.time() - start_time
testcase = TestCase(package.name, package.spec.short_spec, duration)
except InstallError:
# An InstallError is considered a failure (the recipe didn't work correctly)
duration = time.time() - start_time
# Try to get the log
lines = fetch_log(package.build_log_path)
text = '\n'.join(lines)
testcase = TestCase(package.name, package.spec.short_spec, duration)
testcase.set_result(TestResult.FAILED, message='Installation failure', text=text)
except FetchError:
# A FetchError is considered an error (we didn't even start building)
duration = time.time() - start_time
testcase = TestCase(package.name, package.spec.short_spec, duration)
testcase.set_result(TestResult.ERRORED, message='Unable to fetch package')
return testcase
def get_filename(args, top_spec):
if not args.output:
fname = 'test-{x.name}-{x.version}-{hash}.xml'.format(x=top_spec, hash=top_spec.dag_hash())
output_directory = join_path(os.getcwd(), 'test-output')
if not os.path.exists(output_directory):
os.mkdir(output_directory)
output_filename = join_path(output_directory, fname)
else:
output_filename = args.output
return output_filename
def test_install(parser, args): def test_install(parser, args):
# Check the input # Check the input
if not args.package: if not args.package:
@ -173,51 +214,11 @@ def test_install(parser, args):
# Get the one and only top spec # Get the one and only top spec
top_spec = get_top_spec_or_die(args) top_spec = get_top_spec_or_die(args)
# Get the filename of the test
if not args.output: output_filename = get_filename(args, top_spec)
bId = BuildId(top_spec) # TEST SUITE
outputDir = join_path(os.getcwd(), "test-output") with TestSuite(output_filename) as test_suite:
if not os.path.exists(outputDir): # Traverse in post order : each spec is a test case
os.mkdir(outputDir) for spec in top_spec.traverse(order='post'):
outputFpath = join_path(outputDir, "test-{0}.xml".format(bId.stringId())) test_case = install_single_spec(spec, args.jobs)
else: test_suite.append(test_case)
outputFpath = args.output
new_installs = set()
for spec in top_spec.traverse(order='post'):
# Calling do_install for the top-level package would be sufficient but
# this attempts to keep going if any package fails (other packages which
# are not dependents may succeed)
package = spack.repo.get(spec)
if not package.installed:
new_installs.add(spec)
duration = 0.0
if (not failed_dependencies(spec)) and (not package.installed):
try:
start_time = time.time()
package.do_install(keep_prefix=False,
keep_stage=True,
ignore_deps=False,
make_jobs=args.jobs,
verbose=True,
fake=False)
duration = time.time() - start_time
except InstallError:
pass
except FetchError:
pass
jrf = JunitResultFormat()
handled = {}
create_test_output(top_spec, new_installs, jrf)
with open(outputFpath, 'wb') as F:
jrf.write_to(F)
# with JunitTestSuite(filename) as test_suite:
# for spec in top_spec.traverse(order='post'):
# test_case = install_test(spec)
# test_suite.append( test_case )
#