Index: .travis.yml
===================================================================
--- .travis.yml	(revision 2d09df1a016d5aba6f689a4b863e9e4314756ac0)
+++ .travis.yml	(revision a69d8cd7ece8a1df933b123d2e032197783d80b4)
@@ -37,5 +37,6 @@
 - conda update --yes conda
 - conda info -a
-- conda install --yes python=$PY numpy scipy cython mako cffi
+# 2018-01-26 PAK: sasmodels uses yield generator for tests, which isn't supported in pytest 4+
+- conda install --yes python=$PY numpy scipy cython mako cffi "pytest<4"
 install:
 - pip install bumps
@@ -43,5 +44,6 @@
 script:
 - python --version
-- python -m sasmodels.model_test -v dll all
+#- python -m sasmodels.model_test -v dll all
+- python -m pytest -v
 before_deploy:
 - echo -e "Host danse.chem.utk.edu\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config
Index: appveyor.yml
===================================================================
--- appveyor.yml	(revision fefc185ba4c27aefa57a3fb46c7866f0a7a0d4c9)
+++ appveyor.yml	(revision a69d8cd7ece8a1df933b123d2e032197783d80b4)
@@ -44,5 +44,5 @@
 
 install:
-    # Set the CONDA_NPY, although it has no impact on the actual build. 
+    # Set the CONDA_NPY, although it has no impact on the actual build.
     # We need this because of a test within conda-build.
     - cmd: set CONDA_NPY=19
@@ -68,5 +68,6 @@
     #- cmd: conda install --yes --quiet obvious-ci
     # 2018-01-19 PAK: skipping toolchain cython and cffi (these were for pyopencl?)
-    - cmd: conda install --yes --quiet numpy scipy
+    # 2018-01-26 PAK: sasmodels uses yield generator for tests, which isn't supported in pytest 4+
+    - cmd: conda install --yes --quiet numpy scipy "pytest<4"
     # 2018-01-19 PAK: skipping pyopencl; this would be needed for deploy but maybe not for test
     #- cmd: conda install --yes --channel conda-forge pyopencl
@@ -86,3 +87,5 @@
     #- "%CMD_IN_ENV% python -m sasmodels.model_test dll all"
     #- cmd /E:ON /V:ON /C obvci_appveyor_python_build_env.cmd python -m sasmoels.model_test dll all
