Skip to content

PythonPackage: Let There Be Tests!#2869

Merged
tgamblin merged 12 commits intospack:developfrom
adamjstewart:features/python-tests
Mar 31, 2017
Merged

PythonPackage: Let There Be Tests!#2869
tgamblin merged 12 commits intospack:developfrom
adamjstewart:features/python-tests

Conversation

@adamjstewart
Copy link
Copy Markdown
Member

@adamjstewart adamjstewart commented Jan 18, 2017

Resolves #1305

The goal of this PR is to add a better testing framework for our Python packages. I see two low-hanging fruit that we can use for this:

  • Run python setup.py test if available
  • Try to import the Python module after installation

Most Python modules (maybe only the ones that use setuptools?) implement some kind of a "test" command that can be run after "build" and before "install". This provides basic package tests, implemented by the developers.

The main thing holding this up is that we really need a "test" deptype (see #1279). For example, py-setuptools has no dependencies, aside from Python. But the test suite requires:

tests_require=[                                                             
    'setuptools[ssl]',                                                      
    'pytest-flake8',                                                        
    'pytest>=2.8',                                                          
] + (['mock'] if sys.version_info[:2] < (3, 3) else []),     

I certainly don't want to add these as build dependencies, because they will make all package installations significantly more complex. I only want to build these dependencies if the user supplies --run-tests. And I don't want these dependencies included in the hash. spack install --run-tests py-numpy and spack install py-numpy should not be two separate installations.

As for the import tests after installation, do we want to always run these (like the sanity_check_prefix method), or only with --run-tests? Here is what I'm envisioning. By default, Spack tries to import the name of the packages minus the "py-". We will also need a method to override this. For example, the import name for py-meep~mpi is meep, but the import name for py-meep+mpi is meep_mpi. This will need to be tested on a per-package basis, but for the most part, dropping "py-" should give us the import name for most Python packages.

@adamjstewart adamjstewart added python tests General test capability(ies) WIP labels Jan 18, 2017
setup = self.setup_file(self.spec, self.prefix)

python(setup, '--no-user-cfg', command, '--help', **kwargs)
return python.returncode == 0
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This seems kind of hacky, but I couldn't think of a better way to tell whether or not python setup.py test is available. I also don't like how it prints the test line when we run with spack install --verbose --run-tests. If we could make that configurable, it would be nice.

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.

Not sure how best to do this, but note that by default Executables raise an exception on error. You need to pass fail_on_error=False

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@tgamblin See kwargs above.

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.

@adamjstewart: I think I'm past my daily intelligible github post limit 😄

@citibeth
Copy link
Copy Markdown
Member

Everything in the first part of the OP sounds reasonable. I agree... we need a test deptype that doesn't change the hash. Should be possible, no?

As for the import tests after installation, do we want to always run these (like the sanity_check_prefix method), or only with --run-tests?

I would vote for always. Python packages aren't very useful if you can't import them, and it's MUCH quicker than real unit tests.

Alternately, could we have a new kind of test phase that does import tests?

Here is what I'm envisioning. By default, Spack tries to import the name of the packages minus the "py-".

My guess is that will lead to dozens or hundreds of spurious problems, which will have to be worked out immediately. There's not much variation here to make such an implicit rule useful, IMHO. I think we should KISS: the Spack package provides a list of Python packages to be imported on the test. Yes, this would mean we will only achieve test coverage over time. But I think it's a clean approach, and it makes the original PR MUCH easier.

Or do it right... root around in the package post-install, find all the packages, and try to import them.

Or do it really right... Python setup.py scripts contain the list of packages to install. Use setuptools/distutils to read that list directly, and then try your import tests.

@adamjstewart
Copy link
Copy Markdown
Member Author

@citibeth Those are all really useful ideas that I didn't think about! I agree that we should probably always run import tests.

Let's look at the options we have:

  1. The user must provide a list of modules to try importing

As you've probably already noticed, I prefer auto-magic over explicit if it cuts down on work and lines of code. But yes, if I made it auto-magic, I would have to manually test every PythonPackage in Spack to make sure it doesn't break anything. Not ideal. Asking the user to provide a list of modules and only running the import tests if this list exists is definitely the easiest way to go about things.

  1. Root around in the package post-install, find all the packages, and try to import them

This could have unforeseen consequences. For example, we don't want to pick up modules that are only used for testing that would have additional dependencies. We also wouldn't want to pick up any "development" or "internal-use-only" files.

  1. Python setup.py scripts contain the list of packages to install. Use setuptools/distutils to read that list directly, and then try your import tests.

Now this would be really nifty. I'm not sure how reliable it would be though. Some packages like setuptools do this, and we could get the list by running:

$ python
>>> import setup
>>> print(setup.setup_params['packages'])
['pkg_resources', 'setuptools', 'pkg_resources.extern', 'pkg_resources._vendor', 'pkg_resources._vendor.packaging', 'setuptools.extern', 'setuptools.command']

But setup.setup_params isn't any kind of standard convention. Other packages like numpy don't even do this.

I'm also seeing the following option:

$ python setup.py --help
...
  --provides          print the list of packages/modules provided

but this list is empty for all of the packages I've tried.

The way that py-setuptools gets its list is by running:

setuptools.find_packages(exclude=['*.tests'])

This might be the best option, although we run into two problems. Not all packages depend on setuptools, some use the builtin distutils, and I don't think distutils has this capability. Should we add setuptools as a dependency for all PythonPackages anyway? How do we prevent the dependency loop that would occur for py-setuptools depending on itself?

@citibeth
Copy link
Copy Markdown
Member

citibeth commented Jan 19, 2017 via email

@adamjstewart adamjstewart force-pushed the features/python-tests branch from 0f939c8 to 5aa9e5e Compare January 19, 2017 16:44
@adamjstewart
Copy link
Copy Markdown
Member Author

Ok, I implemented the simplest case. If import_modules is defined in a package, Spack will try to import those modules after installation. I'm not particularly fond of the name, import_modules, so if anyone has any better suggestions, let me know.

@adamjstewart
Copy link
Copy Markdown
Member Author

Should I run:

python -c 'import setuptools'

or:

python -c 'from setuptools import *'

I'm not sure if the latter will catch more problems. I'm pretty sure I've been able to import matplotlib but not able to import matplotlib.pyplot before.

@citibeth
Copy link
Copy Markdown
Member

citibeth commented Jan 19, 2017 via email

@adamjstewart
Copy link
Copy Markdown
Member Author

@alalazo How do I override default tests? In PythonPackage, I essentially have:

@PackageBase.sanity_check('build')                                          
@PackageBase.on_package_attributes(run_tests=True)                          
def test(self):
    self.setup_py('test')

However, this doesn't work for numpy. In the py-numpy package, I tried overriding it with:

@PythonPackage.sanity_check('build')                                          
@PythonPackage.on_package_attributes(run_tests=True)                          
def test(self):
    pass

but it is still running python setup.py test. Any ideas?

@alalazo
Copy link
Copy Markdown
Member

alalazo commented Jan 20, 2017

@adamjstewart In AutotoolsPackage and CMakePackage I call a function check indirectly, so that you can easily override just that. Maybe the right thing to do would be to push _run_default_function up the hierarchy, but I need to have a look how to do it cleanly. If you want I may try that in #2860.

@adamjstewart
Copy link
Copy Markdown
Member Author

@alalazo I tried that too. With:

@PackageBase.sanity_check('build')                                          
@PackageBase.on_package_attributes(run_tests=True)                          
def test(self):
    ...

in PythonPackage and:

def test(self):
    pass

in py-numpy, it still tries to run the test in PythonPackage.

@alalazo
Copy link
Copy Markdown
Member

alalazo commented Jan 20, 2017

@adamjstewart You miss indirection. In AutotoolsPackage I always call _run_default_function which does:

fn = getattr(self, 'check')

At that point you can override check everywhere in the hierarchy: the first match in the MRO of self is the one that gets called.

@adamjstewart
Copy link
Copy Markdown
Member Author

Thanks @alalazo, I didn't notice that. I wonder if there's a better way to do that... Well, I can't think of one off the top of my head, so I'll steal your _run_default_function from AutotoolsPackage and roll with it.

@adamjstewart
Copy link
Copy Markdown
Member Author

Now that I think about it, the easiest way to not have to use something like _run_default_function would be my suggestion here. I know you weren't too keen on it, but it might make our lives easier.

@alalazo
Copy link
Copy Markdown
Member

alalazo commented Jan 20, 2017

@adamjstewart The easiest imho is to have only one version of _run_default_function in PackageBase, and register that function with the appropriate phase downstream in the hierarchy. At that point you just need a line in PythonPackage to use it.

@adamjstewart
Copy link
Copy Markdown
Member Author

@alalazo Why did you do a try/except for that method? Was it before you added a default check method in AutotoolsPackage? It seems like some sort of check method will always exist.

@alalazo
Copy link
Copy Markdown
Member

alalazo commented Jan 20, 2017

Why did you do a try/except for that method? Was it before you added a default check method in AutotoolsPackage?

I think your guess is right, if I remember well how it evolved. But I would leave it like it is: extra safety is always welcome! 😄

# >>> import setuptools
# >>> setuptools.find_packages()
#
# in the source tarball directory
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Is there some way to associate a docstring with a particular attribute in a class? I would like this information to show up in the API docs.

'scipy.sparse.linalg.eigen', 'scipy.sparse.linalg.isolve',
'scipy.sparse.linalg.eigen.arpack', 'scipy.sparse.linalg.eigen.lobpcg',
'scipy.special._precompute'
]
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Is there a better way to do this? This list is the output of:

>>> import setuptools
>>> setuptools.find_packages()


# FIXME: Is this really necessary?
# Known not to work with 2.23, 2.25
# depends_on('[email protected]:', type='build')
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We've discussed this before, but things like binutils and curl aren't really dependencies of any particular package. They are necessary for every package in Spack. In a way, they are Spack dependencies. The only reason people have been adding them is that certain packages require newer versions of these dependencies than are found on their ancient systems. We added a note about this in the documentation. I think this dependency should be removed. It won't even build on macOS.

# scipy from its source directory; please exit the scipy
# source tree, and relaunch your python interpreter from there.
with working_dir('..'):
python('-c', 'import scipy; scipy.test("full", verbose=2)')
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

By default, numpy and scipy run a quick test suite. If you pass the "full" label, they run the full test suite. This can take quite a long time (17 minutes for scipy on my Mac), but the good news is that ALL THE TESTS PASS! Should we run the full test suite or just the quick test suite? Neither will be run unless the user passes --run-tests anyway.

Copy link
Copy Markdown
Member

@alalazo alalazo Jan 21, 2017

Choose a reason for hiding this comment

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

My vote is for full test suite if --run-tests is passed.

# FIXME: Is there any way around this problem?
# depends_on('py-pytest-flake8', type='test')
# depends_on('[email protected]:', type='test')
# depends_on('py-mock', when='^python@:3.2', type='test')
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

One constraint I want to put on the 'test' deptype is that both of the following commands would install the same package with the same hash:

spack install py-setuptools
spack install --run-tests py-setuptools

An unfortunate consequence of this is that we have no means of working around dependency loops. We can't do something similar to #2548 if we want to maintain this constraint, because the test dependency would have the same spec as the package that indirectly depends on it.

I don't think there is any good way around this problem. Let me now if you can think of anything better though.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

P.S. The package tests that come with setuptools don't even pass, even when pip installs all of the required dependencies. The annoying thing about this is that if a user runs spack install --run-tests py-numpy, it will always fail when trying to build py-setuptools. Also, even if they did succeed, I'm worried that spack activate will copy these test dependencies to the Python installation, causing conflicts when trying to activate py-pytest or py-mock separately.

@adamjstewart adamjstewart mentioned this pull request Feb 8, 2017
@adamjstewart adamjstewart changed the title [WIP] PythonPackage: Let There Be Tests! PythonPackage: Let There Be Tests! Feb 20, 2017
@adamjstewart adamjstewart removed the WIP label Feb 20, 2017
@adamjstewart
Copy link
Copy Markdown
Member Author

@tgamblin I think this is ready for review. It won't work correctly until a test deptype is added, but it makes a lot of other modifications to numpy and scipy that I would like to get merged. Otherwise, I'll end up duplicating them in another PR.

@adamjstewart
Copy link
Copy Markdown
Member Author

@alalazo I just updated this to use your new run_after('build')(PackageBase._run_default_build_time_test_callbacks) logic. Can you take a look and make sure I did it correctly? I wasn't able to test this as my db appears to be corrupted (or incompatible with this version of Spack).

@adamjstewart
Copy link
Copy Markdown
Member Author

Moved some of the unrelated changes to #3195 to make this PR easier to review. I'll remove them from this PR in a minute.

This was referenced Feb 20, 2017
@adamjstewart
Copy link
Copy Markdown
Member Author

Ping @tgamblin. This PR won't be incredibly useful until we have a test deptype, but it shouldn't break anything if we merge this now. Then we'll at least have import tests for Python packages.

@tgamblin tgamblin merged commit bc40453 into spack:develop Mar 31, 2017
@adamjstewart adamjstewart deleted the features/python-tests branch March 31, 2017 20:48
diaena pushed a commit to diaena/spack that referenced this pull request May 26, 2017
* Run python setup.py test if --run-tests
* Attempt to import the Python module after installation
* Add testing support to numpy and scipy
* Remove duplicated comments
* Update to new run-tests callback methodology
* Remove unrelated changes for another PR
xavierandrade pushed a commit to xavierandrade/spack that referenced this pull request Jun 16, 2017
* Run python setup.py test if --run-tests
* Attempt to import the Python module after installation
* Add testing support to numpy and scipy
* Remove duplicated comments
* Update to new run-tests callback methodology
* Remove unrelated changes for another PR
EmreAtes pushed a commit to EmreAtes/spack that referenced this pull request Jul 10, 2017
* Run python setup.py test if --run-tests
* Attempt to import the Python module after installation
* Add testing support to numpy and scipy
* Remove duplicated comments
* Update to new run-tests callback methodology
* Remove unrelated changes for another PR
amklinv pushed a commit that referenced this pull request Jul 17, 2017
* Run python setup.py test if --run-tests
* Attempt to import the Python module after installation
* Add testing support to numpy and scipy
* Remove duplicated comments
* Update to new run-tests callback methodology
* Remove unrelated changes for another PR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python tests General test capability(ies)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Proposal] Add sanity check to import Python modules

4 participants