Skip to content

Bugfix: restore ability to run virtual tests#35380

Closed
tldahlgren wants to merge 8 commits intospack:developfrom
tldahlgren:bugfix_run_virtual_tests
Closed

Bugfix: restore ability to run virtual tests#35380
tldahlgren wants to merge 8 commits intospack:developfrom
tldahlgren:bugfix_run_virtual_tests

Conversation

@tldahlgren
Copy link
Copy Markdown
Contributor

@tldahlgren tldahlgren commented Feb 8, 2023

Fixes #35519 (at least should)

This PR restores the ability to run stand-alone tests on virtual provider packages, including the ability to "inherit" tests from the virtual packages.

Also fixes an issue reported by @jfinney10 on slack:

$ spack -d test run
[...snip...]
  File "$spack/lib/spack/spack/repo.py", line 1153, in dump_provenance
    spec.package.patches.values()):
  File "$spack/lib/spack/spack/spec.py", line 1520, in package
    assert self.concrete, "Spec.package can only be called on concrete specs"
AssertionError: Spec.package can only be called on concrete specs

TODO

@spackbot-app spackbot-app bot added commands core PR affects Spack core functionality tests General test capability(ies) labels Feb 8, 2023
@tldahlgren tldahlgren force-pushed the bugfix_run_virtual_tests branch from 5d6a2c8 to c2ad1d9 Compare February 8, 2023 21:27
@tldahlgren tldahlgren marked this pull request as ready for review February 8, 2023 21:48
@tldahlgren tldahlgren force-pushed the bugfix_run_virtual_tests branch from c2ad1d9 to ec003c3 Compare February 8, 2023 23:29
@tldahlgren
Copy link
Copy Markdown
Contributor Author