-    - python -m sasmodels.model_test dll all
+    #- python -m sasmodels.model_test dll all
+    #- nosetests -v sasmodels/*.py
+    - python -m pytest -v
Index: conftest.py
===================================================================
--- conftest.py	(revision a69d8cd7ece8a1df933b123d2e032197783d80b4)
+++ conftest.py	(revision a69d8cd7ece8a1df933b123d2e032197783d80b4)
@@ -0,0 +1,65 @@
+"""
+py.test hooks for sasmodels
+
+Hooks for running sasmodels tests via py.test.
+
+*pytest_collection_modifyitems* adds the test description to the end of
+the test name.  This is needed for the generated list of tests is sasmodels,
+where each test has a description giving the name of the model.  For example
+"model_tests::[3]" becomes "model_tests::[3]::bcc_paracrystal-dll".  Need to
+leave the "::[3]" in the name since that is the way you can indicate this
+test specifically from the py.test command line. [This is perhaps because
+the modifyitems hook is only called after test selection.]
+"""
+from __future__ import print_function
+
+import pytest
+from _pytest.unittest import TestCaseFunction
+
+USE_DOCSTRING_AS_DESCRIPTION = True
+def pytest_collection_modifyitems(session, config, items):
+    """
+    Add description to the test node id if item is a function and function
+    has a description attribute or __doc__ attribute.
+    """
+    for item in items:
+        #print(item.nodeid, type(item))
+        #for attr in dir(item): not attr.startswith('__') and print(attr, getattr(item, attr))
+        if isinstance(item, pytest.Function):
+            if isinstance(item, TestCaseFunction):
+                # TestCase uses item.name to find the method so skip
+                continue
+            function = item.obj
+
+            # If the test case provides a "description" attribute then use it
+            # as an extended description.  If there is no description attribute,
+            # then perhaps use the test docstring.
+            if USE_DOCSTRING_AS_DESCRIPTION:
+                description = getattr(function, 'description', function.__doc__)
+            else:
+                description = getattr(function, 'description', "")
+
+            # If description is not supplied but yield args are, then use the
+            # yield args for the description
+            if not description and getattr(item, '_args', ()):
+                description = str(item._args) if len(item._args) > 1 else str(item._args[0])
+            #print(item.nodeid, description, item._args)
+
+            if description:
+                # Strip spaces from start and end and strip dots from end
+                # pytest converts '.' to '::' on output for some reason.
+                description = description.strip().rstrip('.')
+                # Join multi-line descriptions into a single line
+                if '\n' in description:
+                    description = " ".join(line.strip() for line in description.split('\n'))
+
+            # Set the description as part of the node identifier.
+            if description:
+                #print(type(item), dir(item))
+                #print(item.nodeid, description)
+                #print(item.location, item.name, item.nodeid, item.originalname)
+                # Note: leave the current name mostly as-is since the prefix
+                # is needed to specify the nth test from a list of tests.
+                #print("updating with", description)
+                item.name += "::" + description
+            #print("=>", item.nodeid)
Index: pytest.ini
===================================================================
--- pytest.ini	(revision a69d8cd7ece8a1df933b123d2e032197783d80b4)
+++ pytest.ini	(revision a69d8cd7ece8a1df933b123d2e032197783d80b4)
@@ -0,0 +1,7 @@
+[pytest]
+addopts = --doctest-modules
+doctest_optionflags = ELLIPSIS
+testpaths = sasmodels
+python_files = *.py
+python_classes = NoClassTestsWillMatch
+python_functions = *_test test_* model_tests
Index: sasmodels/convert.py
===================================================================
--- sasmodels/convert.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/convert.py	(revision a69d8cd7ece8a1df933b123d2e032197783d80b4)
@@ -633,6 +633,5 @@
 def test_backward_forward():
     from .core import list_models
+    L = lambda name: _check_one(name, seed=1)
     for name in list_models('all'):
-        L = lambda: _check_one(name, seed=1)
-        L.description = name
-        yield L
+        yield L, name
Index: sasmodels/core.py
===================================================================
--- sasmodels/core.py	(revision 7a516d0ce65f7da1dba6a83326e7d52182461976)
+++ sasmodels/core.py	(revision a69d8cd7ece8a1df933b123d2e032197783d80b4)
@@ -320,47 +320,54 @@
     print("\n".join(list_models(kind)))
 
-def test_load():
-    # type: () -> None
-    """Check that model load works"""
+def test_composite_order():
     def test_models(fst, snd):
         """Confirm that two models produce the same parameters"""
         fst = load_model(fst)
         snd = load_model(snd)
-        # Remove the upper case characters frin the parameters, since
-        # they lasndel the order of events and we specfically are
-        # changin that aspect
+        # Un-disambiguate parameter names so that we can check if the same
+        # parameters are in a pair of composite models. Since each parameter in
+        # the mixture model is tagged as e.g., A_sld, we ought to use a
+        # regex subsitution s/^[A-Z]+_/_/, but removing all uppercase letters
+        # is good enough.
         fst = [[x for x in p.name if x == x.lower()] for p in fst.info.parameters.kernel_parameters]
         snd = [[x for x in p.name if x == x.lower()] for p in snd.info.parameters.kernel_parameters]
         assert sorted(fst) == sorted(snd), "{} != {}".format(fst, snd)
 
