Features/spack test refactor cmds (#18518)

* no clean -t option, use 'spack test remove'

* refactor commands to make better use of TestSuite objects
This commit is contained in:
Greg Becker
2020-09-10 12:58:35 -07:00
committed by Tamara Dahlgren
parent 102b91203a
commit 19e226259c
5 changed files with 162 additions and 85 deletions

View File

@@ -47,10 +47,7 @@ def setup_parser(subparser):
'-p', '--python-cache', action='store_true',
help="remove .pyc, .pyo files and __pycache__ folders")
subparser.add_argument(
'-t', '--test-stage', action='store_true',
help="remove all files in Spack test stage")
subparser.add_argument(
'-a', '--all', action=AllClean, help="equivalent to -sdfmpt", nargs=0
'-a', '--all', action=AllClean, help="equivalent to -sdfmp", nargs=0
)
arguments.add_common_arguments(subparser, ['specs'])
@@ -58,7 +55,7 @@ def setup_parser(subparser):
def clean(parser, args):
# If nothing was set, activate the default
if not any([args.specs, args.stage, args.downloads, args.failures,
args.misc_cache, args.test_stage, args.python_cache]):
args.misc_cache, args.python_cache]):
args.stage = True
# Then do the cleaning falling through the cases
@@ -86,11 +83,6 @@ def clean(parser, args):
tty.msg('Removing cached information on repositories')
spack.caches.misc_cache.destroy()
if args.test_stage:
tty.msg("Removing files in test stage")
test_remove_args = collections.namedtuple('args', ['name'])(None)
spack.cmd.test.test_remove(test_remove_args)
if args.python_cache:
tty.msg('Removing python cache files')
for directory in [lib_path, var_path]:

View File