Not clear why the libdwarf source stage directory was missing for the macOS check (https://github.com/spack/spack/actions/runs/4140755709/jobs/7159751165).

@tldahlgren
Copy link
Copy Markdown
Contributor Author

tldahlgren commented Feb 16, 2023

@tgamblin @alalazo Looks like all of the checks are now passing. This PR restores spack test's running of stand-alone tests defined in virtual packages while removing errors related to the fact those specs aren't concretized.

@tldahlgren
Copy link
Copy Markdown
Contributor Author

Grrr. I'm seeing positive coverage for all of the affected files and don't seem to be able to force re-running the check.

@tldahlgren tldahlgren force-pushed the bugfix_run_virtual_tests branch from 20fc891 to 7604e2b Compare February 17, 2023 01:06
Copy link
Copy Markdown
Member

@alalazo alalazo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few initial comments and questions. There's a conflict to be solved.

Comment on lines +121 to +124
cls = spec.package.__class__
except AssertionError:
try:
cls = spec.package_class
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there cases where these two might be different?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. I was thinking the majority of time the Spec is concrete and readily retrieved from spec.package.

When it isn't, such as for virtual packages, then the call spec.package_class that makes multiple calls (e.g., spack.repo.path.get_pkg_class(), importing the module, and more) is performed.

Comment on lines +146 to +167
if isinstance(spec_or_pkg, spack.spec.Spec):
spec = spec_or_pkg
cls = package_class(spec)
if not cls:
tty.debug("Skipping {0}: no package class found".format(spec.name))
return []
elif isinstance(spec_or_pkg, spack.package_base.PackageBase):
pkg = spec_or_pkg
cls = pkg.__class__
if add_virtuals:
methods = []
v_names = virtuals(pkg)
test_specs = [pkg.spec] + [spack.spec.Spec(v_name) for v_name in sorted(v_names)]
for spec in test_specs:
methods.extend(test_functions(spec, names=names))
return methods

elif inspect.isclass(spec_or_pkg):
cls = spec_or_pkg

else:
raise ValueError("Cannot retrieve test methods for {0}".format(spec_or_pkg))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth extracting this part into its own function? If I understand correctly, this gets as input either a spec object or a package object or a package class and returns a package class.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That needed another pass over the logic anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted into its own function.

Comment on lines +122 to +124
def package_directory(cls):
"""Returns the path to the package class directory."""
return os.path.abspath(os.path.dirname(cls.module.__file__))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was extracting this function necessary? I think package_dir would work on package classes too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't recall. Let me look into it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

@alalazo alalazo self-assigned this Feb 21, 2023
@tldahlgren tldahlgren force-pushed the bugfix_run_virtual_tests branch from 7604e2b to 18a594e Compare February 22, 2023 02:34
@tldahlgren tldahlgren force-pushed the bugfix_run_virtual_tests branch from 3364372 to 0328984 Compare February 22, 2023 20:16
@tldahlgren
Copy link
Copy Markdown
Contributor Author

I need to update my local black (or some setting) because it is not reporting the same style issues reported here. There was a style fix reported yesterday that I made but it seems like a new layout was required today.

@adamjstewart
Copy link
Copy Markdown
Member

black 23 is more strict than black 22, I recently updated CI to use black 23 and updated all existing code.

Copy link
Copy Markdown
Member

@alalazo alalazo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have test driven the PR and was able to:

$ spack -v test mpich
[ ... ]
$ spack test results --logs 4lb3fp6vigamkxjn56bxkdvvxtpwmkep
==> Results for test suite '4lb3fp6vigamkxjn56bxkdvvxtpwmkep':
==> test specs:
==>   mpich-4.1-yva65xh PASSED
==> Testing package mpich-4.1-yva65xh
==> [2023-02-27-18:11:01.638157] Installing /home/culpo/PycharmProjects/spack/opt/spack/linux-ubuntu20.04-icelake/gcc-9.4.0/mpich-4.1-yva65xhfzl7xertquaahwt3rg5rwpr6q/.spack/test to /home/culpo/.spack/test/4lb3fp6vigamkxjn56bxkdvvxtpwmkep/mpich-4.1-yva65xh/cache/mpich
==> [2023-02-27-18:11:01.902862] test: generate finalized file
==> [2023-02-27-18:11:01.903060] '/home/culpo/PycharmProjects/spack/opt/spack/linux-ubuntu20.04-icelake/gcc-9.4.0/mpich-4.1-yva65xhfzl7xertquaahwt3rg5rwpr6q/bin/mpicc' '/home/culpo/.spack/test/4lb3fp6vigamkxjn56bxkdvvxtpwmkep/mpich-4.1-yva65xh/cache/mpich/test/mpi/init/finalized.c' '-Wall' '-g' '-o' 'finalized'
PASSED
==> [2023-02-27-18:11:02.020309] test: run finalized example
==> [2023-02-27-18:11:02.020624] './finalized'
 No Errors
PASSED
==> [2023-02-27-18:11:03.382000] test: generate sendrecv file
==> [2023-02-27-18:11:03.382351] '/home/culpo/PycharmProjects/spack/opt/spack/linux-ubuntu20.04-icelake/gcc-9.4.0/mpich-4.1-yva65xhfzl7xertquaahwt3rg5rwpr6q/bin/mpicc' '/home/culpo/.spack/test/4lb3fp6vigamkxjn56bxkdvvxtpwmkep/mpich-4.1-yva65xh/cache/mpich/test/mpi/basic/sendrecv.c' '-Wall' '-g' '-o' 'sendrecv'
PASSED
==> [2023-02-27-18:11:03.530278] test: run sendrecv example
==> [2023-02-27-18:11:03.530589] './sendrecv'
Simple Send/Recv test.
Two processes needed.
PASSED
==> [2023-02-27-18:11:04.027395] test: mpicc: expect command status in [0]
==> [2023-02-27-18:11:04.027744] '/home/culpo/PycharmProjects/spack/opt/spack/linux-ubuntu20.04-icelake/gcc-9.4.0/mpich-4.1-yva65xhfzl7xertquaahwt3rg5rwpr6q/bin/mpicc' '-o' 'mpi_hello_c' '/home/culpo/.spack/test/4lb3fp6vigamkxjn56bxkdvvxtpwmkep/mpich-4.1-yva65xh/data/mpi/mpi_hello.c'
PASSED
==> [2023-02-27-18:11:04.154281] test: mpirun: expect command status in [0]
==> [2023-02-27-18:11:04.154603] '/home/culpo/PycharmProjects/spack/opt/spack/linux-ubuntu20.04-icelake/gcc-9.4.0/mpich-4.1-yva65xhfzl7xertquaahwt3rg5rwpr6q/bin/mpirun' '-np' '1' 'mpi_hello_c'
Hello world! From rank 0 of 1
PASSED
==> [2023-02-27-18:11:04.647879] test: mpif90: expect command status in [0]
==> [2023-02-27-18:11:04.648273] '/home/culpo/PycharmProjects/spack/opt/spack/linux-ubuntu20.04-icelake/gcc-9.4.0/mpich-4.1-yva65xhfzl7xertquaahwt3rg5rwpr6q/bin/mpif90' '-o' 'mpi_hello_f' '/home/culpo/.spack/test/4lb3fp6vigamkxjn56bxkdvvxtpwmkep/mpich-4.1-yva65xh/data/mpi/mpi_hello.f'
PASSED
==> [2023-02-27-18:11:04.769080] test: mpirun: expect command status in [0]
==> [2023-02-27-18:11:04.769346] '/home/culpo/PycharmProjects/spack/opt/spack/linux-ubuntu20.04-icelake/gcc-9.4.0/mpich-4.1-yva65xhfzl7xertquaahwt3rg5rwpr6q/bin/mpirun' '-np' '1' 'mpi_hello_f'
 Hello world! From rank           0 of            1
PASSED
==> [2023-02-27-18:11:05.232167] Completed testing

============================= 1 passed of 1 specs ==============================

I left a few minor comments on the code, but from a user perspective ✔️

Comment on lines +114 to +122
def package_class(spec_or_pkg):
"""Return the package class for the spec or package.

Args:
spec_or_pkg (spack.spec.Spec or spack.package_base.PackageBase): spec or
package (class)

Raises: ValueError: If not given a spec, package, or class
"""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add type annotation like this:

diff --git a/lib/spack/spack/install_test.py b/lib/spack/spack/install_test.py
index 416d502acc..ddbc717195 100644
--- a/lib/spack/spack/install_test.py
+++ b/lib/spack/spack/install_test.py
@@ -8,6 +8,7 @@
 import os
 import re
 import shutil
+from typing import Optional, Type, Union
 
 import llnl.util.filesystem as fs
 import llnl.util.tty as tty
@@ -111,14 +112,19 @@ def get_test_suite(name):
     return names[0]
 
 
-def package_class(spec_or_pkg):
+PkgClass = Type["spack.package_base.PackageBase"]
+
+
+def package_class(
+    spec_or_pkg: "Union[Spec, spack.package_base.PackageBase, PkgClass]",
+) -> Optional[PkgClass]:
     """Return the package class for the spec or package.
 
     Args:
-        spec_or_pkg (spack.spec.Spec or spack.package_base.PackageBase): spec or
-            package (class)
+        spec_or_pkg: spec or package (class)
 
-    Raises: ValueError: If not given a spec, package, or class
+    Raises:
+        ValueError: If not given a spec, package, or class
     """
     if isinstance(spec_or_pkg, spack.spec.Spec):
         spec = spec_or_pkg

so that mypy will check a bit more of our code base.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to get into the habit of adding the annotations.

elif isinstance(spec_or_pkg, spack.package_base.PackageBase):
cls = spec_or_pkg.__class__

elif inspect.isclass(spec_or_pkg):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this:

Suggested change
elif inspect.isclass(spec_or_pkg):
elif issubclass(spec_or_pkg, spack.package_base.PackageBase):

work too?

tty.debug("{0}: Cannot retrieve class from spec: {1}".format(spec.name, str(e)))
return None

elif isinstance(spec_or_pkg, spack.package_base.PackageBase):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we never import spack.package_base in this Python module (and importing it at the module level will probably lead to circular dependency issues).

Comment on lines +160 to +167
if add_virtuals and isinstance(spec_or_pkg, spack.package_base.PackageBase):
pkg = spec_or_pkg
v_names = virtuals(pkg)
test_specs = [pkg.spec] + [spack.spec.Spec(v_name) for v_name in sorted(v_names)]
methods = []
for spec in test_specs:
methods.extend(test_functions(spec, add_virtuals=False, names=names))
return methods
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Minor) If I read this one correctly here we are expanding virtual packages and recursing with the add_virtuals argument flipped, so to fall through and hit the code below. Would it be easy to avoid recursion and separate:

  1. Expansion of virtual specs
  2. Extraction of tests functions from list of specs

