File: test_real_pins.py

package info (click to toggle)
gpiozero 2.0.1-0.1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 17,192 kB
  • sloc: python: 15,355; makefile: 246
file content (464 lines) | stat: -rw-r--r-- 15,964 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
# vim: set fileencoding=utf-8:
#
# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins
#
# Copyright (c) 2016-2024 Dave Jones <dave@waveform.org.uk>
# Copyright (c) 2020 Fangchen Li <fangchen.li@outlook.com>
# Copyright (c) 2020 Andrew Scheller <github@loowis.durge.org>
#
# SPDX-License-Identifier: BSD-3-Clause

import os
import errno
import warnings
from time import time, sleep
from math import isclose
from unittest import mock

# NOTE: Remove try when compatibility moves beyond Python 3.10
try:
    from importlib_metadata import entry_points
except ImportError:
    from importlib.metadata import entry_points

import pytest

from gpiozero import *
from gpiozero.pins.mock import MockConnectedPin, MockFactory, MockSPIDevice
from gpiozero.pins.native import NativeFactory
from gpiozero.pins.local import LocalPiFactory, LocalPiHardwareSPI


# 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). Please note that the name specified *must* be the primary
# name of the pin, e.g. GPIO22 rather than an alias like BCM22 or 22 (several
# tests rely upon this).
TEST_PIN = os.environ.get('GPIOZERO_TEST_PIN', 'GPIO22')
INPUT_PIN = os.environ.get('GPIOZERO_TEST_INPUT_PIN', 'GPIO27')

# The lock path is intended to prevent parallel runs of the "real pins" test
# suite. For example, if you are testing multiple Python versions under tox,
# the mocked devices will all happily test in parallel. However, the real pins
# test must not or you risk one run trying to set a pin to an output while
# another simultaneously demands it's an input. The path specified here must be
# visible and accessible to all simultaneous runs.
TEST_LOCK = os.environ.get('GPIOZERO_TEST_LOCK', '/tmp/real_pins_lock')


def local_only():
    if not isinstance(Device.pin_factory, LocalPiFactory):
        pytest.skip("Test cannot run with non-local pin factories")

def local_hardware_spi_only(intf):
    if not isinstance(intf, LocalPiHardwareSPI):
        pytest.skip("Test assumes LocalPiHardwareSPI descendant")


with warnings.catch_warnings():
    @pytest.fixture(
        scope='module',
        params=[ep.name for ep in entry_points(group='gpiozero_pin_factories')])
    def pin_factory_name(request):
        return request.param


@pytest.fixture()
def pin_factory(request, pin_factory_name):
    try:
        with warnings.catch_warnings():
            eps = entry_points(group='gpiozero_pin_factories')
        for ep in eps:
            if ep.name == pin_factory_name:
                factory = ep.load()()
                break
        else:
            assert False, 'internal error'
    except Exception as e:
        pytest.skip("skipped factory {pin_factory_name}: {e!s}".format(
            pin_factory_name=pin_factory_name, e=e))
    else:
        yield factory
        factory.close()


@pytest.fixture()
def default_factory(request, pin_factory):
    save_pin_factory = Device.pin_factory
    Device.pin_factory = pin_factory
    yield pin_factory
    Device.pin_factory = save_pin_factory


@pytest.fixture(scope='function')
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
    assert not (
        {INPUT_PIN, TEST_PIN} & {
            'GPIO2',
            'GPIO3',
            'GPIO7',
            'GPIO8',
            'GPIO9',
            'GPIO10',
            'GPIO11',
        }), 'Cannot use SPI (7-11) or I2C (2-3) pins for tests'
    input_pin = pin_factory.pin(INPUT_PIN)
    input_pin.function = 'input'
    input_pin.pull = 'down'
    if isinstance(pin_factory, MockFactory):
        test_pin = pin_factory.pin(TEST_PIN, pin_class=MockConnectedPin, input_pin=input_pin)
    else:
        test_pin = pin_factory.pin(TEST_PIN)
    yield test_pin, input_pin
    test_pin.close()
    input_pin.close()


def setup_module(module):
    start = time()
    while True:
        if time() - start > 300:  # 5 minute timeout
            raise RuntimeError('timed out waiting for real pins lock')
        try:
            with open(TEST_LOCK, 'x') as f:
                f.write('Lock file for gpiozero real-pin tests; delete '
                        'this if the test suite is not currently running\n')
        except FileExistsError:
            print('Waiting for lock before testing real-pins')
            sleep(1)
        else:
            break


def teardown_module(module):
    os.unlink(TEST_LOCK)


def test_pin_names(pins):
    test_pin, input_pin = pins
    assert test_pin.info.name == TEST_PIN
    assert input_pin.info.name == INPUT_PIN


def test_function_bad(pins):
    test_pin, input_pin = pins
    with pytest.raises(PinInvalidFunction):
        test_pin.function = 'foo'


def test_output(pins):
    test_pin, input_pin = pins
    test_pin.function = 'output'
    test_pin.state = 0
    assert input_pin.state == 0
    test_pin.state = 1
    assert input_pin.state == 1


