Fix real pin tests ... and some other bits

The real pin tests were broken by the new factory stuff. This commit
fixes them up, and fixes up a few other bits besides (like why the
pigpio PWM tests were failing, why RPi.GPIO sometimes segfaulted on PWM
tests, etc.)

It also causes the real pin tests to run against MockPin (thanks to
@lurch for the suggestion!). This required some tweaks to MockPin to
make it emulate physically pulled up pins itself (which in turn
necessitated changing quite a few pin numbers in the main test suite
because we were using 2 and 3 everywhere), and to allow one MockPin to
drive another. Anyway, everything's working now including all the tests
on a Pi (haven't tried RPIO yet, but only because I'm on a Pi3 -
everything else works with overall coverage of 88% :).
This commit is contained in:
Dave Jones
2016-10-22 22:28:33 +01:00
parent 4049ef5094
commit 2495939603
9 changed files with 265 additions and 159 deletions

View File

@@ -11,20 +11,33 @@ except NameError:
pass
import io
import os
import subprocess
from time import sleep
import pytest
import pkg_resources
from gpiozero import PinFixedPull, PinInvalidPull, PinInvalidFunction
from gpiozero import (
PinFixedPull,
PinInvalidPull,
PinInvalidFunction,
PinPWMUnsupported,
Device,
)
from gpiozero.pins.mock import MockConnectedPin, MockFactory
try:
from math import isclose
except ImportError:
from gpiozero.compat import isclose
# This module assumes you've wired the following GPIO pins together
TEST_PIN = 22
INPUT_PIN = 27
# This module assumes you've wired the following GPIO pins together. The pins
# can be re-configured via the listed environment variables (useful for when
# your testing rig requires different pins because the defaults interfere with
# attached hardware).
TEST_PIN = int(os.getenv('GPIOZERO_TEST_PIN', '22'))
INPUT_PIN = int(os.getenv('GPIOZERO_INPUT_PIN', '27'))
# Skip the entire module if we're not on a Pi
@@ -40,51 +53,48 @@ pytestmark = pytest.mark.skipif(not is_a_pi(), reason='tests cannot run on non-P
del is_a_pi
# Try and import as many pin libraries as possible
PIN_CLASSES = []
try:
from gpiozero.pins.rpigpio import RPiGPIOPin
PIN_CLASSES.append(RPiGPIOPin)
except ImportError:
RPiGPIOPin = None
try:
from gpiozero.pins.rpio import RPIOPin
PIN_CLASSES.append(RPIOPin)
except ImportError:
RPIOPin = None
try:
from gpiozero.pins.pigpiod import PiGPIOPin
PIN_CLASSES.append(PiGPIOPin)
except ImportError:
PiGPIOPin = None
try:
from gpiozero.pins.native import NativePin
PIN_CLASSES.append(NativePin)
except ImportError:
NativePin = None
@pytest.fixture(
scope='module',
params=('rpigpio',))
#params=pkg_resources.get_distribution('gpiozero').get_entry_map('gpiozero_pin_factories').keys())
def pin_factory(request):
# Constructs each pin factory in turn with some extra logic to ensure
# the pigpiod daemon gets started and stopped around the pigpio factory
if request.param == 'pigpio':
try:
subprocess.check_call(['sudo', 'systemctl', 'start', 'pigpiod'])
except subprocess.CalledProcessError:
pytest.skip("skipped factory pigpio: failed to start pigpiod")
try:
factory = pkg_resources.load_entry_point('gpiozero', 'gpiozero_pin_factories', request.param)()
except Exception as e:
if request.param == 'pigpio':
subprocess.check_call(['sudo', 'systemctl', 'stop', 'pigpiod'])
pytest.skip("skipped factory %s: %s" % (request.param, str(e)))
else:
Device._set_pin_factory(factory)
def fin():
Device._set_pin_factory(MockFactory())
if request.param == 'pigpio':
subprocess.check_call(['sudo', 'systemctl', 'stop', 'pigpiod'])
request.addfinalizer(fin)
return factory
@pytest.fixture(scope='module', params=PIN_CLASSES)
def pin_class(request):
# pigpiod needs to be running for PiGPIOPin
if request.param.__name__ == 'PiGPIOPin':
subprocess.check_call(['sudo', 'pigpiod'])
# Unfortunately, pigpiod provides no option for running in the
# foreground, so we have to use the sledgehammer of killall to get shot
# of it
def kill_pigpiod():
subprocess.check_call(['sudo', 'killall', 'pigpiod'])
request.addfinalizer(kill_pigpiod)
return request.param
@pytest.fixture(scope='function')
def pins(request, pin_class):
def pins(request, pin_factory):
# Why return both pins in a single fixture? If we defined one fixture for
# each pin then pytest will (correctly) test RPiGPIOPin(22) against
# NativePin(27) and so on. This isn't supported, so we don't test it
test_pin = pin_class(TEST_PIN)
input_pin = pin_class(INPUT_PIN)
if pin_factory.__class__.__name__ == 'MockFactory':
test_pin = pin_factory.pin(TEST_PIN, pin_class=MockConnectedPin)
else:
test_pin = pin_factory.pin(TEST_PIN)
input_pin = pin_factory.pin(INPUT_PIN)
input_pin.function = 'input'
input_pin.pull = 'down'
if pin_factory.__class__.__name__ == 'MockFactory':
test_pin.input_pin = input_pin
def fin():
test_pin.close()
input_pin.close()
@@ -134,17 +144,18 @@ def test_pull_bad(pins):
with pytest.raises(PinInvalidPull):
test_pin.input_with_pull('foo')
def test_pull_down_warning(pin_class):
# XXX This assumes we're on a vaguely modern Pi and not a compute module
# Might want to refine this with the pi-info database
pin = pin_class(2)
try:
with pytest.raises(PinFixedPull):
pin.pull = 'down'
with pytest.raises(PinFixedPull):
pin.input_with_pull('down')
finally:
pin.close()
def test_pull_down_warning(pin_factory):
if pin_factory.pi_info.pulled_up('GPIO2'):
pin = pin_factory.pin(2)
try:
with pytest.raises(PinFixedPull):
pin.pull = 'down'
with pytest.raises(PinFixedPull):
pin.input_with_pull('down')
finally:
pin.close()
else:
pytest.skip("GPIO2 isn't pulled up on this pi")
def test_input_with_pull(pins):
test_pin, input_pin = pins
@@ -153,25 +164,42 @@ def test_input_with_pull(pins):
test_pin.input_with_pull('down')
assert input_pin.state == 0
@pytest.mark.skipif(True, reason='causes segfaults')
def test_bad_duty_cycle(pins):
test_pin, input_pin = pins
if test_pin.__class__.__name__ == 'NativePin':
pytest.skip("native pin doesn't support PWM")
test_pin.function = 'output'
test_pin.frequency = 100
with pytest.raises(ValueError):
test_pin.state = 1.1
try:
# NOTE: There's some race in RPi.GPIO that causes a segfault if we
# don't pause before starting PWM; only seems to happen when stopping
# and restarting PWM very rapidly (i.e. between test cases).
if Device._pin_factory.__class__.__name__ == 'RPiGPIOFactory':
sleep(0.1)
test_pin.frequency = 100
except PinPWMUnsupported:
pytest.skip("%r doesn't support PWM" % test_pin.factory)
else:
try:
with pytest.raises(ValueError):
test_pin.state = 1.1
finally:
test_pin.frequency = None
def test_duty_cycles(pins):
test_pin, input_pin = pins
if test_pin.__class__.__name__ == 'NativePin':
pytest.skip("native pin doesn't support PWM")
test_pin.function = 'output'
test_pin.frequency = 100
for duty_cycle in (0.0, 0.1, 0.5, 1.0):
test_pin.state = duty_cycle
assert test_pin.state == duty_cycle
total = sum(input_pin.state for i in range(20000))
assert isclose(total / 20000, duty_cycle, rel_tol=0.1, abs_tol=0.1)
try:
# NOTE: see above
if Device._pin_factory.__class__.__name__ == 'RPiGPIOFactory':
sleep(0.1)
test_pin.frequency = 100
except PinPWMUnsupported:
pytest.skip("%r doesn't support PWM" % test_pin.factory)
else:
try:
for duty_cycle in (0.0, 0.1, 0.5, 1.0):
test_pin.state = duty_cycle
assert test_pin.state == duty_cycle
total = sum(input_pin.state for i in range(20000))
assert isclose(total / 20000, duty_cycle, rel_tol=0.1, abs_tol=0.1)
finally:
test_pin.frequency = None