? If it seems complicated, let's leave it for another PR.

return methods

cls = package_class(spec_or_pkg)
if not cls:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Minor) I think this might be slightly safer for future refactoring:

Suggested change
if not cls:
if cls is None:

Comment on lines +185 to +188
source = (inspect.getsource(test_fn)).splitlines()[1:]
lines = (ln.strip() for ln in source)
statements = [ln for ln in lines if not ln.startswith("#")]
empty = len(statements) > 0 and statements[0] == "pass"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why we need to inspect sources. Is it to avoid printing to screen? Or something else?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ppl have expressed concerns about distinguishing between having a test method and having one that actually does something so inspecting to make sure non-empty (non-pass) helps make that distinction.

Comment on lines +1075 to +1079
except AssertionError as e:
# virtual specs won't have patches to install
tty.debug(
"Could not copy patch files for virtual package {0}: {1}".format(spec.name, str(e))
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do this more explicitly with a conditional upfront? This is for 2 main reasons:

  1. We might catch another assertion in the code
  2. Assertion might be turned off from the Python interpreter

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking I'd be more pythonic 😉 but I can certainly check in advance.

@tldahlgren
Copy link
Copy Markdown
Contributor Author

Closing this PR since the changes originated in #34236, which has refactoring that causes redundant work iterating on this and the other PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commands core PR affects Spack core functionality tests General test capability(ies)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spack test: Spec.package can only be called on concrete specs

3 participants