def test_output_with_state(pins):
    test_pin, input_pin = pins
    test_pin.output_with_state(0)
    assert input_pin.state == 0
    test_pin.output_with_state(1)
    assert input_pin.state == 1


def test_pull(pins):
    test_pin, input_pin = pins
    input_pin.pull = 'floating'
    test_pin.function = 'input'
    test_pin.pull = 'up'
    assert test_pin.state == 1
    assert input_pin.state == 1
    test_pin.pull = 'down'
    assert test_pin.state == 0
    assert input_pin.state == 0


def test_pull_bad(pins):
    test_pin, input_pin = pins
    test_pin.function = 'input'
    with pytest.raises(PinInvalidPull):
        test_pin.pull = 'foo'
    with pytest.raises(PinInvalidPull):
        test_pin.input_with_pull('foo')


def test_pull_down_warning(pin_factory):
    with pin_factory.pin('GPIO2') as pin:
        if pin.info.pull != 'up':
            pytest.skip("GPIO2 isn't pulled up on this pi")
        with pytest.raises(PinFixedPull):
            pin.pull = 'down'
        with pytest.raises(PinFixedPull):
            pin.input_with_pull('down')


def test_input_with_pull(pins):
    test_pin, input_pin = pins
    input_pin.pull = 'floating'
    test_pin.input_with_pull('up')
    assert test_pin.state == 1
    assert input_pin.state == 1
    test_pin.input_with_pull('down')
    assert test_pin.state == 0
    assert input_pin.state == 0


def test_pulls_are_weak(pins):
    test_pin, input_pin = pins
    test_pin.function = 'output'
    for pull in ('floating', 'down', 'up'):
        input_pin.pull = pull
        test_pin.state = 0
        assert input_pin.state == 0
        test_pin.state = 1
        assert input_pin.state == 1


def test_bad_duty_cycle(pins):
    test_pin, input_pin = pins
    test_pin.function = 'output'
    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("{test_pin.factory!r} doesn't support PWM".format(
            test_pin=test_pin))
    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
    test_pin.function = 'output'
    try:
        # NOTE: see above
        if Device.pin_factory.__class__.__name__ == 'RPiGPIOFactory':
            sleep(0.1)
        test_pin.frequency = 100
    except PinPWMUnsupported:
        pytest.skip("{test_pin.factory!r} doesn't support PWM".format(
            test_pin=test_pin))
    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


def test_explicit_factory(no_default_factory, pin_factory):
    with GPIODevice(TEST_PIN, pin_factory=pin_factory) as device:
        assert Device.pin_factory is None
        assert device.pin_factory is pin_factory
        assert device.pin.info.name == TEST_PIN


@pytest.mark.filterwarnings('ignore::DeprecationWarning')
def test_envvar_factory(no_default_factory, pin_factory_name):
    os.environ['GPIOZERO_PIN_FACTORY'] = pin_factory_name
    assert Device.pin_factory is None
    try:
        device = GPIODevice(TEST_PIN)
    except Exception as e:
        pytest.skip("skipped factory {pin_factory_name}: {e!s}".format(
            pin_factory_name=pin_factory_name, e=e))
    else:
        try:
            group = entry_points(group='gpiozero_pin_factories')
            for ep in group:
                if ep.name == pin_factory_name:
                    factory_class = ep.load()
                    break
            else:
                assert False, 'internal error'
            assert isinstance(Device.pin_factory, factory_class)
            assert device.pin_factory is Device.pin_factory
            assert device.pin.info.name == TEST_PIN
        finally:
            device.close()
            Device.pin_factory.close()


def test_compatibility_names(no_default_factory):
    os.environ['GPIOZERO_PIN_FACTORY'] = 'NATIVE'
    try:
        device = GPIODevice(TEST_PIN)
    except Exception as e:
        pytest.skip("skipped factory {pin_factory_name}: {e!s}".format(
            pin_factory_name=pin_factory_name, e=e))
    else:
        try:
            assert isinstance(Device.pin_factory, NativeFactory)
            assert device.pin_factory is Device.pin_factory
            assert device.pin.info.name == TEST_PIN
        finally:
            device.close()
            Device.pin_factory.close()


def test_bad_factory(no_default_factory):
    os.environ['GPIOZERO_PIN_FACTORY'] = 'foobarbaz'
    # Waits for someone to implement the foobarbaz pin factory just to
    # mess with our tests ...
    with pytest.raises(BadPinFactory):
        GPIODevice(TEST_PIN)


@pytest.mark.filterwarnings('ignore::gpiozero.exc.PinFactoryFallback')
def test_default_factory(no_default_factory):
    assert Device.pin_factory is None
    os.environ.pop('GPIOZERO_PIN_FACTORY', None)
    try:
        device = GPIODevice(TEST_PIN)
    except Exception as e:
        pytest.skip("no default factories")
    else:
        try:
            assert device.pin_factory is Device.pin_factory
            assert device.pin.info.name == TEST_PIN
        finally:
            device.close()
            Device.pin_factory.close()


