Compare commits

...

1 Commits

Author SHA1 Message Date
Gregory Becker
1c232759da
wip: working for installer tests
Signed-off-by: Gregory Becker <becker33@llnl.gov>
2025-03-20 12:31:33 -07:00
7 changed files with 323 additions and 23 deletions

View File

@ -215,6 +215,7 @@ def create_external_pruner() -> Callable[[spack.spec.Spec], RebuildDecision]:
"""Return a filter that prunes external specs""" """Return a filter that prunes external specs"""
def rebuild_filter(s: spack.spec.Spec) -> RebuildDecision: def rebuild_filter(s: spack.spec.Spec) -> RebuildDecision:
print(s.name, "external:", s.external)
if not s.external: if not s.external:
return RebuildDecision(True, "not external") return RebuildDecision(True, "not external")
return RebuildDecision(False, "external spec") return RebuildDecision(False, "external spec")

View File

@ -16,6 +16,8 @@
import spack.concretize import spack.concretize
import spack.config import spack.config
import spack.environment as ev import spack.environment as ev
import spack.hooks
import spack.hooks.report
import spack.paths import spack.paths
import spack.report import spack.report
import spack.spec import spack.spec
@ -329,13 +331,10 @@ def install(parser, args):
arguments.sanitize_reporter_options(args) arguments.sanitize_reporter_options(args)
def reporter_factory(specs): # TODO: This is hacky as hell
if args.log_format is None: if args.log_format is not None:
return lang.nullcontext() spack.hooks.report.reporter = args.reporter()
spack.hooks.report.report_file = args.log_file
return spack.report.build_context_manager(
reporter=args.reporter(), filename=report_filename(args, specs=specs), specs=specs
)
install_kwargs = install_kwargs_from_args(args) install_kwargs = install_kwargs_from_args(args)
@ -346,9 +345,9 @@ def reporter_factory(specs):
try: try:
if env: if env:
install_with_active_env(env, args, install_kwargs, reporter_factory) install_with_active_env(env, args, install_kwargs)
else: else:
install_without_active_env(args, install_kwargs, reporter_factory) install_without_active_env(args, install_kwargs)
except InstallError as e: except InstallError as e:
if args.show_log_on_error: if args.show_log_on_error:
_dump_log_on_error(e) _dump_log_on_error(e)
@ -382,7 +381,7 @@ def _maybe_add_and_concretize(args, env, specs):
env.write(regenerate=False) env.write(regenerate=False)
def install_with_active_env(env: ev.Environment, args, install_kwargs, reporter_factory): def install_with_active_env(env: ev.Environment, args, install_kwargs):
specs = spack.cmd.parse_specs(args.spec) specs = spack.cmd.parse_specs(args.spec)
# The following two commands are equivalent: # The following two commands are equivalent:
@ -416,7 +415,6 @@ def install_with_active_env(env: ev.Environment, args, install_kwargs, reporter_
install_kwargs["overwrite"] = [spec.dag_hash() for spec in specs_to_install] install_kwargs["overwrite"] = [spec.dag_hash() for spec in specs_to_install]
try: try:
with reporter_factory(specs_to_install):
env.install_specs(specs_to_install, **install_kwargs) env.install_specs(specs_to_install, **install_kwargs)
finally: finally:
if env.views: if env.views:
@ -461,13 +459,12 @@ def concrete_specs_from_file(args):
return result return result
def install_without_active_env(args, install_kwargs, reporter_factory): def install_without_active_env(args, install_kwargs):
concrete_specs = concrete_specs_from_cli(args, install_kwargs) + concrete_specs_from_file(args) concrete_specs = concrete_specs_from_cli(args, install_kwargs) + concrete_specs_from_file(args)
if len(concrete_specs) == 0: if len(concrete_specs) == 0:
tty.die("The `spack install` command requires a spec to install.") tty.die("The `spack install` command requires a spec to install.")
with reporter_factory(concrete_specs):
if args.overwrite: if args.overwrite:
require_user_confirmation_for_overwrite(concrete_specs, args) require_user_confirmation_for_overwrite(concrete_specs, args)
install_kwargs["overwrite"] = [spec.dag_hash() for spec in concrete_specs] install_kwargs["overwrite"] = [spec.dag_hash() for spec in concrete_specs]

View File

@ -27,6 +27,7 @@
class _HookRunner: class _HookRunner:
#: Order in which hooks are executed #: Order in which hooks are executed
HOOK_ORDER = [ HOOK_ORDER = [
"spack.hooks.report",
"spack.hooks.module_file_generation", "spack.hooks.module_file_generation",
"spack.hooks.licensing", "spack.hooks.licensing",
"spack.hooks.sbang", "spack.hooks.sbang",
@ -67,3 +68,6 @@ def __call__(self, *args, **kwargs):
pre_uninstall = _HookRunner("pre_uninstall") pre_uninstall = _HookRunner("pre_uninstall")
post_uninstall = _HookRunner("post_uninstall") post_uninstall = _HookRunner("post_uninstall")
pre_installer = _HookRunner("pre_installer")
post_installer = _HookRunner("post_installer")

View File

@ -0,0 +1,263 @@
# Copyright Spack Project Developers. See COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""Hooks to produce reports of spec installations"""
import collections
import gzip
import os
import time
import traceback
import llnl.util.filesystem as fs
import spack.build_environment
import spack.util.spack_json as sjson
reporter = None
report_file = None
Property = collections.namedtuple("Property", ["name", "value"])
class Record(dict):
def __getattr__(self, name):
# only called if no attribute exists
if name in self:
return self[name]
raise AttributeError(f"RequestRecord for {self.name} has no attribute {name}")
def __setattr__(self, name, value):
if name.startswith("_"):
super().__setattr__(name, value)
else:
self[name] = value
class RequestRecord(Record):
def __init__(self, spec):
super().__init__()
self.name = spec.name
self.errors = None
self.nfailures = None
self.npackages = None
self.time = None
self.timestamp = time.strftime("%a, d %b %Y %H:%M:%S", time.gmtime())
self.properties = [
Property("architecture", spec.architecture),
Property("compiler", spec.compiler),
]
self.packages = []
self._seen = set()
def append_record(self, record, key):
self.packages.append(record)
self._seen.add(key)
def seen(self, key):
return key in self._seen
def summarize(self):
self.npackages = len(self.packages)
self.nfailures = len([r for r in self.packages if r.result == "failure"])
self.nerrors = len([r for r in self.packages if r.result == "error"])
self.time = sum(float(r.elapsed_time or 0.0) for r in self.packages)
class SpecRecord(Record):
pass
class InstallRecord(SpecRecord):
def __init__(self, spec):
super().__init__()
self._spec = spec
self._package = spec.package
self._start_time = time.time()
self.name = spec.name
self.id = spec.dag_hash()
self.elapsed_time = None
self.result = None
self.message = None
self.installed_from_binary_cache = None
def fetch_log(self):
try:
if os.path.exists(self._package.install_log_path):
stream = gzip.open(self._package.install_log_path, "rt", encoding="utf-8")
else:
stream = open(self._package.log_path, encoding="utf-8")
with stream as f:
return f.read()
except OSError:
return f"Cannot open log for {self._spec.cshort_spec}"
def fetch_time(self):
try:
with open(self._package.times_log_path, "r", encoding="utf-8") as f:
data = sjson.load(f.read())
return data["total"]
except Exception:
return None
def skip(self, msg):
self.result = "skipped"
self.elapsed_time = 0.0
self.message = msg
def succeed(self):
self.result = "success"
self.stdout = self.fetch_log()
self.installed_from_binary_cache = self._package.installed_from_binary_cache
self.elapsed_time = self.fetch_time()
def fail(self, exc):
if isinstance(exc, spack.build_environment.InstallError):
self.result = "failure"
self.message = exc.message or "Installation failure"
self.exception = exc.traceback
else:
self.result = "error"
self.message = str(exc) or "Unknown error"
self.exception = traceback.format_exc()
self.stdout = self.fetch_log() + self.message
requests = {}
def pre_installer(specs):
global requests
for root in specs:
request = RequestRecord(root)
requests[root.dag_hash()] = request
for dep in filter(lambda x: x.installed, root.traverse()):
record = InstallRecord(dep)
record.skip(msg="Spec already installed")
request.append_record(record, dep.dag_hash())
def post_installer(specs, hashes_to_failures):
global requests
global report_file
global reporter
try:
for root in specs:
request = requests[root.dag_hash()]
# Associate all dependency jobs with this request
for dep in root.traverse():
if request.seen(dep.dag_hash()):
continue # Already handled
record = InstallRecord(dep)
if dep.dag_hash() in hashes_to_failures:
record.fail(hashes_to_failures[dep.dag_hash()])
elif dep.installed:
record.succeed()
else:
# This package was never reached because of an earlier failure
continue
request.append_record(record, dep.dag_hash())
# Aggregate request-level data
request.summarize()
# Write the actual report
if not report_file:
basename = specs[0].format("test-{name}-{version}-{hash}.xml")
dirname = os.path.join(spack.paths.reports_path, "junit")
fs.mkdirp(dirname)
report_file = os.path.join(dirname, basename)
if reporter:
reporter.build_report(report_file, specs=list(requests.values()))
finally:
# Clean up after ourselves
requests = {}
reporter = None
report_file = None
# This is not thread safe, but that should be ok
# We only have one top-level thread launching build requests, and all parallelism
# is between the jobs of different requests
# requests: Dict[str, RequestRecord] = {}
# specs: Dict[str, InstallRecord] = {}
# def pre_installer(specs):
# global requests
# global specs
# for spec in specs:
# record = RequestRecord(spec)
# requests[spec.dag_hash()] = record
# for dep in filter(lambda x: x.installed, spec.traverse()):
# spec_record = InstallRecord(dep)
# spec_record.elapsed_time = "0.0"
# spec_record.result = "skipped"
# spec_record.message = "Spec already installed"
# specs[dep.dag_hash()] = spec_record
# def pre_install(spec):
# global specs
# specs[spec.dag_hash()] = InstallRecord(spec)
# def post_install(spec, explicit: bool):
# global specs
# record = specs[spec.dag_hash()]
# record.result = "success"
# record.stdout = record.fetch_log()
# record.installed_from_binary_cache = record._package.installed_from_binary_cache
# record.elapsed_time = time.time() - record._start_time
# def post_failure(spec, error):
# global specs
# record = specs[spec.dag_hash()]
# if isinstance(error, spack.build_environment.InstallError):
# record.result = "failure"
# record.message = exc.message or "Installation failure"
# record.exception = exc.traceback
# else:
# record.result = "error"
# record.message = str(exc) or "Unknown error"
# record.exception = traceback.format_exc()
# record.stdout = record.fetch_log() + record.message
# record.elapsed_time = time.time() - record._start_time
# def post_installer(specs):
# global requests
# global specs
# global reporter
# global report_file
# for spec in specs:
# # Find all associated spec records
# request_record = requests[spec.dag_hash()]
# for dep in spec.traverse(root=True):
# spec_record = specs[dep.dag_hash()]
# request_record.records.append(spec_record)
# # Aggregate statistics
# request_record.npackages = len(request_record.records)
# request_record.nfailures = len([r for r in request_record.records if r.result == "failure"])
# request_record.errors = len([r for r in request_record.records if r.result == "error"])
# request_record.time = sum(float(r.elapsed_time) for r in request_record.records)
# # Write the actual report
# filename = report_file or specs[0].name
# reporter.build_report(filename, specs=specs)
# # Clean up after ourselves
# requests = {}
# specs = {}

View File

@ -2014,11 +2014,13 @@ def _install_action(self, task: Task) -> InstallAction:
def install(self) -> None: def install(self) -> None:
"""Install the requested package(s) and or associated dependencies.""" """Install the requested package(s) and or associated dependencies."""
spack.hooks.pre_installer([r.pkg.spec for r in self.build_requests])
self._init_queue() self._init_queue()
fail_fast_err = "Terminating after first install failure" fail_fast_err = "Terminating after first install failure"
single_requested_spec = len(self.build_requests) == 1 single_requested_spec = len(self.build_requests) == 1
failed_build_requests = [] failed_build_requests = []
failed_tasks = [] # self.failed tracks dependents of failed tasks, here only failures
install_status = InstallStatus(len(self.build_pq)) install_status = InstallStatus(len(self.build_pq))
@ -2171,13 +2173,24 @@ def install(self) -> None:
except KeyboardInterrupt as exc: except KeyboardInterrupt as exc:
# The build has been terminated with a Ctrl-C so terminate # The build has been terminated with a Ctrl-C so terminate
# regardless of the number of remaining specs. # regardless of the number of remaining specs.
failed_tasks.append((pkg, exc))
tty.error( tty.error(
f"Failed to install {pkg.name} due to " f"{exc.__class__.__name__}: {str(exc)}" f"Failed to install {pkg.name} due to " f"{exc.__class__.__name__}: {str(exc)}"
) )
hashes_to_failures = {pkg.spec.dag_hash(): exc for pkg, exc in failed_tasks}
spack.hooks.post_installer(
[r.pkg.spec for r in self.build_requests], hashes_to_failures
)
print("DDDDDD")
raise raise
except binary_distribution.NoChecksumException as exc: except binary_distribution.NoChecksumException as exc:
if task.cache_only: if task.cache_only:
failed_tasks.append((pkg, exc))
hashes_to_failures = {pkg.spec.dag_hash(): exc for pkg, exc in failed_tasks}
spack.hooks.post_installer(
[r.pkg.spec for r in self.build_requests], hashes_to_failures
)
raise raise
# Checking hash on downloaded binary failed. # Checking hash on downloaded binary failed.
@ -2192,6 +2205,7 @@ def install(self) -> None:
except (Exception, SystemExit) as exc: except (Exception, SystemExit) as exc:
self._update_failed(task, True, exc) self._update_failed(task, True, exc)
failed_tasks.append((pkg, exc))
# Best effort installs suppress the exception and mark the # Best effort installs suppress the exception and mark the
# package as a failure. # package as a failure.
@ -2204,8 +2218,14 @@ def install(self) -> None:
f"Failed to install {pkg.name} due to " f"Failed to install {pkg.name} due to "
f"{exc.__class__.__name__}: {str(exc)}" f"{exc.__class__.__name__}: {str(exc)}"
) )
# Terminate if requested to do so on the first failure. # Terminate if requested to do so on the first failure.
if self.fail_fast: if self.fail_fast:
hashes_to_failures = {pkg.spec.dag_hash(): exc for pkg, exc in failed_tasks}
spack.hooks.post_installer(
[r.pkg.spec for r in self.build_requests], hashes_to_failures
)
print("AAAAAAA")
raise spack.error.InstallError( raise spack.error.InstallError(
f"{fail_fast_err}: {str(exc)}", pkg=pkg f"{fail_fast_err}: {str(exc)}", pkg=pkg
) from exc ) from exc
@ -2213,8 +2233,15 @@ def install(self) -> None:
# Terminate when a single build request has failed, or summarize errors later. # Terminate when a single build request has failed, or summarize errors later.
if task.is_build_request: if task.is_build_request:
if single_requested_spec: if single_requested_spec:
hashes_to_failures = {
pkg.spec.dag_hash(): exc for pkg, exc in failed_tasks
}
spack.hooks.post_installer(
[r.pkg.spec for r in self.build_requests], hashes_to_failures
)
print("BBBBB")
raise raise
failed_build_requests.append((pkg, pkg_id, str(exc))) failed_build_requests.append((pkg, pkg_id, exc))
finally: finally:
# Remove the install prefix if anything went wrong during # Remove the install prefix if anything went wrong during
@ -2238,9 +2265,13 @@ def install(self) -> None:
if request.install_args.get("install_package") and request.pkg_id not in self.installed if request.install_args.get("install_package") and request.pkg_id not in self.installed
] ]
hashes_to_failures = {pkg.spec.dag_hash(): exc for pkg, exc in failed_tasks}
spack.hooks.post_installer([r.pkg.spec for r in self.build_requests], hashes_to_failures)
print("CCCCC", failed_build_requests)
if failed_build_requests or missing: if failed_build_requests or missing:
for _, pkg_id, err in failed_build_requests: for _, pkg_id, err in failed_build_requests:
tty.error(f"{pkg_id}: {err}") tty.error(f"{pkg_id}: {str(err)}")
for _, pkg_id in missing: for _, pkg_id in missing:
tty.error(f"{pkg_id}: Package was not installed") tty.error(f"{pkg_id}: Package was not installed")

View File

@ -217,6 +217,7 @@ def build_report_for_package(self, report_dir, package, duration):
nerrors = len(errors) nerrors = len(errors)
if nerrors > 0: if nerrors > 0:
print("NERRORS")
self.success = False self.success = False
if phase == "configure": if phase == "configure":
report_data[phase]["status"] = 1 report_data[phase]["status"] = 1
@ -410,6 +411,7 @@ def concretization_report(self, report_dir, msg):
self.current_package_name = self.base_buildname self.current_package_name = self.base_buildname
self.upload(output_filename) self.upload(output_filename)
self.success = False self.success = False
print("CONCRETIZATION")
self.finalize_report() self.finalize_report()
def initialize_report(self, report_dir): def initialize_report(self, report_dir):

View File

@ -450,6 +450,8 @@ def just_throw(*args, **kwargs):
content = filename.open().read() content = filename.open().read()
print(content)
# Only libelf error is reported (through libdwarf root spec). libdwarf # Only libelf error is reported (through libdwarf root spec). libdwarf
# install is skipped and it is not an error. # install is skipped and it is not an error.
assert 'tests="1"' in content assert 'tests="1"' in content