1
0
mirror of https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git synced 2026-10-25 12:39:18 +03:00
Files
libgpiod/bindings/python/tests/tests_line_request.py
Vincent Fazio 2e0045486b bindings: python: parse non-tuple Iterable keys
When `chip.request_lines` was modified to allow `Iterable`s instead of
only `tuple`s, the code that checked for duplicate line entries in the
`config` argument was not updated to account for the expanded types.

If the `config` argument had a key that was not a `tuple`, `str`, or
`int`, a `TypeError` would be raised when resolving the line offset.

Refactor the code the resolves IDs to offsets into a separate function
to make the logic a bit clearer and to account for the widened types.

Fixes: 8f62e6c453 ("bindings: python: loosen type requirements in public API")
Closes: https://github.com/brgl/libgpiod/issues/148
Signed-off-by: Vincent Fazio <vfazio@gmail.com>
Link: https://lore.kernel.org/r/20250902123905.6491-1-vfazio@gmail.com
[Bartosz: don't pass the only argument by name in the call to
_resolve_config_keys_to_offsets()]
Signed-off-by: Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
2025-09-08 09:31:28 +02:00

649 lines
23 KiB
Python

# SPDX-License-Identifier: GPL-2.0-or-later
# SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
from unittest import TestCase
import gpiod
from gpiod.line import Clock, Direction, Drive, Edge, Value
from . import gpiosim
Pull = gpiosim.Chip.Pull
SimVal = gpiosim.Chip.Value
class ChipLineRequestsBehaveCorrectlyWithInvalidArguments(TestCase):
def setUp(self) -> None:
self.sim = gpiosim.Chip(num_lines=8)
self.chip = gpiod.Chip(self.sim.dev_path)
def tearDown(self) -> None:
self.chip.close()
del self.chip
del self.sim
def test_passing_invalid_types_as_configs(self) -> None:
with self.assertRaises(AttributeError):
self.chip.request_lines("foobar") # type: ignore[arg-type]
with self.assertRaises(AttributeError):
self.chip.request_lines(None, "foobar") # type: ignore[arg-type]
def test_offset_out_of_range(self) -> None:
with self.assertRaises(ValueError):
self.chip.request_lines(config={(1, 0, 4, 8): None})
def test_line_name_not_found(self) -> None:
with self.assertRaises(FileNotFoundError):
self.chip.request_lines(config={"foo": None})
def test_request_no_arguments(self) -> None:
with self.assertRaises(TypeError):
self.chip.request_lines() # type: ignore[call-arg]
class ModuleLineRequestsBehaveCorrectlyWithInvalidArguments(TestCase):
def setUp(self) -> None:
self.sim = gpiosim.Chip(num_lines=8)
def tearDown(self) -> None:
del self.sim
def test_passing_invalid_types_as_configs(self) -> None:
with self.assertRaises(AttributeError):
gpiod.request_lines(self.sim.dev_path, "foobar") # type: ignore[arg-type]
with self.assertRaises(AttributeError):
gpiod.request_lines(self.sim.dev_path, None, "foobar") # type: ignore[arg-type]
def test_offset_out_of_range(self) -> None:
with self.assertRaises(ValueError):
gpiod.request_lines(self.sim.dev_path, config={(1, 0, 4, 8): None})
def test_line_name_not_found(self) -> None:
with self.assertRaises(FileNotFoundError):
gpiod.request_lines(self.sim.dev_path, config={"foo": None})
def test_request_no_arguments(self) -> None:
with self.assertRaises(TypeError):
gpiod.request_lines() # type: ignore[call-arg]
class ChipLineRequestWorks(TestCase):
def setUp(self) -> None:
self.sim = gpiosim.Chip(num_lines=8, line_names={5: "foo", 7: "bar"})
self.chip = gpiod.Chip(self.sim.dev_path)
def tearDown(self) -> None:
self.chip.close()
del self.chip
del self.sim
def test_request_with_positional_arguments(self) -> None:
with self.chip.request_lines({(0, 5, 3, 1): None}, "foobar", 32) as req:
self.assertEqual(req.offsets, [0, 5, 3, 1])
self.assertEqual(self.chip.get_line_info(0).consumer, "foobar")
def test_request_with_keyword_arguments(self) -> None:
with self.chip.request_lines(
config={(0, 5, 6): None},
consumer="foobar",
event_buffer_size=16,
) as req:
self.assertEqual(req.offsets, [0, 5, 6])
self.assertEqual(self.chip.get_line_info(0).consumer, "foobar")
def test_request_single_offset_as_int(self) -> None:
with self.chip.request_lines(config={4: None}) as req:
self.assertEqual(req.offsets, [4])
def test_request_single_offset_as_tuple(self) -> None:
with self.chip.request_lines(config={(4): None}) as req:
self.assertEqual(req.offsets, [4])
def test_request_single_offset_as_frozenset(self) -> None:
with self.chip.request_lines(config={frozenset([4]): None}) as req:
self.assertEqual(req.offsets, [4])
def test_request_by_name(self) -> None:
with self.chip.request_lines(config={(1, 2, "foo", "bar"): None}) as req:
self.assertEqual(req.offsets, [1, 2, 5, 7])
def test_request_single_line_by_name(self) -> None:
with self.chip.request_lines(config={"foo": None}) as req:
self.assertEqual(req.offsets, [5])
class ModuleLineRequestWorks(TestCase):
def setUp(self) -> None:
self.sim = gpiosim.Chip(num_lines=8, line_names={5: "foo", 7: "bar"})
def tearDown(self) -> None:
del self.sim
def test_request_with_positional_arguments(self) -> None:
with gpiod.request_lines(
self.sim.dev_path, {(0, 5, 3, 1): None}, "foobar", 32
) as req:
self.assertEqual(req.offsets, [0, 5, 3, 1])
with gpiod.Chip(self.sim.dev_path) as chip:
self.assertEqual(chip.get_line_info(5).consumer, "foobar")
def test_request_with_keyword_arguments(self) -> None:
with gpiod.request_lines(
path=self.sim.dev_path,
config={(0, 5, 6): None},
consumer="foobar",
event_buffer_size=16,
) as req:
self.assertEqual(req.offsets, [0, 5, 6])
with gpiod.Chip(self.sim.dev_path) as chip:
self.assertEqual(chip.get_line_info(5).consumer, "foobar")
def test_request_single_offset_as_int(self) -> None:
with gpiod.request_lines(path=self.sim.dev_path, config={4: None}) as req:
self.assertEqual(req.offsets, [4])
def test_request_single_offset_as_tuple(self) -> None:
with gpiod.request_lines(path=self.sim.dev_path, config={(4): None}) as req:
self.assertEqual(req.offsets, [4])
def test_request_by_name(self) -> None:
with gpiod.request_lines(
self.sim.dev_path, {(1, 2, "foo", "bar"): None}
) as req:
self.assertEqual(req.offsets, [1, 2, 5, 7])
class LineRequestGettingValues(TestCase):
def setUp(self) -> None:
self.sim = gpiosim.Chip(num_lines=8)
self.req = gpiod.request_lines(
self.sim.dev_path,
{(0, 1, 2, 3): gpiod.LineSettings(direction=Direction.INPUT)},
)
def tearDown(self) -> None:
self.req.release()
del self.req
del self.sim
def test_get_single_value(self) -> None:
self.sim.set_pull(1, Pull.UP)
self.assertEqual(self.req.get_values([1]), [Value.ACTIVE])
def test_get_single_value_helper(self) -> None:
self.sim.set_pull(1, Pull.UP)
self.assertEqual(self.req.get_value(1), Value.ACTIVE)
def test_get_values_for_subset_of_lines(self) -> None:
self.sim.set_pull(0, Pull.UP)
self.sim.set_pull(1, Pull.DOWN)
self.sim.set_pull(3, Pull.UP)
self.assertEqual(
self.req.get_values([0, 1, 3]), [Value.ACTIVE, Value.INACTIVE, Value.ACTIVE]
)
def test_get_all_values(self) -> None:
self.sim.set_pull(0, Pull.DOWN)
self.sim.set_pull(1, Pull.UP)
self.sim.set_pull(2, Pull.UP)
self.sim.set_pull(3, Pull.UP)
self.assertEqual(
self.req.get_values(),
[Value.INACTIVE, Value.ACTIVE, Value.ACTIVE, Value.ACTIVE],
)
def test_get_values_invalid_offset(self) -> None:
with self.assertRaises(ValueError):
self.req.get_values([9])
def test_get_values_invalid_argument_type(self) -> None:
with self.assertRaises(TypeError):
self.req.get_values(True) # type: ignore[arg-type]
class LineRequestGettingValuesByName(TestCase):
def setUp(self) -> None:
self.sim = gpiosim.Chip(num_lines=4, line_names={2: "foo", 3: "bar", 1: "baz"})
self.req = gpiod.request_lines(
self.sim.dev_path,
{(0, "baz", "bar", "foo"): gpiod.LineSettings(direction=Direction.INPUT)},
)
def tearDown(self) -> None:
self.req.release()
del self.req
del self.sim
def test_get_values_by_name(self) -> None:
self.sim.set_pull(1, Pull.UP)
self.sim.set_pull(2, Pull.DOWN)
self.sim.set_pull(3, Pull.UP)
self.assertEqual(
self.req.get_values(["foo", "bar", 1]),
[Value.INACTIVE, Value.ACTIVE, Value.ACTIVE],
)
def test_get_values_by_bad_name(self) -> None:
with self.assertRaises(ValueError):
self.req.get_values(["xyz"])
class LineRequestSettingValues(TestCase):
def setUp(self) -> None:
self.sim = gpiosim.Chip(num_lines=8)
self.req = gpiod.request_lines(
self.sim.dev_path,
{(0, 1, 2, 3): gpiod.LineSettings(direction=Direction.OUTPUT)},
)
def tearDown(self) -> None:
self.req.release()
del self.req
del self.sim
def test_set_single_value(self) -> None:
self.req.set_values({1: Value.ACTIVE})
self.assertEqual(self.sim.get_value(1), SimVal.ACTIVE)
def test_set_single_value_helper(self) -> None:
self.req.set_value(1, Value.ACTIVE)
self.assertEqual(self.sim.get_value(1), SimVal.ACTIVE)
def test_set_values_for_subset_of_lines(self) -> None:
self.req.set_values({0: Value.ACTIVE, 1: Value.INACTIVE, 3: Value.ACTIVE})
self.assertEqual(self.sim.get_value(0), SimVal.ACTIVE)
self.assertEqual(self.sim.get_value(1), SimVal.INACTIVE)
self.assertEqual(self.sim.get_value(3), SimVal.ACTIVE)
def test_set_values_invalid_offset(self) -> None:
with self.assertRaises(ValueError):
self.req.set_values({9: Value.ACTIVE})
class LineRequestSettingValuesByName(TestCase):
def setUp(self) -> None:
self.sim = gpiosim.Chip(num_lines=4, line_names={2: "foo", 3: "bar", 1: "baz"})
self.req = gpiod.request_lines(
self.sim.dev_path,
{(0, "baz", "bar", "foo"): gpiod.LineSettings(direction=Direction.OUTPUT)},
)
def tearDown(self) -> None:
self.req.release()
del self.req
del self.sim
def test_set_values_by_name(self) -> None:
self.req.set_values(
{"foo": Value.INACTIVE, "bar": Value.ACTIVE, 1: Value.ACTIVE}
)
self.assertEqual(self.sim.get_value(2), SimVal.INACTIVE)
self.assertEqual(self.sim.get_value(1), SimVal.ACTIVE)
self.assertEqual(self.sim.get_value(3), SimVal.ACTIVE)
def test_set_values_by_bad_name(self) -> None:
with self.assertRaises(ValueError):
self.req.set_values({"xyz": Value.ACTIVE})
class LineRequestComplexConfig(TestCase):
def test_complex_config(self) -> None:
sim = gpiosim.Chip(num_lines=8)
with gpiod.Chip(sim.dev_path) as chip:
with chip.request_lines(
config={
(0, 2, 4): gpiod.LineSettings(
direction=Direction.OUTPUT, output_value=Value.ACTIVE
),
(1, 3, 5): gpiod.LineSettings(
direction=Direction.INPUT,
edge_detection=Edge.BOTH,
event_clock=Clock.REALTIME,
),
},
) as req:
self.assertEqual(chip.get_line_info(2).direction, Direction.OUTPUT)
self.assertEqual(chip.get_line_info(3).edge_detection, Edge.BOTH)
self.assertEqual(chip.get_line_info(5).event_clock, Clock.REALTIME)
class LineRequestMixedConfigByName(TestCase):
def setUp(self) -> None:
self.sim = gpiosim.Chip(
num_lines=4, line_names={2: "foo", 3: "bar", 1: "baz", 0: "xyz"}
)
self.req = gpiod.request_lines(
self.sim.dev_path,
{
("baz", "bar"): gpiod.LineSettings(direction=Direction.OUTPUT),
("foo", "xyz"): gpiod.LineSettings(direction=Direction.INPUT),
},
)
def tearDown(self) -> None:
self.req.release()
del self.req
del self.sim
def test_set_values_by_name(self) -> None:
self.req.set_values({"bar": Value.ACTIVE, "baz": Value.INACTIVE})
self.assertEqual(self.sim.get_value(1), SimVal.INACTIVE)
self.assertEqual(self.sim.get_value(3), SimVal.ACTIVE)
def test_get_values_by_name(self) -> None:
self.sim.set_pull(0, Pull.UP)
self.sim.set_pull(2, Pull.DOWN)
self.assertEqual(
self.req.get_values(["foo", "xyz", "baz"]),
[Value.INACTIVE, Value.ACTIVE, Value.INACTIVE],
)
class RepeatingLinesInRequestConfig(TestCase):
def setUp(self) -> None:
self.sim = gpiosim.Chip(num_lines=4, line_names={0: "foo", 2: "bar"})
self.chip = gpiod.Chip(self.sim.dev_path)
def tearDown(self) -> None:
self.chip.close()
del self.chip
del self.sim
def test_offsets_repeating_within_the_same_tuple(self) -> None:
with self.assertRaises(ValueError):
self.chip.request_lines({(0, 1, 2, 1): None})
def test_offsets_repeating_in_different_tuples(self) -> None:
with self.assertRaises(ValueError):
self.chip.request_lines({(0, 1, 2): None, (3, 4, 0): None})
def test_offset_and_name_conflict_in_the_same_tuple(self) -> None:
with self.assertRaises(ValueError):
self.chip.request_lines({(2, "bar"): None})
def test_offset_and_name_conflict_in_different_tuples(self) -> None:
with self.assertRaises(ValueError):
self.chip.request_lines({(0, 1, 2): None, (4, 5, "bar"): None})
class LineRequestPropertiesWork(TestCase):
def setUp(self) -> None:
self.sim = gpiosim.Chip(num_lines=16, line_names={0: "foo", 2: "bar", 5: "baz"})
def tearDown(self) -> None:
del self.sim
def test_property_fd(self) -> None:
with gpiod.request_lines(
self.sim.dev_path,
config={
0: gpiod.LineSettings(
direction=Direction.INPUT, edge_detection=Edge.BOTH
)
},
) as req:
self.assertGreaterEqual(req.fd, 0)
def test_property_num_lines(self) -> None:
with gpiod.request_lines(
self.sim.dev_path, config={(0, 2, 3, 5, 6, 8, 12): None}
) as req:
self.assertEqual(req.num_lines, 7)
def test_property_offsets(self) -> None:
with gpiod.request_lines(
self.sim.dev_path, config={(1, 6, 12, 4): None}
) as req:
self.assertEqual(req.offsets, [1, 6, 12, 4])
def test_property_lines(self) -> None:
with gpiod.request_lines(
self.sim.dev_path, config={("foo", 1, "bar", 4, "baz"): None}
) as req:
self.assertEqual(req.lines, ["foo", 1, "bar", 4, "baz"])
class LineRequestConsumerString(TestCase):
def setUp(self) -> None:
self.sim = gpiosim.Chip(num_lines=4)
self.chip = gpiod.Chip(self.sim.dev_path)
def tearDown(self) -> None:
self.chip.close()
del self.chip
del self.sim
def test_custom_consumer(self) -> None:
with self.chip.request_lines(
consumer="foobar", config={(2, 3): None}
) as request:
info = self.chip.get_line_info(2)
self.assertEqual(info.consumer, "foobar")
def test_empty_consumer(self) -> None:
with self.chip.request_lines(consumer="", config={(2, 3): None}) as request:
info = self.chip.get_line_info(2)
self.assertEqual(info.consumer, "?")
def test_default_consumer(self) -> None:
with self.chip.request_lines(config={(2, 3): None}) as request:
info = self.chip.get_line_info(2)
self.assertEqual(info.consumer, "?")
class LineRequestSetOutputValues(TestCase):
def setUp(self) -> None:
self.sim = gpiosim.Chip(
num_lines=4, line_names={0: "foo", 1: "bar", 2: "baz", 3: "xyz"}
)
def tearDown(self) -> None:
del self.sim
def test_request_with_globally_set_output_values(self) -> None:
with gpiod.request_lines(
self.sim.dev_path,
config={(0, 1, 2, 3): gpiod.LineSettings(direction=Direction.OUTPUT)},
output_values={
0: Value.ACTIVE,
1: Value.INACTIVE,
2: Value.ACTIVE,
3: Value.INACTIVE,
},
) as request:
self.assertEqual(self.sim.get_value(0), SimVal.ACTIVE)
self.assertEqual(self.sim.get_value(1), SimVal.INACTIVE)
self.assertEqual(self.sim.get_value(2), SimVal.ACTIVE)
self.assertEqual(self.sim.get_value(3), SimVal.INACTIVE)
def test_request_with_globally_set_output_values_with_mapping(self) -> None:
with gpiod.request_lines(
self.sim.dev_path,
config={(0, 1, 2, 3): gpiod.LineSettings(direction=Direction.OUTPUT)},
output_values={"baz": Value.ACTIVE, "foo": Value.ACTIVE},
) as request:
self.assertEqual(self.sim.get_value(0), SimVal.ACTIVE)
self.assertEqual(self.sim.get_value(1), SimVal.INACTIVE)
self.assertEqual(self.sim.get_value(2), SimVal.ACTIVE)
self.assertEqual(self.sim.get_value(3), SimVal.INACTIVE)
def test_request_with_globally_set_output_values_bad_mapping(self) -> None:
with self.assertRaises(FileNotFoundError):
with gpiod.request_lines(
self.sim.dev_path,
config={(0, 1, 2, 3): gpiod.LineSettings(direction=Direction.OUTPUT)},
output_values={"foobar": Value.ACTIVE},
) as request:
pass
def test_request_with_globally_set_output_values_bad_offset(self) -> None:
with self.assertRaises(ValueError):
with gpiod.request_lines(
self.sim.dev_path,
config={(0, 1, 2, 3): gpiod.LineSettings(direction=Direction.OUTPUT)},
output_values={5: Value.ACTIVE},
) as request:
pass
class ReconfigureRequestedLines(TestCase):
def setUp(self) -> None:
self.sim = gpiosim.Chip(num_lines=8, line_names={3: "foo", 4: "bar", 6: "baz"})
self.chip = gpiod.Chip(self.sim.dev_path)
self.req = self.chip.request_lines(
{
(0, 2, "foo", "baz"): gpiod.LineSettings(
direction=Direction.OUTPUT, active_low=True, drive=Drive.OPEN_DRAIN
)
}
)
def tearDown(self) -> None:
self.chip.close()
del self.chip
self.req.release()
del self.req
del self.sim
def test_reconfigure_by_offsets(self) -> None:
info = self.chip.get_line_info(2)
self.assertEqual(info.direction, Direction.OUTPUT)
self.req.reconfigure_lines(
{(0, 2, 3, 6): gpiod.LineSettings(direction=Direction.INPUT)}
)
info = self.chip.get_line_info(2)
self.assertEqual(info.direction, Direction.INPUT)
def test_reconfigure_by_names(self) -> None:
info = self.chip.get_line_info(2)
self.assertEqual(info.direction, Direction.OUTPUT)
self.req.reconfigure_lines(
{(0, 2, "foo", "baz"): gpiod.LineSettings(direction=Direction.INPUT)}
)
info = self.chip.get_line_info(2)
self.assertEqual(info.direction, Direction.INPUT)
def test_reconfigure_by_misordered_offsets(self) -> None:
info = self.chip.get_line_info(2)
self.assertEqual(info.direction, Direction.OUTPUT)
self.req.reconfigure_lines(
{(6, 0, 3, 2): gpiod.LineSettings(direction=Direction.INPUT)}
)
info = self.chip.get_line_info(2)
self.assertEqual(info.direction, Direction.INPUT)
def test_reconfigure_by_misordered_names(self) -> None:
info = self.chip.get_line_info(2)
self.assertEqual(info.direction, Direction.OUTPUT)
self.req.reconfigure_lines(
{(0, "baz", 2, "foo"): gpiod.LineSettings(direction=Direction.INPUT)}
)
info = self.chip.get_line_info(2)
self.assertEqual(info.direction, Direction.INPUT)
def test_reconfigure_with_default(self) -> None:
info = self.chip.get_line_info(2)
self.assertEqual(info.direction, Direction.OUTPUT)
self.assertTrue(info.active_low)
self.assertEqual(info.drive, Drive.OPEN_DRAIN)
self.req.reconfigure_lines(
{
0: gpiod.LineSettings(direction=Direction.INPUT),
2: None,
("baz", "foo"): gpiod.LineSettings(direction=Direction.INPUT),
}
)
info = self.chip.get_line_info(0)
self.assertEqual(info.direction, Direction.INPUT)
info = self.chip.get_line_info(2)
self.assertEqual(info.direction, Direction.OUTPUT)
self.assertTrue(info.active_low)
self.assertEqual(info.drive, Drive.OPEN_DRAIN)
def test_reconfigure_missing_offsets(self) -> None:
info = self.chip.get_line_info(2)
self.assertEqual(info.direction, Direction.OUTPUT)
self.assertTrue(info.active_low)
self.assertEqual(info.drive, Drive.OPEN_DRAIN)
self.req.reconfigure_lines(
{(6, 0): gpiod.LineSettings(direction=Direction.INPUT)}
)
info = self.chip.get_line_info(0)
self.assertEqual(info.direction, Direction.INPUT)
info = self.chip.get_line_info(2)
self.assertEqual(info.direction, Direction.OUTPUT)
self.assertTrue(info.active_low)
self.assertEqual(info.drive, Drive.OPEN_DRAIN)
def test_reconfigure_extra_offsets(self) -> None:
info = self.chip.get_line_info(2)
self.assertEqual(info.direction, Direction.OUTPUT)
self.req.reconfigure_lines(
{(0, 2, 3, 6, 5): gpiod.LineSettings(direction=Direction.INPUT)}
)
info = self.chip.get_line_info(2)
self.assertEqual(info.direction, Direction.INPUT)
class ReleasedLineRequestCannotBeUsed(TestCase):
def test_using_released_line_request(self) -> None:
sim = gpiosim.Chip()
with gpiod.Chip(sim.dev_path) as chip:
req = chip.request_lines(config={0: None})
req.release()
with self.assertRaises(gpiod.RequestReleasedError):
_ = req.fd
class LineRequestSurvivesParentChip(TestCase):
def test_line_request_survives_parent_chip(self) -> None:
sim = gpiosim.Chip()
chip = gpiod.Chip(sim.dev_path)
try:
req = chip.request_lines(
config={0: gpiod.LineSettings(direction=Direction.INPUT)}
)
except:
chip.close()
raise
chip.close()
self.assertEqual(req.get_values([0]), [Value.INACTIVE])
req.release()
class LineRequestStringRepresentation(TestCase):
def setUp(self) -> None:
self.sim = gpiosim.Chip(num_lines=8)
def tearDown(self) -> None:
del self.sim
def test_str(self) -> None:
with gpiod.Chip(self.sim.dev_path) as chip:
with chip.request_lines(config={(2, 6, 4, 1): None}) as req:
self.assertEqual(
str(req),
f'<LineRequest chip="{self.sim.name}" num_lines=4 offsets=[2, 6, 4, 1] fd={req.fd}>',
)
def test_str_released(self) -> None:
req = gpiod.request_lines(self.sim.dev_path, config={(2, 6, 4, 1): None})
req.release()
self.assertEqual(str(req), "<LineRequest RELEASED>")