@@ -7,13 +7,12 @@
import os
import argparse
import textwrap
import datetime
import fnmatch
import re
import shutil
import sys
import llnl.util.tty as tty
import llnl.util.filesystem as fs
import spack.install_test
import spack.environment as ev
@@ -33,10 +32,9 @@ def setup_parser(subparser):
# Run
run_parser = sp.add_parser('run', help=test_run.__doc__)
name_help_msg = "Name the test for subsequent access."
name_help_msg += " Default is the timestamp of the run formatted"
name_help_msg += " 'YYYY-MM-DD_HH:MM:SS'"
run_parser.add_argument('-n', '--name', help=name_help_msg)
alias_help_msg = "Provide an alias for this test-suite"
alias_help_msg += " for subsequent access."
run_parser.add_argument('--alias', help=alias_help_msg)
run_parser.add_argument(
'--fail-fast', action='store_true',
@@ -88,19 +86,30 @@ def setup_parser(subparser):
'filter', nargs=argparse.REMAINDER,
help='optional case-insensitive glob patterns to filter results.')
# Find
find_parser = sp.add_parser('find', help=test_find.__doc__)
find_parser.add_argument(
'filter', nargs=argparse.REMAINDER,
help='optional case-insensitive glob patterns to filter results.')
# Status
status_parser = sp.add_parser('status', help=test_status.__doc__)
status_parser.add_argument('name', help="Test for which to provide status")
status_parser.add_argument(
'names', nargs=argparse.REMAINDER,
help="Test suites for which to print status")
# Results
results_parser = sp.add_parser('results', help=test_results.__doc__)
results_parser.add_argument('name', help="Test for which to print results")
results_parser.add_argument(
'names', nargs=argparse.REMAINDER,
help="Test suites for which to print results")
# Remove
remove_parser = sp.add_parser('remove', help=test_remove.__doc__)
arguments.add_common_arguments(remove_parser, ['yes_to_all'])
remove_parser.add_argument(
'name', nargs='?',
help="Test to remove from test stage")
'names', nargs=argparse.REMAINDER,
help="Test suites to remove from test stage")
def test_run(args):
@@ -139,7 +148,7 @@ def test_run(args):
specs_to_test.extend(matching)
# test_stage_dir
test_suite = spack.install_test.TestSuite(specs_to_test, args.name)
test_suite = spack.install_test.TestSuite(specs_to_test, args.alias)
test_suite.ensure_stage()
tty.msg("Spack test %s" % test_suite.name)
@@ -171,9 +180,16 @@ def test_run(args):
def test_list(args):
"""List tests that are running or have available results."""
stage_dir = spack.install_test.get_test_stage_dir()
tests = os.listdir(stage_dir)
"""List all installed packages with available tests."""
raise NotImplementedError
def test_find(args): # TODO: merge with status (noargs)
"""Find tests that are running or have available results.
Displays aliases for tests that have them, otherwise test suite content
hashes."""
test_suites = spack.install_test.get_all_test_suites()
# Filter tests by filter argument
if args.filter:
@@ -185,14 +201,16 @@ def create_filter(f):
def match(t, f):
return f.match(t)
tests = [t for t in tests
if any(match(t, f) for f in filters) and
os.path.isdir(os.path.join(stage_dir, t))]
test_suites = [t for t in test_suites
if any(match(t.alias, f) for f in filters) and
os.path.isdir(os.path.join(stage_dir, t))]
if tests:
names = [t.name for t in test_suites]
if names:
# TODO: Make these specify results vs active
msg = "Spack test results available for the following tests:\n"
msg += " %s\n" % ' '.join(tests)
msg += " %s\n" % ' '.join(names)
msg += " Run `spack test remove` to remove all tests"
tty.msg(msg)
else:
@@ -202,40 +220,57 @@ def match(t, f):
def test_status(args):
"""Get the current status for a particular Spack test."""
name = args.name
stage = spack.install_test.get_test_stage(name)
if os.path.exists(stage):
# TODO: Make this handle capability tests too
tty.msg("Test %s completed" % name)
"""Get the current status for a particular Spack test suites."""
if args.names:
test_suites = []
for name in args.names:
test_suite = spack.install_test.get_test_suite(name)
if test_suite:
test_suites.append(test_suite)
else:
tty.msg("No test suite %s found in test stage" % name)
else:
tty.msg("Test %s no longer available" % name)
test_suites = spack.install_test.get_all_test_suites()
if not test_suites:
tty.msg("No test suites with status to report")
for test_suite in test_suites:
# TODO: Make this handle capability tests too
# TODO: Make this handle tests running in another process
tty.msg("Test suite %s completed" % test_suite.name)
def test_results(args):
"""Get the results for a particular Spack test."""
name = args.name
stage = spack.install_test.get_test_stage(name)
"""Get the results from Spack test suites (default all)."""
if args.names:
test_suites = []
for name in args.names:
test_suite = spack.install_test.get_test_suite(name)
if test_suite:
test_suites.append(test_suite)
else:
tty.msg("No test suite %s found in test stage" % name)
else:
test_suites = spack.install_test.get_all_test_suites()
if not test_suites:
tty.msg("No test suites with results to report")
# TODO: Make this handle capability tests too
# The results file may turn out to be a placeholder for future work
if os.path.exists(stage):
results_file = spack.install_test.get_results_file(name)
for test_suite in test_suites:
results_file = test_suite.results_file
if os.path.exists(results_file):
msg = "Results for test %s: \n" % name
msg = "Results for test suite %s: \n" % test_suite.name
with open(results_file, 'r') as f:
lines = f.readlines()
for line in lines:
msg += " %s" % line
tty.msg(msg)
else:
msg = "Test %s has no results.\n" % name
msg += " Check if it is active with "
msg += "`spack test status %s`" % name
msg = "Test %s has no results.\n" % test_suite.name
msg += " Check if it is running with "
msg += "`spack test status %s`" % test_suite.name
tty.msg(msg)
else:
tty.msg("No test %s found in test stage" % name)
def test_remove(args):
@@ -245,11 +280,32 @@ def test_remove(args):
Removed tests can no longer be accessed for results or status, and will not
appear in `spack test list` results."""
if args.name:
shutil.rmtree(spack.install_test.get_test_stage(args.name))
if args.names:
test_suites = []
for name in args.names:
test_suite = spack.install_test.get_test_suite(name)
if test_suite:
test_suites.append(test_suite)
else:
tty.msg("No test suite %s found in test stage" % name)
else:
fs.remove_directory_contents(spack.install_test.get_test_stage_dir())
test_suites = spack.install_test.get_all_test_suites()
if not test_suites:
tty.msg("No test suites to remove")
return
if not args.yes_to_all:
msg = 'The following test suites will be removed'
msg += '\n\n ' + ' '.join(test.name for test in test_suites) + '\n'
tty.msg(msg)
answer = tty.get_yes_or_no('Do you want to proceed?', default=False)
if not answer:
tty.msg('Aborting removal of test suites')
return
for test_suite in test_suites:
shutil.rmtree(test_suite.stage)
def test(parser, args):
globals()['test_%s' % args.test_command](args)

View File

@@ -18,6 +18,10 @@
import spack.util.spack_json as sjson
test_suite_filename = 'test_suite.lock'
results_filename = 'results.txt'
def get_escaped_text_output(filename):
"""Retrieve and escape the expected text output from the file
@@ -41,43 +45,61 @@ def get_test_stage_dir():
spack.config.get('config:test_stage', '~/.spack/test'))
def get_test_stage(name):
return spack.util.prefix.Prefix(os.path.join(get_test_stage_dir(), name))
def get_all_test_suites():
stage_root = get_test_stage_dir()
def valid_stage(d):
dirpath = os.path.join(stage_root, d)
return (os.path.isdir(dirpath) and
test_suite_filename in os.listdir(dirpath))
candidates = [
os.path.join(stage_root, d, test_suite_filename)
for d in os.listdir(stage_root)
if valid_stage(d)
]
test_suites = [TestSuite.from_file(c) for c in candidates]
return test_suites
def get_results_file(name):
return get_test_stage(name).join('results.txt')
def get_test_suite(name):
assert name, "Cannot search for empty test name or 'None'"
test_suites = get_all_test_suites()
names = [ts for ts in test_suites
if ts.name == name]
assert len(names) < 2, "alias shadows test suite hash"
def get_test_by_name(name):
test_suite_file = get_test_stage(name).join('specs.lock')
if not os.path.isdir(test_suite_file):
raise Exception
test_suite_data = sjson.load(test_suite_file)
return TestSuite.from_dict(test_suite_data, name)
if not names:
return None
return names[0]
class TestSuite(object):
def __init__(self, specs, name=None):
self._name = name
def __init__(self, specs, alias=None):
# copy so that different test suites have different package objects
# even if they contain the same spec
self.specs = [spec.copy() for spec in specs]
self.current_test_spec = None # spec currently tested, can be virtual
self.current_base_spec = None # spec currently running do_test
self.alias = alias
self._hash = None
@property
def name(self):
if not self._name:
return self.alias if self.alias else self.content_hash
@property
def content_hash(self):
if not self._hash:
json_text = sjson.dump(self.to_dict())
sha = hashlib.sha1(json_text.encode('utf-8'))
b32_hash = base64.b32encode(sha.digest()).lower()
if sys.version_info[0] >= 3:
b32_hash = b32_hash.decode('utf-8')
self._name = b32_hash
return self._name
self._hash = b32_hash
return self._hash
def __call__(self, *args, **kwargs):
self.write_reproducibility_data()
@@ -104,7 +126,6 @@ def __call__(self, *args, **kwargs):
# run the package tests
spec.package.do_test(
name=self.name,
dirty=dirty
)
@@ -134,11 +155,12 @@ def ensure_stage(self):
@property
def stage(self):
return get_test_stage(self.name)
return spack.util.prefix.Prefix(
os.path.join(get_test_stage_dir(), self.content_hash))
@property
def results_file(self):
return get_results_file(self.name)
return self.stage.join(results_filename)
@classmethod
def test_pkg_id(cls, spec):
@@ -191,17 +213,31 @@ def write_reproducibility_data(self):
except spack.repo.UnknownPackageError:
pass # not all virtuals have package files
with open(self.stage.join('specs.lock'), 'w') as f:
with open(self.stage.join(test_suite_filename), 'w') as f:
sjson.dump(self.to_dict(), stream=f)
def to_dict(self):
specs = [s.to_dict() for s in self.specs]
return {'specs': specs}
d = {'specs': specs}
if self.alias:
d['alias'] = self.alias
return d
@staticmethod
def from_dict(d, name=None):
def from_dict(d):
specs = [Spec.from_dict(spec_dict) for spec_dict in d['specs']]
return TestSuite(specs, name)
alias = d.get('alias', None)
return TestSuite(specs, alias)
@staticmethod
def from_file(filename):
try:
with open(filename, 'r') as f:
data = sjson.load(f)
return TestSuite.from_dict(data)
except Exception as e:
tty.debug(e)
raise sjson.SpackJSONError("error parsing JSON TestSuite:", str(e))
def _add_msg_to_file(filename, msg):

