Repository: scikit-image/scikit-image

Issue:
travis has random failures on rank filters
## Description

See for ex: https://travis-ci.org/scikit-image/scikit-image/jobs/563363217


## Way to reproduce
```python
# Place the full code we need to recreate your issue here
# upload all necessary images to github too!
```


## Version information
```python
# Paste the output of the following python commands
from __future__ import print_function
import sys; print(sys.version)
import platform; print(platform.platform())
import skimage; print("scikit-image version: {}".format(skimage.__version__))
import numpy; print("numpy version: {}".format(numpy.__version__))
```

```python
# your output here

```




Discussion:
@sciunto this is incredibly surprising behaviour???? But I have now seen it with my own eyes so I have no choice but to believe it. Does anyone @scikit-image/core have any ideas???
Yeah. I think it is because warning filters are not thread safe. 

There is something about filters being added on a stack, and that the stack can get wildly out of control if multiple threads are touching it.

We may want to use a fixture to disable warnings all together during the multithreaded tests ( I think rank uses that crazy multi threaded stuff right)

Resolving patch:
diff --git a/skimage/_shared/_warnings.py b/skimage/_shared/_warnings.py
--- a/skimage/_shared/_warnings.py
+++ b/skimage/_shared/_warnings.py
@@ -79,8 +79,9 @@ def expected_warnings(matching):
 
     Parameters
     ----------
-    matching : list of strings or compiled regexes
+    matching : None or a list of strings or compiled regexes
         Regexes for the desired warning to catch
+        If matching is None, this behaves as a no-op.
 
     Examples
     --------
@@ -112,6 +113,11 @@ def expected_warnings(matching):
         raise ValueError('``matching`` should be a list of strings and not '
                          'a string itself.')
 
+    # Special case for disabling the context manager
+    if matching is None:
+        yield None
+        return
+
     strict_warnings = os.environ.get('SKIMAGE_TEST_STRICT_WARNINGS', '1')
     if strict_warnings.lower() == 'true':
         strict_warnings = True


Test patch:
diff --git a/skimage/_shared/testing.py b/skimage/_shared/testing.py
--- a/skimage/_shared/testing.py
+++ b/skimage/_shared/testing.py
@@ -22,6 +22,7 @@
 from .. import data, io
 from ..util import img_as_uint, img_as_float, img_as_int, img_as_ubyte
 import pytest
+from ._warnings import expected_warnings
 
 
 SKIP_RE = re.compile(r"(\s*>>>.*?)(\s*)#\s*skip\s+if\s+(.*)$")
@@ -209,7 +210,7 @@ def teardown_test():
     warnings.simplefilter('default')
 
 
-def test_parallel(num_threads=2):
+def test_parallel(num_threads=2, warnings_matching=None):
     """Decorator to run the same function multiple times in parallel.
 
     This decorator is useful to ensure that separate threads execute
@@ -220,6 +221,12 @@ def test_parallel(num_threads=2):
     num_threads : int, optional
         The number of times the function is run in parallel.
 
+    warnings_matching: list or None
+        This parameter is passed on to `expected_warnings` so as not to have
+        race conditions with the warnings filters. A single
+        `expected_warnings` context manager is used for all threads.
+        If None, then no warnings are checked.
+
     """
 
     assert num_threads > 0
@@ -227,20 +234,21 @@ def test_parallel(num_threads=2):
     def wrapper(func):
         @functools.wraps(func)
         def inner(*args, **kwargs):
-            threads = []
-            for i in range(num_threads - 1):
-                thread = threading.Thread(target=func, args=args,
-                                          kwargs=kwargs)
-                threads.append(thread)
-            for thread in threads:
-                thread.start()
+            with expected_warnings(warnings_matching):
+                threads = []
+                for i in range(num_threads - 1):
+                    thread = threading.Thread(target=func, args=args,
+                                              kwargs=kwargs)
+                    threads.append(thread)
+                for thread in threads:
+                    thread.start()
 
-            result = func(*args, **kwargs)
+                result = func(*args, **kwargs)
 
-            for thread in threads:
-                thread.join()
+                for thread in threads:
+                    thread.join()
 
-            return result
+                return result
 
         return inner
 
diff --git a/skimage/_shared/tests/test_testing.py b/skimage/_shared/tests/test_testing.py
--- a/skimage/_shared/tests/test_testing.py
+++ b/skimage/_shared/tests/test_testing.py
@@ -5,6 +5,10 @@
 from numpy.testing import assert_equal
 from skimage._shared.testing import doctest_skip_parser, test_parallel
 from skimage._shared import testing
+import pytest
+
+from skimage._shared._warnings import expected_warnings
+from warnings import warn
 
 
 def test_skipper():
@@ -97,5 +101,29 @@ def change_state3():
     assert len(state) == 6
 
 
+def test_parallel_warning():
+    @test_parallel()
+    def change_state_warns_fails():
+        warn("Test warning for test parallel", stacklevel=2)
+
+    with expected_warnings(['Test warning for test parallel']):
+        change_state_warns_fails()
+
+    @test_parallel(warnings_matching=['Test warning for test parallel'])
+    def change_state_warns_passes():
+        warn("Test warning for test parallel", stacklevel=2)
+
+    change_state_warns_passes()
+
+
+def test_expected_warnings_noop():
+    # This will ensure the line beolow it behaves like a no-op
+    with expected_warnings(['Expected warnings test']):
+
+        # This should behave as a no-op
+        with expected_warnings(None):
+            warn('Expected warnings test')
+
+
 if __name__ == '__main__':
     np.testing.run_module_suite()
diff --git a/skimage/filters/rank/tests/test_rank.py b/skimage/filters/rank/tests/test_rank.py
--- a/skimage/filters/rank/tests/test_rank.py
+++ b/skimage/filters/rank/tests/test_rank.py
@@ -56,11 +56,10 @@ def setup(self):
 
     @parametrize('filter', all_rank_filters)
     def test_rank_filter(self, filter):
-        @test_parallel()
+        @test_parallel(warnings_matching=['Possible precision loss'])
         def check():
             expected = self.refs[filter]
-            with expected_warnings(['Possible precision loss']):
-                result = getattr(rank, filter)(self.image, self.selem)
+            result = getattr(rank, filter)(self.image, self.selem)
             if filter == "entropy":
                 # There may be some arch dependent rounding errors
                 # See the discussions in