def test_spi_init(pin_factory):
    with pin_factory.spi() as intf:
        assert isinstance(intf, SPI)
        assert repr(intf) in (
            "SPI(clock_pin='GPIO11', mosi_pin='GPIO10', miso_pin='GPIO9', "
            "select_pin='GPIO8')",
            "SPI(port=0, device=0)"
        )
        intf.close()
        assert intf.closed
        assert repr(intf) == 'SPI(closed)'
    with pin_factory.spi(port=0, device=1) as intf:
        assert repr(intf) in (
            "SPI(clock_pin='GPIO11', mosi_pin='GPIO10', miso_pin='GPIO9', "
            "select_pin='GPIO7')",
            "SPI(port=0, device=1)"
        )
    with pin_factory.spi(clock_pin=11, mosi_pin=10, select_pin=8) as intf:
        assert repr(intf) in (
            "SPI(clock_pin='GPIO11', mosi_pin='GPIO10', miso_pin='GPIO9', "
            "select_pin='GPIO8')",
            "SPI(port=0, device=0)"
        )
    # Ensure we support "partial" SPI where we don't reserve a pin because
    # the device wants it for general IO (see SPI screens which use a pin
    # for data/commands)
    with pin_factory.spi(clock_pin=11, mosi_pin=10, miso_pin=None, select_pin=7) as intf:
        assert isinstance(intf, SPI)
    with pin_factory.spi(clock_pin=11, mosi_pin=None, miso_pin=9, select_pin=7) as intf:
        assert isinstance(intf, SPI)
    with pin_factory.spi(shared=True) as intf:
        assert isinstance(intf, SPI)
    with pytest.raises(ValueError):
        pin_factory.spi(port=1)
    with pytest.raises(ValueError):
        pin_factory.spi(device=2)
    with pytest.raises(ValueError):
        pin_factory.spi(port=0, clock_pin=12)
    with pytest.raises(ValueError):
        pin_factory.spi(foo='bar')


def test_spi_hardware_conflict(default_factory):
    with LED(11) as led:
        with pytest.raises(GPIOPinInUse):
            Device.pin_factory.spi(port=0, device=0)
    with Device.pin_factory.spi(port=0, device=0) as spi:
        with pytest.raises(GPIOPinInUse):
            LED(11)


def test_spi_hardware_same_port(default_factory):
    with Device.pin_factory.spi(device=0) as intf:
        local_hardware_spi_only(intf)
        with pytest.raises(GPIOPinInUse):
            Device.pin_factory.spi(device=0)
        with Device.pin_factory.spi(device=1) as another_intf:
            assert intf._port == another_intf._port


def test_spi_hardware_shared_bus(default_factory):
    with Device.pin_factory.spi(device=0, shared=True) as intf:
        with Device.pin_factory.spi(device=0, shared=True) as another_intf:
            assert intf is another_intf


def test_spi_hardware_read(default_factory):
    local_only()
    with mock.patch('gpiozero.pins.local.SpiDev') as spidev:
        spidev.return_value.xfer2.side_effect = lambda data: list(range(10))[:len(data)]
        with Device.pin_factory.spi() as intf:
            local_hardware_spi_only(intf)
            assert intf.read(3) == [0, 1, 2]
            assert intf.read(6) == list(range(6))


def test_spi_hardware_write(default_factory):
    local_only()
    with mock.patch('gpiozero.pins.local.SpiDev') as spidev:
        spidev.return_value.xfer2.side_effect = lambda data: list(range(10))[:len(data)]
        with Device.pin_factory.spi() as intf:
            local_hardware_spi_only(intf)
            assert intf.write([0, 1, 2]) == 3
            assert spidev.return_value.xfer2.called_with([0, 1, 2])
            assert intf.write(list(range(6))) == 6
            assert spidev.return_value.xfer2.called_with(list(range(6)))


def test_spi_hardware_modes(default_factory):
    local_only()
    with mock.patch('gpiozero.pins.local.SpiDev') as spidev:
        spidev.return_value.mode = 0
        spidev.return_value.lsbfirst = False
        spidev.return_value.cshigh = True
        spidev.return_value.bits_per_word = 8
        with Device.pin_factory.spi() as intf:
            local_hardware_spi_only(intf)
            assert intf.clock_mode == 0
            assert not intf.clock_polarity
            assert not intf.clock_phase
            intf.clock_polarity = False
            assert intf.clock_mode == 0
            intf.clock_polarity = True
            assert intf.clock_mode == 2
            intf.clock_phase = True
            assert intf.clock_mode == 3
            assert not intf.lsb_first
            assert intf.select_high
            assert intf.bits_per_word == 8
            intf.select_high = False
            intf.lsb_first = True
            intf.bits_per_word = 12
            assert not spidev.return_value.cshigh
            assert spidev.return_value.lsbfirst
            assert spidev.return_value.bits_per_word == 12
            intf.rate = 1000000
            assert intf.rate == 1000000
            intf.rate = 500000
            assert intf.rate == 500000


# XXX Test two simultaneous SPI devices sharing clock, MOSI, and MISO, with
# separate select pins (including threaded tests which attempt simultaneous
# reading/writing)