View File

@@ -1659,7 +1659,7 @@ def cache_extra_test_sources(self, srcs):
test_failures = None
test_suite = None
def do_test(self, name, remove_directory=False, dirty=False):
def do_test(self, dirty=False):
if self.test_requires_compiler:
compilers = spack.compilers.compilers_for_spec(
self.spec.compiler, arch_spec=self.spec.architecture)
@@ -1733,17 +1733,12 @@ def test_process():
if self.test_failures:
raise TestFailure(self.test_failures)
# cleanup test directory on success
if remove_directory:
shutil.rmtree(testdir)
finally:
# reset debug level
tty.set_debug(old_debug)
spack.build_environment.fork(
self, test_process, dirty=dirty, fake=False, context='test',
test_name=name)
self, test_process, dirty=dirty, fake=False, context='test')
def test(self):
pass

View File

@@ -33,7 +33,6 @@ def __call__(self, *args, **kwargs):
spack.caches.misc_cache, 'destroy', Counter('caches'))
monkeypatch.setattr(
spack.installer, 'clear_failures', Counter('failures'))
monkeypatch.setattr(fs, 'remove_directory_contents', Counter('tests'))
yield counts
@@ -50,7 +49,6 @@ def __call__(self, *args, **kwargs):
('-sd', ['stages', 'downloads']),
('-m', ['caches']),
('-f', ['failures']),
('-t', ['tests']),
('-a', all_effects),
('', []),
])