Skip to content

Commit 8b4206e

Browse files
committed
Merge branch '1.3' into 1.4
2 parents 126a16a + d61c603 commit 8b4206e

File tree

7 files changed

+76
-6
lines changed

7 files changed

+76
-6
lines changed

.travis.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ python:
1616
matrix:
1717
allow_failures:
1818
- python: "3.8-dev"
19+
# WHY does this have to be in before_install and not install? o_O
20+
before_install:
21+
# Used by 'inv regression' (more performant/safe/likely to expose real issues
22+
# than in-Python threads...)
23+
- sudo apt-get -y install parallel
1924
install:
2025
# For some reason Travis' build envs have wildly different pip/setuptools
2126
# versions between minor Python versions, and this can cause many hilarious
@@ -45,6 +50,8 @@ before_script:
4550
script:
4651
# Execute full test suite + coverage, as the new sudo-capable user
4752
- inv travis.sudo-coverage
53+
# Perform extra "not feasible inside pytest for no obvious reason" tests
54+
- inv regression
4855
# Websites build OK? (Not on PyPy3, Sphinx is all "who the hell are you?" =/
4956
- "if [[ $TRAVIS_PYTHON_VERSION != 'pypy3' ]]; then inv sites; fi"
5057
# Doctests in websites OK? (Same caveat as above...)

integration/_support/regression.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""
2+
Barebones regression-catching script that looks for ephemeral run() failures.
3+
4+
Intended to be run from top level of project via ``inv regression``. In an
5+
ideal world this would be truly part of the integration test suite, but:
6+
7+
- something about the outer invoke or pytest environment seems to prevent such
8+
issues from appearing reliably (see eg issue #660)
9+
- it can take quite a while to run, even compared to other integration tests.
10+
"""
11+
12+
13+
import sys
14+
15+
from invoke import task
16+
17+
18+
@task
19+
def check(c):
20+
count = 0
21+
failures = []
22+
for _ in range(0, 1000):
23+
count += 1
24+
try:
25+
# 'ls' chosen as an arbitrary, fast-enough-for-looping but
26+
# does-some-real-work example (where eg 'sleep' is less useful)
27+
response = c.run("ls", hide=True)
28+
if not response.ok:
29+
failures.append(response)
30+
except Exception as e:
31+
failures.append(e)
32+
if failures:
33+
print("run() FAILED {}/{} times!".format(len(failures), count))
34+
sys.exit(1)

integration/runners.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,12 @@ def nonpty_subproc_should_not_hang_if_IO_thread_has_an_exception(self):
126126

127127
class timeouts:
128128
def does_not_fire_when_command_quick(self):
129-
assert Local(Context()).run("sleep 1", timeout=5)
129+
assert run("sleep 1", timeout=5)
130130

131131
def triggers_exception_when_command_slow(self):
132132
before = time.time()
133133
with raises(CommandTimedOut) as info:
134-
Local(Context()).run("sleep 5", timeout=0.5)
134+
run("sleep 5", timeout=0.5)
135135
after = time.time()
136136
# Fudge real time check a bit, <=0.5 typically fails due to
137137
# overhead etc. May need raising further to avoid races? Meh.

invoke/util.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,7 @@ def is_dead(self):
271271
# NOTE: it seems highly unlikely that a thread could still be
272272
# is_alive() but also have encountered an exception. But hey. Why not
273273
# be thorough?
274-
alive = self.is_alive() and (self.exc_info is None)
275-
return not alive
274+
return (not self.is_alive()) and self.exc_info is not None
276275

277276
def __repr__(self):
278277
# TODO: beef this up more

sites/www/changelog.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
Changelog
33
=========
44

5+
- :bug:`660` Fix an issue with `~invoke.run` & friends having intermittent
6+
problems at exit time (symptom was typically about the exit code value being
7+
``None`` instead of an integer; often with an exception trace). Thanks to
8+
Frank Lazzarini for the report and to the numerous others who provided
9+
reproduction cases.
510
- :bug:`518` Close pseudoterminals opened by the `~invoke.runners.Local` class
611
during ``run(..., pty=True)``. Previously, these were only closed
712
incidentally at process shutdown, causing file descriptor leakage in

tasks.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,23 @@ def coverage(c, report="term", opts=""):
7272
return coverage_(c, report=report, opts=opts, tester=test)
7373

7474

75+
@task
76+
def regression(c, jobs=8):
77+
"""
78+
Run an expensive, hard-to-test-in-pytest run() regression checker.
79+
80+
:param int jobs: Number of jobs to run, in total. Ideally num of CPUs.
81+
"""
82+
os.chdir("integration/_support")
83+
cmd = "seq {} | parallel -n0 --halt=now,fail=1 inv -c regression check"
84+
c.run(cmd.format(jobs))
85+
86+
7587
ns = Collection(
7688
test,
7789
coverage,
7890
integration,
91+
regression,
7992
vendorize,
8093
release,
8194
www,

tests/concurrency.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,20 @@ def catches_exceptions(self):
3232
assert isinstance(wrapper.value, AttributeError)
3333

3434
def exhibits_is_dead_flag(self):
35+
# Spin up a thread that will except internally (can't put() on a
36+
# None object)
3537
t = EHThread(target=self.worker, args=[None])
3638
t.start()
3739
t.join()
40+
# Excepted -> it's dead
3841
assert t.is_dead
42+
# Spin up a happy thread that can exit peacefully (it's not "dead",
43+
# though...maybe we should change that terminology)
3944
t = EHThread(target=self.worker, args=[Queue()])
4045
t.start()
4146
t.join()
42-
assert t.is_dead
47+
# Not dead, just uh...sleeping?
48+
assert not t.is_dead
4349

4450
class via_subclassing:
4551
def setup(self):
@@ -73,11 +79,17 @@ def catches_exceptions(self):
7379
assert isinstance(wrapper.value, AttributeError)
7480

7581
def exhibits_is_dead_flag(self):
82+
# Spin up a thread that will except internally (can't put() on a
83+
# None object)
7684
t = self.klass(queue=None)
7785
t.start()
7886
t.join()
87+
# Excepted -> it's dead
7988
assert t.is_dead
89+
# Spin up a happy thread that can exit peacefully (it's not "dead",
90+
# though...maybe we should change that terminology)
8091
t = self.klass(queue=Queue())
8192
t.start()
8293
t.join()
83-
assert t.is_dead
94+
# Not dead, just uh...sleeping?
95+
assert not t.is_dead

0 commit comments

Comments
 (0)