-
-    test_models(
+    def build_test(first, second):
+        test = lambda description: test_models(first, second)
+        description = first + " vs. " + second
+        return test, description
+
+    yield build_test(
         "cylinder+sphere",
         "sphere+cylinder")
-    test_models(
+    yield build_test(
         "cylinder*sphere",
         "sphere*cylinder")
-    test_models(
+    yield build_test(
         "cylinder@hardsphere*sphere",
         "sphere*cylinder@hardsphere")
-    test_models(
+    yield build_test(
         "barbell+sphere*cylinder@hardsphere",
         "sphere*cylinder@hardsphere+barbell")
-    test_models(
+    yield build_test(
         "barbell+cylinder@hardsphere*sphere",
         "cylinder@hardsphere*sphere+barbell")
-    test_models(
+    yield build_test(
         "barbell+sphere*cylinder@hardsphere",
         "barbell+cylinder@hardsphere*sphere")
-    test_models(
+    yield build_test(
         "sphere*cylinder@hardsphere+barbell",
         "cylinder@hardsphere*sphere+barbell")
-    test_models(
+    yield build_test(
         "barbell+sphere*cylinder@hardsphere",
         "cylinder@hardsphere*sphere+barbell")
-    test_models(
+    yield build_test(
         "barbell+cylinder@hardsphere*sphere",
         "sphere*cylinder@hardsphere+barbell")
 
+def test_composite():
+    # type: () -> None
+    """Check that model load works"""
     #Test the the model produces the parameters that we would expect
     model = load_model("cylinder@hardsphere*sphere")
Index: sasmodels/model_test.py
===================================================================
--- sasmodels/model_test.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/model_test.py	(revision a69d8cd7ece8a1df933b123d2e032197783d80b4)
@@ -123,5 +123,5 @@
 
         if is_py:  # kernel implemented in python
-            test_name = "Model: %s, Kernel: python"%model_name
+            test_name = "%s-python"%model_name
             test_method_name = "test_%s_python" % model_info.id
             test = ModelTestCase(test_name, model_info,
@@ -135,5 +135,5 @@
             # test using dll if desired
             if 'dll' in loaders or not core.HAVE_OPENCL:
-                test_name = "Model: %s, Kernel: dll"%model_name
+                test_name = "%s-dll"%model_name
                 test_method_name = "test_%s_dll" % model_info.id
                 test = ModelTestCase(test_name, model_info,
@@ -146,5 +146,5 @@
             # test using opencl if desired and available
             if 'opencl' in loaders and core.HAVE_OPENCL:
-                test_name = "Model: %s, Kernel: OpenCL"%model_name
+                test_name = "%s-opencl"%model_name
                 test_method_name = "test_%s_opencl" % model_info.id
                 # Using dtype=None so that the models that are only
@@ -469,13 +469,9 @@
     loaders = ['opencl', 'dll'] if core.HAVE_OPENCL else ['dll']
     tests = make_suite(loaders, ['all'])
-    for test_i in tests:
-        # In order for nosetest to see the correct test name, need to set
-        # the description attribute of the returned function.  Since we
-        # can't do this for the returned instance, wrap it in a lambda and
-        # set the description on the lambda.  Otherwise we could just do:
-        #    yield test_i.run_all
-        L = lambda: test_i.run_all()
-        L.description = test_i.test_name
-        yield L
+    for test in tests:
+        # In order for nosetest to show the test name, wrap the test.run_all
+        # instance in function that takes the test name as a parameter which
+        # will be displayed when the test is run.
+        yield lambda name: test.run_all(), test.test_name
 
 
Index: sasmodels/sasview_model.py
===================================================================
--- sasmodels/sasview_model.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/sasview_model.py	(revision a69d8cd7ece8a1df933b123d2e032197783d80b4)
@@ -859,5 +859,5 @@
     # type: () -> None
     """
-    Load and run cylinder model from sas.models.CylinderModel
+    Load and run cylinder model as sas-models-CylinderModel
     """
     if not SUPPORT_OLD_STYLE_PLUGINS:
