linux-gpio.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [libgpiod v2][PATCH v3 0/3] libgpiod v2: rewrite tests for the C library
@ 2022-03-03  9:18 Bartosz Golaszewski
  2022-03-03  9:18 ` [libgpiod v2][PATCH v3 1/3] API: add an enum for line values Bartosz Golaszewski
                   ` (2 more replies)
  0 siblings, 3 replies; 8+ messages in thread
From: Bartosz Golaszewski @ 2022-03-03  9:18 UTC (permalink / raw)
  To: Kent Gibson, Linus Walleij, Andy Shevchenko
  Cc: linux-gpio, Bartosz Golaszewski

This series contains a rework of the line_config interface and a bit patch
that replaces the old test suite for libgpiod v1 based on gpio-mockup with
a one covering around 95% of the libgpiod v2 code based on the upcoming
gpio-sim module.

v1 -> v2:
- drop applied patches
- improve documentation
- skip offsets that are overridden in line_config but not actually requested

v2 -> v3:
- add a patch adding an enum for line values
- rework the mechanism for retrieving overrides
- add new "constructors" for line_config
- incorporate part of Kent's points in the documentation
- a bunch of improvements to tests as per Kent's reviews

Bartosz Golaszewski (3):
  API: add an enum for line values
  line-config: expose the override logic to users
  tests: rewrite core C tests using libgpiosim

 configure.ac                 |    8 +-
 include/gpiod.h              |  603 +++++++++++++------
 lib/line-config.c            |  871 ++++++++++++++++++---------
 tests/Makefile.am            |   24 +-
 tests/gpiod-test-helpers.c   |   49 ++
 tests/gpiod-test-helpers.h   |  139 +++++
 tests/gpiod-test-sim.c       |  308 ++++++++++
 tests/gpiod-test-sim.h       |   42 ++
 tests/gpiod-test.c           |  233 +-------
 tests/gpiod-test.h           |   83 +--
 tests/gpiosim/gpiosim.c      |    1 +
 tests/mockup/Makefile.am     |   11 -
 tests/mockup/gpio-mockup.c   |  496 ----------------
 tests/mockup/gpio-mockup.h   |   36 --
 tests/tests-chip.c           |  282 ++++-----
 tests/tests-edge-event.c     |  490 +++++++++++++++
 tests/tests-event.c          |  908 ----------------------------
 tests/tests-info-event.c     |  301 ++++++++++
 tests/tests-line-config.c    |  503 ++++++++++++++++
 tests/tests-line-info.c      |  318 ++++++++++
 tests/tests-line-request.c   |  526 ++++++++++++++++
 tests/tests-line.c           | 1091 ----------------------------------
 tests/tests-misc.c           |   80 ++-
 tests/tests-request-config.c |   90 +++
 tools/gpioget.c              |    6 +-
 tools/gpiomon.c              |    6 +-
 tools/gpioset.c              |    9 +-
 27 files changed, 4017 insertions(+), 3497 deletions(-)
 create mode 100644 tests/gpiod-test-helpers.c
 create mode 100644 tests/gpiod-test-helpers.h
 create mode 100644 tests/gpiod-test-sim.c
 create mode 100644 tests/gpiod-test-sim.h
 delete mode 100644 tests/mockup/Makefile.am
 delete mode 100644 tests/mockup/gpio-mockup.c
 delete mode 100644 tests/mockup/gpio-mockup.h
 create mode 100644 tests/tests-edge-event.c
 delete mode 100644 tests/tests-event.c
 create mode 100644 tests/tests-info-event.c
 create mode 100644 tests/tests-line-config.c
 create mode 100644 tests/tests-line-info.c
 create mode 100644 tests/tests-line-request.c
 delete mode 100644 tests/tests-line.c
 create mode 100644 tests/tests-request-config.c

-- 
2.30.1


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [libgpiod v2][PATCH v3 1/3] API: add an enum for line values
  2022-03-03  9:18 [libgpiod v2][PATCH v3 0/3] libgpiod v2: rewrite tests for the C library Bartosz Golaszewski
@ 2022-03-03  9:18 ` Bartosz Golaszewski
  2022-03-05  5:50   ` Kent Gibson
  2022-03-03  9:18 ` [libgpiod v2][PATCH v3 2/3] line-config: expose the override logic to users Bartosz Golaszewski
  2022-03-03  9:18 ` [libgpiod v2][PATCH v3 3/3] tests: rewrite core C tests using libgpiosim Bartosz Golaszewski
  2 siblings, 1 reply; 8+ messages in thread
From: Bartosz Golaszewski @ 2022-03-03  9:18 UTC (permalink / raw)
  To: Kent Gibson, Linus Walleij, Andy Shevchenko
  Cc: linux-gpio, Bartosz Golaszewski

In order to explicitly stress that line values as understood by libgpiod
are logical values, expose a two-value enum with values called: ACTIVE
and INACTIVE that should be used whenever referring to the state of GPIO
lines.

The value of INACTIVE is set to 0 while that of ACTIVE to 1 so that users
can still use integers in C (where no scoped enums exist).

Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
---
 include/gpiod.h | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/include/gpiod.h b/include/gpiod.h
index 074e395..34fdad6 100644
--- a/include/gpiod.h
+++ b/include/gpiod.h
@@ -185,12 +185,20 @@ gpiod_chip_request_lines(struct gpiod_chip *chip,
 /**
  * @}
  *
- * @defgroup line_settings Line settings
+ * @defgroup line_settings Line definitions
  * @{
  *
- * These defines are used both by gpiod_line_info and gpiod_line_config.
+ * These defines are used across the API.
  */
 
+/**
+ * @brief Logical line state.
+ */
+enum {
+	GPIOD_LINE_VALUE_INACTIVE = 0,
+	GPIOD_LINE_VALUE_ACTIVE = 1,
+};
+
 /**
  * @brief Direction settings.
  */
-- 
2.30.1


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [libgpiod v2][PATCH v3 2/3] line-config: expose the override logic to users
  2022-03-03  9:18 [libgpiod v2][PATCH v3 0/3] libgpiod v2: rewrite tests for the C library Bartosz Golaszewski
  2022-03-03  9:18 ` [libgpiod v2][PATCH v3 1/3] API: add an enum for line values Bartosz Golaszewski
@ 2022-03-03  9:18 ` Bartosz Golaszewski
  2022-03-05  5:51   ` Kent Gibson
  2022-03-03  9:18 ` [libgpiod v2][PATCH v3 3/3] tests: rewrite core C tests using libgpiosim Bartosz Golaszewski
  2 siblings, 1 reply; 8+ messages in thread
From: Bartosz Golaszewski @ 2022-03-03  9:18 UTC (permalink / raw)
  To: Kent Gibson, Linus Walleij, Andy Shevchenko
  Cc: linux-gpio, Bartosz Golaszewski

We've already added getters for line-config but without exposing some
parts of the internal logic of the object, the user can't really get
the full picture and inspect the contents. This patch reworks the
accessors further by providing access to the underlying override
mechanism.

For every setting, we expose a getter and setter for the default value
as well as a set of four functions for setting, getting, clearing and
checking per-offset overrides.

An override can initially have the same value as the defaults but will
retain the overridden value should the defaults change.

We also complete the API by providing functions that allow to retrieve
the overridden offsets and their corresponding property types.

This way the caller can fully inspect the line_config and high-level
language bindings can provide stringification methods.

While at it: we fix a couple bugs in the implementation of struct
line_config and add new constructors that take a variable list of
arguments.

Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
---
 include/gpiod.h   | 591 +++++++++++++++++++++----------
 lib/line-config.c | 871 +++++++++++++++++++++++++++++++---------------
 tools/gpioget.c   |   6 +-
 tools/gpiomon.c   |   6 +-
 tools/gpioset.c   |   9 +-
 5 files changed, 1012 insertions(+), 471 deletions(-)

diff --git a/include/gpiod.h b/include/gpiod.h
index 34fdad6..dc50634 100644
--- a/include/gpiod.h
+++ b/include/gpiod.h
@@ -9,6 +9,7 @@
 #ifndef __LIBGPIOD_GPIOD_H__
 #define __LIBGPIOD_GPIOD_H__
 
+#include <stdarg.h>
 #include <stdbool.h>
 #include <stdint.h>
 
@@ -176,6 +177,8 @@ int gpiod_chip_find_line(struct gpiod_chip *chip, const char *name);
  * @param line_cfg Line config object.
  * @return New line request object or NULL if an error occurred. The request
  *         must be released by the caller using ::gpiod_line_request_release.
+ * @note Line configuration overrides set for offsets that don't end up being
+ *       requested are silently ignored.
  */
 struct gpiod_line_request *
 gpiod_chip_request_lines(struct gpiod_chip *chip,
@@ -458,13 +461,65 @@ gpiod_info_event_get_line_info(struct gpiod_info_event *event);
  *
  * The line-config object stores the configuration for lines that can be used
  * in two cases: when making a line request and when reconfiguring a set of
- * already requested lines. The mutators for the line request don't return
- * errors. If the set of options is too complex to be translated into kernel
- * uAPI structures - the error will be returned at the time of the request or
- * reconfiguration. If an invalid value was passed to any of the getters - the
- * default value will be silently used instead. Each option can be set
- * globally, for a single line offset or for multiple line offsets.
+ * already requested lines.
+ *
+ * A new line-config object is instantiated containing a set of sane defaults
+ * for all supported configuration settings. Those defaults can be modified by
+ * the caller. Default values can be overridden by applying different values
+ * for specific line offsets. When making a request or reconfiguring an
+ * existing one, the overridden settings for specific offsets will be taken
+ * into account first and for every other offset and setting the defaults will
+ * be used.
+ *
+ * For every setting there are two mutators (one setting the default and one
+ * for the per-offset override), two getters (one for reading the global
+ * default and one for retrieving the effective value for the line at given
+ * offset), a function for testing if a setting is overridden for the line at
+ * given offset and finally a function for clearing the overrides (per offset).
+ *
+ * The mutators don't return errors. If the set of options is too complex to
+ * be translated into kernel uAPI structures - the error will be returned at
+ * the time of the request or reconfiguration. If an invalid value was passed
+ * to any of the mutators - the default value will be silently used instead.
+ *
+ * Operating on offsets in struct line_config has no effect on real GPIOs. It
+ * only manipulates the object in memory and is only applied to the hardware
+ * at the time of the request or reconfiguration.
+ *
+ * Overrides set for offsets that don't end up being requested are silently
+ * ignored both in ::gpiod_chip_request_lines as well as in
+ * ::gpiod_line_request_reconfigure_lines.
+ *
+ * In cases where all requested lines are using the global defaults, the
+ * offsets can be entirely ignored when preparing the line configuration.
+ */
+
+/**
+ * @brief List of properties that can be stored in a line_config object.
+ *
+ * Used when setting the defaults with ::gpiod_line_config_new_defaults and
+ * when retrieving the overrides.
  */
+enum {
+	GPIOD_LINE_CONFIG_PROP_END = 0,
+	/**< Delimiter. */
+	GPIOD_LINE_CONFIG_PROP_DIRECTION,
+	/**< Line direction. */
+	GPIOD_LINE_CONFIG_PROP_EDGE,
+	/**< Edge detection. */
+	GPIOD_LINE_CONFIG_PROP_BIAS,
+	/**< Bias. */
+	GPIOD_LINE_CONFIG_PROP_DRIVE,
+	/**< Drive. */
+	GPIOD_LINE_CONFIG_PROP_ACTIVE_LOW,
+	/**< Active-low setting. */
+	GPIOD_LINE_CONFIG_PROP_DEBOUNCE_PERIOD,
+	/** Debounce period. */
+	GPIOD_LINE_CONFIG_PROP_EVENT_CLOCK,
+	/**< Event clock type. */
+	GPIOD_LINE_CONFIG_PROP_OUTPUT_VALUE,
+	/**< Output value. */
+};
 
 /**
  * @brief Create a new line config object.
@@ -472,6 +527,26 @@ gpiod_info_event_get_line_info(struct gpiod_info_event *event);
  */
 struct gpiod_line_config *gpiod_line_config_new(void);
 
+/**
+ * @brief Create a new line config object with a set of default properties.
+ * @param first First property type.
+ * @return New line config object or NULL on error.
+ *
+ * The list of arguments consists of a number of type->value pairs and must
+ * end with ::GPIOD_LINE_CONFIG_PROP_END.
+ */
+struct gpiod_line_config *gpiod_line_config_new_defaults(int first, ...);
+
+/**
+ * @brief Variant of ::gpiod_line_config_new_defaults taking va_list as
+ *        argument.
+ * @param first First property type.
+ * @param va List of variadic arguments.
+ * @return New line config object or NULL on error.
+ */
+struct gpiod_line_config *
+gpiod_line_config_new_defaults_va(int first, va_list va);
+
 /**
  * @brief Free the line config object and release all associated resources.
  * @param config Line config object to free.
@@ -488,329 +563,428 @@ void gpiod_line_config_free(struct gpiod_line_config *config);
 void gpiod_line_config_reset(struct gpiod_line_config *config);
 
 /**
- * @brief Set the direction of all lines.
+ * @brief Set the default direction setting.
  * @param config Line config object.
  * @param direction New direction.
  */
-void gpiod_line_config_set_direction(struct gpiod_line_config *config,
-				     int direction);
+void gpiod_line_config_set_direction_default(struct gpiod_line_config *config,
+					     int direction);
 
 /**
- * @brief Set the direction for a single line at given offset.
+ * @brief Set the direction override at given offset.
  * @param config Line config object.
  * @param direction New direction.
- * @param offset Offset of the line for which to set the direction.
+ * @param offset Offset of the line for which to set the override.
  */
-void gpiod_line_config_set_direction_offset(struct gpiod_line_config *config,
-					    int direction, unsigned int offset);
+void gpiod_line_config_set_direction_override(struct gpiod_line_config *config,
+					      int direction,
+					      unsigned int offset);
 
 /**
- * @brief Set the direction for a subset of lines.
+ * @brief Clear the direction override at given offset.
  * @param config Line config object.
- * @param direction New direction.
- * @param num_offsets Number of offsets in the array.
- * @param offsets Array of line offsets for which to set the direction.
+ * @param offset Offset of the line for which to clear the override.
+ * @note Does nothing if no override is set for this line.
+ */
+void
+gpiod_line_config_clear_direction_override(struct gpiod_line_config *config,
+					   unsigned int offset);
+
+/**
+ * @brief Check if the direction setting is overridden at given offset.
+ * @param config Line config object.
+ * @param offset Offset of the line for which to check the override.
+ * @return True if direction is overridden at this offset, false otherwise.
  */
-void gpiod_line_config_set_direction_subset(struct gpiod_line_config *config,
-					    int direction,
-					    unsigned int num_offsets,
-					    const unsigned int *offsets);
+bool gpiod_line_config_direction_is_overridden(struct gpiod_line_config *config,
+					       unsigned int offset);
 
 /**
- * @brief Get the direction setting for a given line.
+ * @brief Get the default direction setting.
  * @param config Line config object.
- * @param offset Line offset for which to read the direction setting.
- * @return Direction setting that would have been used for given offset if the
- *         config object was used in a request at the time of the call.
- * @note If an offset is used for which no config was provided, the function
- *       will return the global default value.
+ * @return Direction setting that would have been used for any non-overridden
+ *         offset.
  */
-int gpiod_line_config_get_direction(struct gpiod_line_config *config,
-				    unsigned int offset);
+int gpiod_line_config_get_direction_default(struct gpiod_line_config *config);
 
 /**
- * @brief Set the edge event detection for all lines.
+ * @brief Get the direction setting for the line at given offset.
  * @param config Line config object.
- * @param edge Type of edge events to detect.
+ * @param offset Offset of the line for which to read the direction setting.
+ * @return Direction setting that would have been used for the line at given
+ *         offset if the config object was used in a request at the time of the
+ *         call.
  */
-void gpiod_line_config_set_edge_detection(struct gpiod_line_config *config,
-					  int edge);
+int gpiod_line_config_get_direction_offset(struct gpiod_line_config *config,
+					   unsigned int offset);
 
 /**
- * @brief Set the edge event detection for a single line at given offset.
+ * @brief Set the default edge event detection.
  * @param config Line config object.
  * @param edge Type of edge events to detect.
- * @param offset Offset of the line for which to set the edge detection.
  */
 void
-gpiod_line_config_set_edge_detection_offset(struct gpiod_line_config *config,
-					    int edge, unsigned int offset);
+gpiod_line_config_set_edge_detection_default(struct gpiod_line_config *config,
+					     int edge);
 
 /**
- * @brief Set the edge event detection for a subset of lines.
+ * @brief Set the edge detection override at given offset.
  * @param config Line config object.
  * @param edge Type of edge events to detect.
- * @param num_offsets Number of offsets in the array.
- * @param offsets Array of line offsets for which to set the edge detection.
+ * @param offset Offset of the line for which to set the override.
  */
 void
-gpiod_line_config_set_edge_detection_subset(struct gpiod_line_config *config,
-					    int edge, unsigned int num_offsets,
-					    const unsigned int *offsets);
+gpiod_line_config_set_edge_detection_override(struct gpiod_line_config *config,
+					      int edge, unsigned int offset);
 
 /**
- * @brief Get the edge event detection setting for a given line.
+ * @brief Clear the edge detection override at given offset.
  * @param config Line config object.
- * @param offset Line offset for which to read the edge event detection setting.
+ * @param offset Offset of the line for which to clear the override.
+ * @note Does nothing if no override is set for this line.
+ */
+void
+gpiod_line_config_clear_edge_detection_override(
+			struct gpiod_line_config *config, unsigned int offset);
+
+/**
+ * @brief Check if the edge detection setting is overridden at given offset.
+ * @param config Line config object.
+ * @param offset Offset of the line for which to check the override.
+ * @return True if edge detection is overridden at this offset, false otherwise.
+ */
+bool
+gpiod_line_config_edge_detection_is_overridden(struct gpiod_line_config *config,
+					       unsigned int offset);
+
+/**
+ * @brief Get the default edge detection setting.
+ * @param config Line config object.
+ * @return Edge detection setting that would have been used for any offset not
+ *         assigned its own direction value.
+ */
+int
+gpiod_line_config_get_edge_detection_default(struct gpiod_line_config *config);
+
+/**
+ * @brief Get the edge event detection setting for a given offset.
+ * @param config Line config object.
+ * @param offset Offset of the line for which to read the edge event detection
+ *               setting.
  * @return Edge event detection setting that would have been used for given
  *         offset if the config object was used in a request at the time of
  *         the call.
- * @note If an offset is used for which no config was provided, the function
- *       will return the global default value.
  */
-int gpiod_line_config_get_edge_detection(struct gpiod_line_config *config,
-					 unsigned int offset);
+int
+gpiod_line_config_get_edge_detection_offset(struct gpiod_line_config *config,
+					    unsigned int offset);
 
 /**
- * @brief Set the bias of all lines.
+ * @brief Set the default bias setting.
  * @param config Line config object.
  * @param bias New bias.
  */
-void gpiod_line_config_set_bias(struct gpiod_line_config *config, int bias);
+void gpiod_line_config_set_bias_default(struct gpiod_line_config *config,
+					int bias);
 
 /**
- * @brief Set the bias for a single line at given offset.
+ * @brief Set the bias override at given offset.
  * @param config Line config object.
- * @param bias New bias.
- * @param offset Offset of the line for which to set the bias.
+ * @param bias New bias setting.
+ * @param offset Offset of the line for which to set the override.
  */
-void gpiod_line_config_set_bias_offset(struct gpiod_line_config *config,
-				       int bias, unsigned int offset);
+void gpiod_line_config_set_bias_override(struct gpiod_line_config *config,
+					 int bias, unsigned int offset);
 
 /**
- * @brief Set the bias for a subset of lines.
+ * @brief Clear the bias override at given offset.
  * @param config Line config object.
- * @param bias New bias.
- * @param num_offsets Number of offsets in the array.
- * @param offsets Array of line offsets for which to set the bias.
+ * @param offset Offset of the line for which to clear the override.
+ * @note Does nothing if no override is set for this line.
  */
-void gpiod_line_config_set_bias_subset(struct gpiod_line_config *config,
-				       int bias, unsigned int num_offsets,
-				       const unsigned int *offsets);
+void gpiod_line_config_clear_bias_override(struct gpiod_line_config *config,
+					   unsigned int offset);
 
 /**
- * @brief Get the bias setting for a given line.
+ * @brief Check if the bias setting is overridden at given offset.
  * @param config Line config object.
- * @param offset Line offset for which to read the bias setting.
- * @return Bias setting that would have been used for given offset if the
- *         config object was used in a request at the time of the call.
- * @note If an offset is used for which no config was provided, the function
- *       will return the global default value.
+ * @param offset Offset of the line for which to check the override.
+ * @return True if bias is overridden at this offset, false otherwise.
  */
-int gpiod_line_config_get_bias(struct gpiod_line_config *config,
+bool gpiod_line_config_bias_is_overridden(struct gpiod_line_config *config,
+					  unsigned int offset);
+/**
+ * @brief Get the default bias setting.
+ * @param config Line config object.
+ * @return Bias setting that would have been used for any offset not assigned
+ *         its own direction value.
+ */
+int gpiod_line_config_get_bias_default(struct gpiod_line_config *config);
+
+/**
+ * @brief Get the bias setting for a given offset.
+ * @param config Line config object.
+ * @param offset Offset of the line for which to read the bias setting.
+ * @return Bias setting that would have been used for the line at given offset
+ *         if the config object was used in a request at the time of the call.
+ */
+int gpiod_line_config_get_bias_offset(struct gpiod_line_config *config,
 			       unsigned int offset);
 
 /**
- * @brief Set the drive of all lines.
+ * @brief Set the default drive setting.
  * @param config Line config object.
  * @param drive New drive.
  */
-void gpiod_line_config_set_drive(struct gpiod_line_config *config, int drive);
+void gpiod_line_config_set_drive_default(struct gpiod_line_config *config,
+					 int drive);
 
 /**
- * @brief Set the drive for a single line at given offset.
+ * @brief Set the drive override at given offset.
  * @param config Line config object.
- * @param drive New drive.
- * @param offset Offset of the line for which to set the drive.
+ * @param drive New drive setting.
+ * @param offset Offset of the line for which to set the override.
  */
-void gpiod_line_config_set_drive_offset(struct gpiod_line_config *config,
-					int drive, unsigned int offset);
+void gpiod_line_config_set_drive_override(struct gpiod_line_config *config,
+					  int drive, unsigned int offset);
 
 /**
- * @brief Set the drive for a subset of lines.
+ * @brief Clear the drive override at given offset.
  * @param config Line config object.
- * @param drive New drive.
- * @param num_offsets Number of offsets in the array.
- * @param offsets Array of line offsets for which to set the drive.
+ * @param offset Offset of the line for which to clear the override.
+ * @note Does nothing if no override is set for this line.
  */
-void gpiod_line_config_set_drive_subset(struct gpiod_line_config *config,
-					int drive, unsigned int num_offsets,
-					const unsigned int *offsets);
+void gpiod_line_config_clear_drive_override(struct gpiod_line_config *config,
+					    unsigned int offset);
 
 /**
- * @brief Get the drive setting for a given line.
+ * @brief Check if the drive setting is overridden at given offset.
  * @param config Line config object.
- * @param offset Line offset for which to read the drive setting.
- * @return Drive setting that would have been used for given offset if the
- *         config object was used in a request at the time of the call.
- * @note If an offset is used for which no config was provided, the function
- *       will return the global default value.
+ * @param offset Offset of the line for which to check the override.
+ * @return True if drive is overridden at this offset, false otherwise.
  */
-int gpiod_line_config_get_drive(struct gpiod_line_config *config,
-				unsigned int offset);
+bool gpiod_line_config_drive_is_overridden(struct gpiod_line_config *config,
+					   unsigned int offset);
 
 /**
- * @brief Set all lines as active-low.
+ * @brief Get the default drive setting.
  * @param config Line config object.
+ * @return Drive setting that would have been used for any offset not assigned
+ *         its own direction value.
  */
-void gpiod_line_config_set_active_low(struct gpiod_line_config *config);
+int gpiod_line_config_get_drive_default(struct gpiod_line_config *config);
 
 /**
- * @brief Set a single line as active-low.
+ * @brief Get the drive setting for a given offset.
  * @param config Line config object.
- * @param offset Offset of the line for which to set the active setting.
+ * @param offset Offset of the line for which to read the drive setting.
+ * @return Drive setting that would have been used for the line at given offset
+ *         if the config object was used in a request at the time of the call.
  */
-void gpiod_line_config_set_active_low_offset(struct gpiod_line_config *config,
-					     unsigned int offset);
+int gpiod_line_config_get_drive_offset(struct gpiod_line_config *config,
+				       unsigned int offset);
 
 /**
- * @brief Set a subset of lines as active-low.
+ * @brief Set lines to active-low by default.
  * @param config Line config object.
- * @param num_offsets Number of offsets in the array.
- * @param offsets Array of line offsets for which to set the active setting.
+ * @param active_low New active-low setting.
  */
-void gpiod_line_config_set_active_low_subset(struct gpiod_line_config *config,
-					     unsigned int num_offsets,
-					     const unsigned int *offsets);
+void gpiod_line_config_set_active_low_default(struct gpiod_line_config *config,
+					      bool active_low);
 
 /**
- * @brief Check if the line at given offset was configured as active-low.
+ * @brief Override the active-low setting at given offset.
  * @param config Line config object.
- * @param offset Line offset for which to read the active-low setting.
- * @return Active-low setting that would have been used for given offset if the
- *         config object was used in a request at the time of the call.
- * @note If an offset is used for which no config was provided, the function
- *       will return the global default value.
+ * @param active_low New active-low setting.
+ * @param offset Offset of the line for which to set the override.
  */
-bool gpiod_line_config_get_active_low(struct gpiod_line_config *config,
-				      unsigned int offset);
+void gpiod_line_config_set_active_low_override(struct gpiod_line_config *config,
+					       bool active_low,
+					       unsigned int offset);
 
 /**
- * @brief Set all lines as active-high.
+ * @brief Clear the active-low override at given offset.
  * @param config Line config object.
+ * @param offset Offset of the line for which to clear the override.
+ * @note Does nothing if no override is set for this line.
  */
-void gpiod_line_config_set_active_high(struct gpiod_line_config *config);
+void
+gpiod_line_config_clear_active_low_override(struct gpiod_line_config *config,
+					    unsigned int offset);
 
 /**
- * @brief Set a single line as active-high.
+ * @brief Check if the active-low setting is overridden at given offset.
  * @param config Line config object.
- * @param offset Offset of the line for which to set the active setting.
+ * @param offset Offset of the line for which to check the override.
+ * @return True if active-low is overridden at this offset, false otherwise.
  */
-void gpiod_line_config_set_active_high_offset(struct gpiod_line_config *config,
-					      unsigned int offset);
+bool
+gpiod_line_config_active_low_is_overridden(struct gpiod_line_config *config,
+					   unsigned int offset);
 
 /**
- * @brief Set a subset of lines as active-high.
+ * @brief Check if active-low is the default setting.
  * @param config Line config object.
- * @param num_offsets Number of offsets in the array.
- * @param offsets Array of line offsets for which to set the active setting.
+ * @return Active-low setting that would have been used for any offset not
+ *         assigned its own value.
  */
-void gpiod_line_config_set_active_high_subset(struct gpiod_line_config *config,
-					      unsigned int num_offsets,
-					      const unsigned int *offsets);
+bool gpiod_line_config_get_active_low_default(struct gpiod_line_config *config);
 
 /**
- * @brief Set the debounce period for all lines.
+ * @brief Check if the line at given offset was configured as active-low.
  * @param config Line config object.
- * @param period New debounce period. Disables debouncing if 0.
+ * @param offset Offset of the line for which to read the active-low setting.
+ * @return Active-low setting that would have been used for the line at given
+ *         offset if the config object was used in a request at the time of the
+ *         call.
  */
-void gpiod_line_config_set_debounce_period_us(struct gpiod_line_config *config,
-					      unsigned long period);
+bool gpiod_line_config_get_active_low_offset(struct gpiod_line_config *config,
+					     unsigned int offset);
 
 /**
- * @brief Set the debounce period for a single line at given offset.
+ * @brief Set the default debounce period.
  * @param config Line config object.
  * @param period New debounce period. Disables debouncing if 0.
- * @param offset Offset of the line for which to set the debounce period.
+ */
+void gpiod_line_config_set_debounce_period_us_default(
+		struct gpiod_line_config *config, unsigned long period);
+
+/**
+ * @brief Override the debounce period setting at given offset.
+ * @param config Line config object.
+ * @param period New debounce period in microseconds.
+ * @param offset Offset of the line for which to set the override.
  */
 void
-gpiod_line_config_set_debounce_period_us_offset(
+gpiod_line_config_set_debounce_period_us_override(
 					struct gpiod_line_config *config,
 					unsigned long period,
 					unsigned int offset);
 
 /**
- * @brief Set the debounce period for a subset of lines.
+ * @brief Clear the debounce period override at given offset.
  * @param config Line config object.
- * @param period New debounce period. Disables debouncing if 0.
- * @param num_offsets Number of offsets in the array.
- * @param offsets Array of line offsets for which to set the debounce period.
+ * @param offset Offset of the line for which to clear the override.
+ * @note Does nothing if no override is set for this line.
  */
-void
-gpiod_line_config_set_debounce_period_us_subset(
+void gpiod_line_config_clear_debounce_period_us_override(
 					struct gpiod_line_config *config,
-					unsigned long period,
-					unsigned int num_offsets,
-					const unsigned int *offsets);
+					unsigned int offset);
+
+/**
+ * @brief Check if the debounce period setting is overridden at given offset.
+ * @param config Line config object.
+ * @param offset Offset of the line for which to check the override.
+ * @return True if debounce period is overridden at this offset, false
+ *         otherwise.
+ */
+bool gpiod_line_config_debounce_period_us_is_overridden(
+					struct gpiod_line_config *config,
+					unsigned int offset);
+
+/**
+ * @brief Get the default debounce period.
+ * @param config Line config object.
+ * @return Debounce period that would have been used for any offset not
+ *         assigned its own debounce period. 0 if not debouncing is disabled.
+ */
+unsigned long gpiod_line_config_get_debounce_period_us_default(
+					struct gpiod_line_config *config);
 
 /**
- * @brief Get the debounce period for a given line.
+ * @brief Get the debounce period for a given offset.
  * @param config Line config object.
- * @param offset Line offset for which to read the debounce period.
- * @return Debounce period that would have been used for given offset if the
- *         config object was used in a request at the time of the call.
- * @note If an offset is used for which no config was provided, the function
- *       will return the global default value.
+ * @param offset Offset of the line for which to read the debounce period.
+ * @return Debounce period that would have been used for the line at given
+ *         offset if the config object was used in a request at the time of
+ *         the call. 0 if debouncing is disabled.
  */
 unsigned long
-gpiod_line_config_get_debounce_us_period(struct gpiod_line_config *config,
-					 unsigned int offset);
+gpiod_line_config_get_debounce_period_us_offset(
+			struct gpiod_line_config *config, unsigned int offset);
 
 /**
- * @brief Set the event timestamp clock for all lines.
+ * @brief Set the default event timestamp clock.
  * @param config Line config object.
  * @param clock New clock to use.
  */
-void gpiod_line_config_set_event_clock(struct gpiod_line_config *config,
-				       int clock);
+void gpiod_line_config_set_event_clock_default(struct gpiod_line_config *config,
+					       int clock);
 
 /**
- * @brief Set the event clock for a single line at given offset.
+ * @brief Override the event clock setting at given offset.
  * @param config Line config object.
  * @param clock New event clock to use.
- * @param offset Offset of the line for which to set the event clock type.
+ * @param offset Offset of the line for which to set the override.
  */
-void gpiod_line_config_set_event_clock_offset(struct gpiod_line_config *config,
-					      int clock, unsigned int offset);
+void
+gpiod_line_config_set_event_clock_override(struct gpiod_line_config *config,
+					   int clock, unsigned int offset);
 
 /**
- * @brief Set the event clock for a subset of lines.
+ * @brief Clear the event clock override at given offset.
  * @param config Line config object.
- * @param clock New event clock to use.
- * @param num_offsets Number of offsets in the array.
- * @param offsets Array of line offsets for which to set the event clock type.
+ * @param offset Offset of the line for which to clear the override.
+ * @note Does nothing if no override is set for this line.
+ */
+void
+gpiod_line_config_clear_event_clock_override(struct gpiod_line_config *config,
+					     unsigned int offset);
+
+/**
+ * @brief Check if the event clock setting is overridden at given offset.
+ * @param config Line config object.
+ * @param offset Offset of the line for which to check the override.
+ * @return True if event clock period is overridden at this offset, false
+ *         otherwise.
+ */
+bool
+gpiod_line_config_event_clock_is_overridden(struct gpiod_line_config *config,
+					    unsigned int offset);
+
+/**
+ * @brief Get the default event clock setting.
+ * @param config Line config object.
+ * @return Event clock setting that would have been used for any offset not
+ *         assigned its own direction value.
  */
-void gpiod_line_config_set_event_clock_subset(struct gpiod_line_config *config,
-					      int clock,
-					      unsigned int num_offsets,
-					      const unsigned int *offsets);
+int gpiod_line_config_get_event_clock_default(struct gpiod_line_config *config);
 
 /**
- * @brief Get the event clock setting for a given line.
+ * @brief Get the event clock setting for a given offset.
  * @param config Line config object.
- * @param offset Line offset for which to read the event clock setting.
- * @return Event clock setting that would have been used for given offset if
- *         the config object was used in a request at the time of the call.
- * @note If an offset is used for which no config was provided, the function
- *       will return the global default value.
+ * @param offset Offset of the line for which to read the event clock setting.
+ * @return Event clock setting that would have been used for the line at given
+ *         offset if the config object was used in a request at the time of the
+ *         call.
  */
-int gpiod_line_config_get_event_clock(struct gpiod_line_config *config,
-				      unsigned int offset);
+int gpiod_line_config_get_event_clock_offset(struct gpiod_line_config *config,
+					     unsigned int offset);
 
 /**
- * @brief Set the output value for a single offset.
+ * @brief Set the default output value.
+ * @param config Line config object.
+ * @param value New value.
+ */
+void
+gpiod_line_config_set_output_value_default(struct gpiod_line_config *config,
+					   int value);
+
+/**
+ * @brief Override the output value for a single offset.
  * @param config Line config object.
  * @param offset Offset of the line.
  * @param value Output value to set.
  */
-void gpiod_line_config_set_output_value(struct gpiod_line_config *config,
-					unsigned int offset, int value);
+void
+gpiod_line_config_set_output_value_override(struct gpiod_line_config *config,
+					    unsigned int offset, int value);
 
 /**
- * @brief Set the output values for a set of offsets.
+ * @brief Override the output values for multiple offsets.
  * @param config Line config object.
- * @param num_values Number of offsets for which to set values.
- * @param offsets Array of line offsets to set values for.
+ * @param num_values Number of offsets for which to override values.
+ * @param offsets Array of line offsets to override values for.
  * @param values Array of output values associated with the offsets passed in
  *               the previous argument.
  */
@@ -819,14 +993,69 @@ void gpiod_line_config_set_output_values(struct gpiod_line_config *config,
 					 const unsigned int *offsets,
 					 const int *values);
 
+/**
+ * @brief Clear the output value override at given offset.
+ * @param config Line config object.
+ * @param offset Offset of the line for which to clear the override.
+ * @note Does nothing if no override is set for this line.
+ */
+void
+gpiod_line_config_clear_output_value_override(struct gpiod_line_config *config,
+					      unsigned int offset);
+
+/**
+ * @brief Check if the output value is overridden at given offset.
+ * @param config Line config object.
+ * @param offset Offset of the line for which to check the override.
+ * @return True if output value period is overridden at this offset, false
+ *         otherwise.
+ */
+bool
+gpiod_line_config_output_value_is_overridden(struct gpiod_line_config *config,
+					     unsigned int offset);
+
+/**
+ * @brief Get the default output value.
+ * @param config Line config object.
+ * @return Output value that would have been used for any offset not
+ *         assigned its own output value.
+ */
+int
+gpiod_line_config_get_output_value_default(struct gpiod_line_config *config);
+
 /**
  * @brief Get the output value configured for a given line.
  * @param config Line config object.
  * @param offset Line offset for which to read the value.
- * @return 1 or 0 if the value was configured for this line, -1 otherwise.
+ * @return Output value that would have been used for the line at given offset
+ *         if the config object was used in a request at the time of the call.
  */
-int gpiod_line_config_get_output_value(struct gpiod_line_config *config,
-				       unsigned int offset);
+int gpiod_line_config_get_output_value_offset(struct gpiod_line_config *config,
+					      unsigned int offset);
+
+/**
+ * @brief Get the total number of overridden settings currently stored by this
+ *        line config object.
+ * @param config Line config object.
+ * @return Number of individual overridden settings.
+ */
+unsigned int
+gpiod_line_config_get_num_overrides(struct gpiod_line_config *config);
+
+/**
+ * @brief Get the list of overridden offsets and the corresponding types of
+ *        overridden settings.
+ * @param config Line config object.
+ * @param offsets Array to store the overidden offsets. Must hold at least the
+ *                number of unsigned integers returned by
+ *                ::gpiod_line_config_get_output_value_offset.
+ * @param props Array to store the types of overridden settings. Must hold at
+ *              least the number of integers returned by
+ *              gpiod_line_config_get_output_value_offset.
+ */
+void
+gpiod_line_config_get_overrides(struct gpiod_line_config *config,
+				unsigned int *offsets, int *props);
 
 /**
  * @}
@@ -1028,6 +1257,8 @@ int gpiod_line_request_set_values(struct gpiod_line_request *request,
  * @param request GPIO line request.
  * @param config New line config to apply.
  * @return 0 on success, -1 on failure.
+ * @note Line configuration overrides set for offsets that don't end up being
+ *       requested are silently ignored.
  */
 int gpiod_line_request_reconfigure_lines(struct gpiod_line_request *request,
 					 struct gpiod_line_config *config);
diff --git a/lib/line-config.c b/lib/line-config.c
index 346d331..98f403b 100644
--- a/lib/line-config.c
+++ b/lib/line-config.c
@@ -18,6 +18,7 @@ struct base_config {
 	bool active_low : 1;
 	unsigned int clock : 2;
 	unsigned long debounce_period_us;
+	unsigned int value : 1;
 } GPIOD_PACKED;
 
 #define OVERRIDE_FLAG_DIRECTION		GPIOD_BIT(0)
@@ -27,6 +28,20 @@ struct base_config {
 #define OVERRIDE_FLAG_ACTIVE_LOW	GPIOD_BIT(4)
 #define OVERRIDE_FLAG_CLOCK		GPIOD_BIT(5)
 #define OVERRIDE_FLAG_DEBOUNCE_PERIOD	GPIOD_BIT(6)
+#define OVERRIDE_FLAG_OUTPUT_VALUE	GPIOD_BIT(7)
+
+static const int override_flag_list[] = {
+	OVERRIDE_FLAG_DIRECTION,
+	OVERRIDE_FLAG_EDGE,
+	OVERRIDE_FLAG_BIAS,
+	OVERRIDE_FLAG_DRIVE,
+	OVERRIDE_FLAG_ACTIVE_LOW,
+	OVERRIDE_FLAG_DEBOUNCE_PERIOD,
+	OVERRIDE_FLAG_CLOCK,
+	OVERRIDE_FLAG_OUTPUT_VALUE
+};
+
+#define NUM_OVERRIDE_FLAGS		8
 
 /*
  * Config overriding the defaults for a single line offset. Only flagged
@@ -35,16 +50,13 @@ struct base_config {
 struct override_config {
 	struct base_config base;
 	unsigned int offset;
-	bool value_set : 1;
-	unsigned int value : 1;
-	unsigned int override_flags : 7;
+	unsigned int override_flags : 8;
 } GPIOD_PACKED;
 
 struct gpiod_line_config {
 	bool too_complex;
 	struct base_config defaults;
 	struct override_config overrides[GPIO_V2_LINES_MAX];
-	unsigned int num_overrides;
 };
 
 static void init_base_config(struct base_config *config)
@@ -58,6 +70,17 @@ static void init_base_config(struct base_config *config)
 	config->debounce_period_us = 0;
 }
 
+static void init_override_config(struct override_config *override)
+{
+	override->override_flags = 0;
+	init_base_config(&override->base);
+}
+
+static bool override_used(struct override_config *override)
+{
+	return !!override->override_flags;
+}
+
 GPIOD_API struct gpiod_line_config *gpiod_line_config_new(void)
 {
 	struct gpiod_line_config *config;
@@ -71,6 +94,72 @@ GPIOD_API struct gpiod_line_config *gpiod_line_config_new(void)
 	return config;
 }
 
+GPIOD_API struct gpiod_line_config *
+gpiod_line_config_new_defaults(int first, ...)
+{
+	struct gpiod_line_config *config;
+	va_list va;
+
+	va_start(va, first);
+	config = gpiod_line_config_new_defaults_va(first, va);
+	va_end(va);
+
+	return config;
+}
+
+GPIOD_API struct gpiod_line_config *
+gpiod_line_config_new_defaults_va(int first, va_list va)
+{
+	struct gpiod_line_config *config;
+	int prop;
+
+	config = gpiod_line_config_new();
+	if (!config)
+		return NULL;
+
+	for (prop = first;
+	     prop != GPIOD_LINE_CONFIG_PROP_END;
+	     prop = va_arg(va, int))
+	{
+		switch (prop) {
+		case GPIOD_LINE_CONFIG_PROP_DIRECTION:
+			gpiod_line_config_set_direction_default(config,
+							va_arg(va, int));
+			break;
+		case GPIOD_LINE_CONFIG_PROP_EDGE:
+			gpiod_line_config_set_edge_detection_default(config,
+							va_arg(va, int));
+			break;
+		case GPIOD_LINE_CONFIG_PROP_BIAS:
+			gpiod_line_config_set_bias_default(config,
+							va_arg(va, int));
+			break;
+		case GPIOD_LINE_CONFIG_PROP_DRIVE:
+			gpiod_line_config_set_drive_default(config,
+							va_arg(va, int));
+			break;
+		case GPIOD_LINE_CONFIG_PROP_ACTIVE_LOW:
+			gpiod_line_config_set_active_low_default(config,
+							(bool)va_arg(va, int));
+			break;
+		case GPIOD_LINE_CONFIG_PROP_DEBOUNCE_PERIOD:
+			gpiod_line_config_set_debounce_period_us_default(config,
+						va_arg(va, unsigned long));
+			break;
+		case GPIOD_LINE_CONFIG_PROP_EVENT_CLOCK:
+			gpiod_line_config_set_event_clock_default(config,
+							va_arg(va, int));
+			break;
+		case GPIOD_LINE_CONFIG_PROP_OUTPUT_VALUE:
+			gpiod_line_config_set_output_value_default(config,
+							va_arg(va, int));
+			break;
+		}
+	}
+
+	return config;
+}
+
 GPIOD_API void gpiod_line_config_free(struct gpiod_line_config *config)
 {
 	free(config);
@@ -82,8 +171,8 @@ GPIOD_API void gpiod_line_config_reset(struct gpiod_line_config *config)
 
 	memset(config, 0, sizeof(*config));
 	init_base_config(&config->defaults);
-	for (i = 0; i < GPIO_V2_LINE_NUM_ATTRS_MAX; i++)
-		init_base_config(&config->overrides[i].base);
+	for (i = 0; i < GPIO_V2_LINES_MAX; i++)
+		init_override_config(&config->overrides[i]);
 }
 
 static struct override_config *
@@ -92,7 +181,7 @@ get_override_by_offset(struct gpiod_line_config *config, unsigned int offset)
 	struct override_config *override;
 	unsigned int i;
 
-	for (i = 0; i < config->num_overrides; i++) {
+	for (i = 0; i < GPIO_V2_LINES_MAX; i++) {
 		override = &config->overrides[i];
 
 		if (override->offset == offset)
@@ -103,19 +192,24 @@ get_override_by_offset(struct gpiod_line_config *config, unsigned int offset)
 }
 
 static struct override_config *
-get_new_override(struct gpiod_line_config *config, unsigned int offset)
+get_free_override(struct gpiod_line_config *config, unsigned int offset)
 {
 	struct override_config *override;
+	unsigned int i;
 
-	if (config->num_overrides == GPIO_V2_LINES_MAX) {
-		config->too_complex = true;
-		return NULL;
-	}
+	for (i = 0; i < GPIO_V2_LINES_MAX; i++) {
+		override = &config->overrides[i];
 
-	override = &config->overrides[config->num_overrides++];
-	override->offset = offset;
+		if (override->override_flags)
+			continue;
 
-	return override;
+		override->offset = offset;
+		return override;
+	}
+
+	/* No more free overrides. */
+	config->too_complex = true;
+	return NULL;
 }
 
 static struct override_config *
@@ -129,7 +223,7 @@ get_override_config_for_writing(struct gpiod_line_config *config,
 
 	override = get_override_by_offset(config, offset);
 	if (!override) {
-		override = get_new_override(config, offset);
+		override = get_free_override(config, offset);
 		if (!override)
 			return NULL;
 	}
@@ -139,7 +233,7 @@ get_override_config_for_writing(struct gpiod_line_config *config,
 
 static struct base_config *
 get_base_config_for_reading(struct gpiod_line_config *config,
-			   unsigned int offset, unsigned int flag)
+			    unsigned int offset, unsigned int flag)
 {
 	struct override_config *override;
 
@@ -150,6 +244,35 @@ get_base_config_for_reading(struct gpiod_line_config *config,
 	return &config->defaults;
 }
 
+static void clear_override(struct gpiod_line_config *config,
+			   unsigned int offset, int flag)
+{
+	struct override_config *override;
+
+	override = get_override_config_for_writing(config, offset);
+	if (!override)
+		return;
+
+	if (override->override_flags & flag) {
+		override->override_flags &= ~flag;
+
+		if (!override->override_flags)
+			init_override_config(override);
+	}
+}
+
+static bool check_override(struct gpiod_line_config *config,
+			   unsigned int offset, int flag)
+{
+	struct override_config *override;
+
+	override = get_override_config_for_writing(config, offset);
+	if (!override)
+		return false;
+
+	return override->override_flags & flag;
+}
+
 static void set_direction(struct base_config *config, int direction)
 {
 	switch (direction) {
@@ -165,38 +288,49 @@ static void set_direction(struct base_config *config, int direction)
 }
 
 GPIOD_API void
-gpiod_line_config_set_direction(struct gpiod_line_config *config, int direction)
+gpiod_line_config_set_direction_default(struct gpiod_line_config *config,
+					int direction)
 {
 	set_direction(&config->defaults, direction);
 }
 
 GPIOD_API void
-gpiod_line_config_set_direction_offset(struct gpiod_line_config *config,
+gpiod_line_config_set_direction_override(struct gpiod_line_config *config,
 				       int direction, unsigned int offset)
 {
-	gpiod_line_config_set_direction_subset(config, direction, 1, &offset);
+	struct override_config *override;
+
+	override = get_override_config_for_writing(config, offset);
+	if (!override)
+		return;
+
+	set_direction(&override->base, direction);
+	override->override_flags |= OVERRIDE_FLAG_DIRECTION;
 }
 
 GPIOD_API void
-gpiod_line_config_set_direction_subset(struct gpiod_line_config *config,
-				       int direction, unsigned int num_offsets,
-				       const unsigned int *offsets)
+gpiod_line_config_clear_direction_override(struct gpiod_line_config *config,
+					   unsigned int offset)
 {
-	struct override_config *override;
-	unsigned int i;
+	clear_override(config, offset, OVERRIDE_FLAG_DIRECTION);
+}
 
-	for (i = 0; i < num_offsets; i++) {
-		override = get_override_config_for_writing(config, offsets[i]);
-		if (!override)
-			return;
+GPIOD_API bool
+gpiod_line_config_direction_is_overridden(struct gpiod_line_config *config,
+					 unsigned int offset)
+{
+	return check_override(config, offset, OVERRIDE_FLAG_DIRECTION);
+}
 
-		set_direction(&override->base, direction);
-		override->override_flags |= OVERRIDE_FLAG_DIRECTION;
-	}
+GPIOD_API int
+gpiod_line_config_get_direction_default(struct gpiod_line_config *config)
+{
+	return config->defaults.direction;
 }
 
-GPIOD_API int gpiod_line_config_get_direction(struct gpiod_line_config *config,
-					      unsigned int offset)
+GPIOD_API int
+gpiod_line_config_get_direction_offset(struct gpiod_line_config *config,
+				       unsigned int offset)
 {
 	struct base_config *base;
 
@@ -222,39 +356,49 @@ static void set_edge_detection(struct base_config *config, int edge)
 }
 
 GPIOD_API void
-gpiod_line_config_set_edge_detection(struct gpiod_line_config *config, int edge)
+gpiod_line_config_set_edge_detection_default(struct gpiod_line_config *config,
+					     int edge)
 {
 	set_edge_detection(&config->defaults, edge);
 }
 
 GPIOD_API void
-gpiod_line_config_set_edge_detection_offset(struct gpiod_line_config *config,
-					    int edge, unsigned int offset)
+gpiod_line_config_set_edge_detection_override(struct gpiod_line_config *config,
+					      int edge, unsigned int offset)
 {
-	gpiod_line_config_set_edge_detection_subset(config, edge, 1, &offset);
+	struct override_config *override;
+
+	override = get_override_config_for_writing(config, offset);
+	if (!override)
+		return;
+
+	set_edge_detection(&override->base, edge);
+	override->override_flags |= OVERRIDE_FLAG_EDGE;
 }
 
 GPIOD_API void
-gpiod_line_config_set_edge_detection_subset(struct gpiod_line_config *config,
-					    int edge, unsigned int num_offsets,
-					    const unsigned int *offsets)
+gpiod_line_config_clear_edge_detection_override(
+			struct gpiod_line_config *config, unsigned int offset)
 {
-	struct override_config *override;
-	unsigned int i;
+	clear_override(config, offset, OVERRIDE_FLAG_EDGE);
+}
 
-	for (i = 0; i < num_offsets; i++) {
-		override = get_override_config_for_writing(config, offsets[i]);
-		if (!override)
-			return;
+GPIOD_API bool
+gpiod_line_config_edge_detection_is_overridden(struct gpiod_line_config *config,
+					      unsigned int offset)
+{
+	return check_override(config, offset, OVERRIDE_FLAG_EDGE);
+}
 
-		set_edge_detection(&override->base, edge);
-		override->override_flags |= OVERRIDE_FLAG_EDGE;
-	}
+GPIOD_API int
+gpiod_line_config_get_edge_detection_default(struct gpiod_line_config *config)
+{
+	return config->defaults.edge;
 }
 
 GPIOD_API int
-gpiod_line_config_get_edge_detection(struct gpiod_line_config *config,
-				     unsigned int offset)
+gpiod_line_config_get_edge_detection_offset(struct gpiod_line_config *config,
+					    unsigned int offset)
 {
 	struct base_config *base;
 
@@ -279,38 +423,48 @@ static void set_bias(struct base_config *config, int bias)
 }
 
 GPIOD_API void
-gpiod_line_config_set_bias(struct gpiod_line_config *config, int bias)
+gpiod_line_config_set_bias_default(struct gpiod_line_config *config, int bias)
 {
 	set_bias(&config->defaults, bias);
 }
 
 GPIOD_API void
-gpiod_line_config_set_bias_offset(struct gpiod_line_config *config,
+gpiod_line_config_set_bias_override(struct gpiod_line_config *config,
 				  int bias, unsigned int offset)
 {
-	gpiod_line_config_set_bias_subset(config, bias, 1, &offset);
+	struct override_config *override;
+
+	override = get_override_config_for_writing(config, offset);
+	if (!override)
+		return;
+
+	set_bias(&override->base, bias);
+	override->override_flags |= OVERRIDE_FLAG_BIAS;
 }
 
 GPIOD_API void
-gpiod_line_config_set_bias_subset(struct gpiod_line_config *config,
-				  int bias, unsigned int num_offsets,
-				  const unsigned int *offsets)
+gpiod_line_config_clear_bias_override(struct gpiod_line_config *config,
+				      unsigned int offset)
 {
-	struct override_config *override;
-	unsigned int i;
+	clear_override(config, offset, OVERRIDE_FLAG_BIAS);
+}
 
-	for (i = 0; i < num_offsets; i++) {
-		override = get_override_config_for_writing(config, offsets[i]);
-		if (!override)
-			return;
+GPIOD_API bool
+gpiod_line_config_bias_is_overridden(struct gpiod_line_config *config,
+				     unsigned int offset)
+{
+	return check_override(config, offset, OVERRIDE_FLAG_BIAS);
+}
 
-		set_bias(&override->base, bias);
-		override->override_flags |= OVERRIDE_FLAG_BIAS;
-	}
+GPIOD_API int
+gpiod_line_config_get_bias_default(struct gpiod_line_config *config)
+{
+	return config->defaults.bias;
 }
 
-GPIOD_API int gpiod_line_config_get_bias(struct gpiod_line_config *config,
-					 unsigned int offset)
+GPIOD_API int
+gpiod_line_config_get_bias_offset(struct gpiod_line_config *config,
+				  unsigned int offset)
 {
 	struct base_config *base;
 
@@ -334,38 +488,48 @@ static void set_drive(struct base_config *config, int drive)
 }
 
 GPIOD_API void
-gpiod_line_config_set_drive(struct gpiod_line_config *config, int drive)
+gpiod_line_config_set_drive_default(struct gpiod_line_config *config, int drive)
 {
 	set_drive(&config->defaults, drive);
 }
 
 GPIOD_API void
-gpiod_line_config_set_drive_offset(struct gpiod_line_config *config,
-				   int drive, unsigned int offset)
+gpiod_line_config_set_drive_override(struct gpiod_line_config *config,
+				     int drive, unsigned int offset)
 {
-	gpiod_line_config_set_drive_subset(config, drive, 1, &offset);
+	struct override_config *override;
+
+	override = get_override_config_for_writing(config, offset);
+	if (!override)
+		return;
+
+	set_drive(&override->base, drive);
+	override->override_flags |= OVERRIDE_FLAG_DRIVE;
 }
 
 GPIOD_API void
-gpiod_line_config_set_drive_subset(struct gpiod_line_config *config,
-				   int drive, unsigned int num_offsets,
-				   const unsigned int *offsets)
+gpiod_line_config_clear_drive_override(struct gpiod_line_config *config,
+				       unsigned int offset)
 {
-	struct override_config *override;
-	unsigned int i;
+	clear_override(config, offset, OVERRIDE_FLAG_DRIVE);
+}
 
-	for (i = 0; i < num_offsets; i++) {
-		override = get_override_config_for_writing(config, offsets[i]);
-		if (!override)
-			return;
+GPIOD_API bool
+gpiod_line_config_drive_is_overridden(struct gpiod_line_config *config,
+				      unsigned int offset)
+{
+	return check_override(config, offset, OVERRIDE_FLAG_DRIVE);
+}
 
-		set_drive(&override->base, drive);
-		override->override_flags |= OVERRIDE_FLAG_DRIVE;
-	}
+GPIOD_API int
+gpiod_line_config_get_drive_default(struct gpiod_line_config *config)
+{
+	return config->defaults.drive;
 }
 
-GPIOD_API int gpiod_line_config_get_drive(struct gpiod_line_config *config,
-					  unsigned int offset)
+GPIOD_API int
+gpiod_line_config_get_drive_offset(struct gpiod_line_config *config,
+				   unsigned int offset)
 {
 	struct base_config *base;
 
@@ -375,49 +539,50 @@ GPIOD_API int gpiod_line_config_get_drive(struct gpiod_line_config *config,
 }
 
 GPIOD_API void
-gpiod_line_config_set_active_low(struct gpiod_line_config *config)
+gpiod_line_config_set_active_low_default(struct gpiod_line_config *config,
+					 bool active_low)
 {
-	config->defaults.active_low = true;
+	config->defaults.active_low = active_low;
 }
 
 GPIOD_API void
-gpiod_line_config_set_active_low_offset(struct gpiod_line_config *config,
-					unsigned int offset)
-{
-	gpiod_line_config_set_active_low_subset(config, 1, &offset);
-}
-
-static void
-gpiod_line_config_set_active_setting(struct gpiod_line_config *config,
-				     unsigned int num_offsets,
-				     const unsigned int *offsets,
-				     bool active_low)
+gpiod_line_config_set_active_low_override(struct gpiod_line_config *config,
+					  bool active_low,
+					  unsigned int offset)
 {
 	struct override_config *override;
-	unsigned int i;
 
-	for (i = 0; i < num_offsets; i++) {
-		override = get_override_config_for_writing(config, offsets[i]);
-		if (!override)
-			return;
+	override = get_override_config_for_writing(config, offset);
+	if (!override)
+		return;
 
-		override->base.active_low = active_low;
-		override->override_flags |= OVERRIDE_FLAG_ACTIVE_LOW;
-	}
+	override->base.active_low = active_low;
+	override->override_flags |= OVERRIDE_FLAG_ACTIVE_LOW;
 }
 
 GPIOD_API void
-gpiod_line_config_set_active_low_subset(struct gpiod_line_config *config,
-					unsigned int num_offsets,
-					const unsigned int *offsets)
+gpiod_line_config_clear_active_low_override(struct gpiod_line_config *config,
+					    unsigned int offset)
+{
+	clear_override(config, offset, OVERRIDE_FLAG_ACTIVE_LOW);
+}
+
+GPIOD_API bool
+gpiod_line_config_active_low_is_overridden(struct gpiod_line_config *config,
+					   unsigned int offset)
+{
+	return check_override(config, offset, OVERRIDE_FLAG_ACTIVE_LOW);
+}
+
+GPIOD_API bool
+gpiod_line_config_get_active_low_default(struct gpiod_line_config *config)
 {
-	gpiod_line_config_set_active_setting(config,
-					     num_offsets, offsets, true);
+	return config->defaults.active_low;
 }
 
 GPIOD_API bool
-gpiod_line_config_get_active_low(struct gpiod_line_config *config,
-				 unsigned int offset)
+gpiod_line_config_get_active_low_offset(struct gpiod_line_config *config,
+					unsigned int offset)
 {
 	struct base_config *base;
 
@@ -428,69 +593,52 @@ gpiod_line_config_get_active_low(struct gpiod_line_config *config,
 }
 
 GPIOD_API void
-gpiod_line_config_set_active_high(struct gpiod_line_config *config)
+gpiod_line_config_set_debounce_period_us_default(
+		struct gpiod_line_config *config, unsigned long period)
 {
-	config->defaults.active_low = false;
+	config->defaults.debounce_period_us = period;
 }
 
 GPIOD_API void
-gpiod_line_config_set_active_high_offset(struct gpiod_line_config *config,
-					 unsigned int offset)
+gpiod_line_config_set_debounce_period_us_override(
+					struct gpiod_line_config *config,
+					unsigned long period,
+					unsigned int offset)
 {
-	gpiod_line_config_set_active_high_subset(config, 1, &offset);
-}
+	struct override_config *override;
 
-GPIOD_API void
-gpiod_line_config_set_active_high_subset(struct gpiod_line_config *config,
-					 unsigned int num_offsets,
-					 const unsigned int *offsets)
-{
-	gpiod_line_config_set_active_setting(config,
-					     num_offsets, offsets, false);
-}
+	override = get_override_config_for_writing(config, offset);
+	if (!override)
+		return;
 
-GPIOD_API void
-gpiod_line_config_set_debounce_period_us(struct gpiod_line_config *config,
-				      unsigned long period)
-{
-	config->defaults.debounce_period_us = period;
+	override->base.debounce_period_us = period;
+	override->override_flags |= OVERRIDE_FLAG_DEBOUNCE_PERIOD;
 }
 
-GPIOD_API void
-gpiod_line_config_set_debounce_period_us_offset(
+GPIOD_API void gpiod_line_config_clear_debounce_period_us_override(
 					struct gpiod_line_config *config,
-					unsigned long period,
 					unsigned int offset)
 {
-	gpiod_line_config_set_debounce_period_us_subset(config, period,
-						     1, &offset);
+	clear_override(config, offset, OVERRIDE_FLAG_DEBOUNCE_PERIOD);
 }
 
-GPIOD_API void
-gpiod_line_config_set_debounce_period_us_subset(
+GPIOD_API bool gpiod_line_config_debounce_period_us_is_overridden(
 					struct gpiod_line_config *config,
-					unsigned long period,
-					unsigned int num_offsets,
-					const unsigned int *offsets)
+					unsigned int offset)
 {
-	struct override_config *override;
-	unsigned int i, offset;
-
-	for (i = 0; i < num_offsets; i++) {
-		offset = offsets[i];
-
-		override = get_override_config_for_writing(config, offset);
-		if (!override)
-			return;
+	return check_override(config, offset, OVERRIDE_FLAG_DEBOUNCE_PERIOD);
+}
 
-		override->base.debounce_period_us = period;
-		override->override_flags |= OVERRIDE_FLAG_DEBOUNCE_PERIOD;
-	}
+GPIOD_API unsigned long
+gpiod_line_config_get_debounce_period_us_default(
+					struct gpiod_line_config *config)
+{
+	return config->defaults.debounce_period_us;
 }
 
 GPIOD_API unsigned long
-gpiod_line_config_get_debounce_us_period(struct gpiod_line_config *config,
-					 unsigned int offset)
+gpiod_line_config_get_debounce_period_us_offset(
+			struct gpiod_line_config *config, unsigned int offset)
 {
 	struct base_config *base;
 
@@ -514,38 +662,49 @@ static void set_event_clock(struct base_config *config, int clock)
 }
 
 GPIOD_API void
-gpiod_line_config_set_event_clock(struct gpiod_line_config *config, int clock)
+gpiod_line_config_set_event_clock_default(struct gpiod_line_config *config,
+					  int clock)
 {
 	set_event_clock(&config->defaults, clock);
 }
 
 GPIOD_API void
-gpiod_line_config_set_event_clock_offset(struct gpiod_line_config *config,
-					 int clock, unsigned int offset)
+gpiod_line_config_set_event_clock_override(struct gpiod_line_config *config,
+					   int clock, unsigned int offset)
 {
-	gpiod_line_config_set_event_clock_subset(config, clock, 1, &offset);
+	struct override_config *override;
+
+	override = get_override_config_for_writing(config, offset);
+	if (!override)
+		return;
+
+	set_event_clock(&override->base, clock);
+	override->override_flags |= OVERRIDE_FLAG_CLOCK;
 }
 
 GPIOD_API void
-gpiod_line_config_set_event_clock_subset(struct gpiod_line_config *config,
-					 int clock, unsigned int num_offsets,
-					 const unsigned int *offsets)
+gpiod_line_config_clear_event_clock_override(struct gpiod_line_config *config,
+					     unsigned int offset)
 {
-	struct override_config *override;
-	unsigned int i;
+	clear_override(config, offset, OVERRIDE_FLAG_CLOCK);
+}
 
-	for (i = 0; i < num_offsets; i++) {
-		override = get_override_config_for_writing(config, offsets[i]);
-		if (!override)
-			return;
+GPIOD_API bool
+gpiod_line_config_event_clock_is_overridden(struct gpiod_line_config *config,
+					    unsigned int offset)
+{
+	return check_override(config, offset, OVERRIDE_FLAG_CLOCK);
+}
 
-		set_event_clock(&override->base, clock);
-	}
+GPIOD_API int
+gpiod_line_config_get_event_clock_default(struct gpiod_line_config *config)
+{
+	return config->defaults.clock;
 }
 
 GPIOD_API int
-gpiod_line_config_get_event_clock(struct gpiod_line_config *config,
-				  unsigned int offset)
+gpiod_line_config_get_event_clock_offset(struct gpiod_line_config *config,
+					 unsigned int offset)
 {
 	struct base_config *base;
 
@@ -554,17 +713,25 @@ gpiod_line_config_get_event_clock(struct gpiod_line_config *config,
 	return base->clock;
 }
 
-static void set_output_value(struct override_config *override, int value)
+GPIOD_API void
+gpiod_line_config_set_output_value_default(struct gpiod_line_config *config,
+					   int value)
 {
-	override->value = !!value;
-	override->value_set = true;
+	config->defaults.value = value;
 }
 
 GPIOD_API void
-gpiod_line_config_set_output_value(struct gpiod_line_config *config,
-				   unsigned int offset, int value)
+gpiod_line_config_set_output_value_override(struct gpiod_line_config *config,
+					  unsigned int offset, int value)
 {
-	gpiod_line_config_set_output_values(config, 1, &offset, &value);
+	struct override_config *override;
+
+	override = get_override_config_for_writing(config, offset);
+	if (!override)
+		return;
+
+	override->base.value = !!value;
+	override->override_flags |= OVERRIDE_FLAG_OUTPUT_VALUE;
 }
 
 GPIOD_API void
@@ -573,38 +740,147 @@ gpiod_line_config_set_output_values(struct gpiod_line_config *config,
 				    const unsigned int *offsets,
 				    const int *values)
 {
-	struct override_config *override;
-	unsigned int i, offset, val;
+	unsigned int i;
 
-	for (i = 0; i < num_values; i++) {
-		offset = offsets[i];
-		val = values[i];
+	for (i = 0; i < num_values; i++)
+		gpiod_line_config_set_output_value_override(config,
+							    offsets[i],
+							    values[i]);
+}
 
-		override = get_override_by_offset(config, offset);
-		if (!override) {
-			override = get_new_override(config, offset);
-			if (!override)
-				return;
-		}
+GPIOD_API void
+gpiod_line_config_clear_output_value_override(struct gpiod_line_config *config,
+					      unsigned int offset)
+{
+	clear_override(config, offset, OVERRIDE_FLAG_OUTPUT_VALUE);
+}
 
-		set_output_value(override, val);
-	}
+GPIOD_API bool
+gpiod_line_config_output_value_is_overridden(struct gpiod_line_config *config,
+					     unsigned int offset)
+{
+	return check_override(config, offset, OVERRIDE_FLAG_OUTPUT_VALUE);
 }
 
 GPIOD_API int
-gpiod_line_config_get_output_value(struct gpiod_line_config *config,
-				   unsigned int offset)
+gpiod_line_config_get_output_value_default(struct gpiod_line_config *config)
+{
+	return config->defaults.value;
+}
+
+GPIOD_API int
+gpiod_line_config_get_output_value_offset(struct gpiod_line_config *config,
+					  unsigned int offset)
 {
 	struct override_config *override;
 
 	override = get_override_by_offset(config, offset);
-	if (override && override->value_set)
-		return override->value;
+	if (override && (override->override_flags & OVERRIDE_FLAG_OUTPUT_VALUE))
+		return override->base.value;
+
+	return config->defaults.value;
+}
+
+static bool base_config_flags_are_equal(struct base_config *base,
+					struct override_config *override)
+{
+	if (((override->override_flags & OVERRIDE_FLAG_DIRECTION) &&
+	     base->direction != override->base.direction) ||
+	    ((override->override_flags & OVERRIDE_FLAG_EDGE) &&
+	     base->edge != override->base.edge) ||
+	    ((override->override_flags & OVERRIDE_FLAG_DRIVE) &&
+	     base->drive != override->base.drive) ||
+	    ((override->override_flags & OVERRIDE_FLAG_BIAS) &&
+	     base->bias != override->base.bias) ||
+	    ((override->override_flags & OVERRIDE_FLAG_ACTIVE_LOW) &&
+	     base->active_low != override->base.active_low) ||
+	    ((override->override_flags & OVERRIDE_FLAG_CLOCK) &&
+	     base->clock != override->base.clock))
+		return false;
+
+	return true;
+}
+
+static bool base_debounce_period_is_equal(struct base_config *base,
+					  struct override_config *override)
+{
+	if ((override->override_flags & OVERRIDE_FLAG_DEBOUNCE_PERIOD) &&
+	    base->debounce_period_us != override->base.debounce_period_us)
+		return false;
+
+	return true;
+}
+
+GPIOD_API unsigned int
+gpiod_line_config_get_num_overrides(struct gpiod_line_config *config)
+{
+	struct override_config *override;
+	unsigned int i, j, count = 0;
 
-	errno = ENXIO;
+	for (i = 0; i < GPIO_V2_LINES_MAX; i++) {
+		override = &config->overrides[i];
+
+		if (override_used(override)) {
+			for (j = 0; j < NUM_OVERRIDE_FLAGS; j++) {
+				if (override->override_flags &
+				    override_flag_list[j])
+					count++;
+			}
+		}
+	}
+
+	return count;
+}
+
+static int override_flag_to_prop(int flag)
+{
+	switch (flag) {
+	case OVERRIDE_FLAG_DIRECTION:
+		return GPIOD_LINE_CONFIG_PROP_DIRECTION;
+	case OVERRIDE_FLAG_EDGE:
+		return GPIOD_LINE_CONFIG_PROP_EDGE;
+	case OVERRIDE_FLAG_BIAS:
+		return GPIOD_LINE_CONFIG_PROP_BIAS;
+	case OVERRIDE_FLAG_DRIVE:
+		return GPIOD_LINE_CONFIG_PROP_DRIVE;
+	case OVERRIDE_FLAG_ACTIVE_LOW:
+		return GPIOD_LINE_CONFIG_PROP_ACTIVE_LOW;
+	case OVERRIDE_FLAG_DEBOUNCE_PERIOD:
+		return GPIOD_LINE_CONFIG_PROP_DEBOUNCE_PERIOD;
+	case OVERRIDE_FLAG_CLOCK:
+		return GPIOD_LINE_CONFIG_PROP_EVENT_CLOCK;
+	case OVERRIDE_FLAG_OUTPUT_VALUE:
+		return GPIOD_LINE_CONFIG_PROP_OUTPUT_VALUE;
+	}
+
+	/* Can't happen. */
 	return -1;
 }
 
+GPIOD_API void
+gpiod_line_config_get_overrides(struct gpiod_line_config *config,
+				unsigned int *offsets, int *props)
+{
+	struct override_config *override;
+	unsigned int i, j, count = 0;
+
+	for (i = 0; i < GPIO_V2_LINES_MAX; i++) {
+		override = &config->overrides[i];
+
+		if (override_used(override)) {
+			for (j = 0; j < NUM_OVERRIDE_FLAGS; j++) {
+				if (override->override_flags &
+				    override_flag_list[j]) {
+					offsets[count] = override->offset;
+					props[count] = override_flag_to_prop(
+							override_flag_list[j]);
+					count++;
+				}
+			}
+		}
+	}
+}
+
 static uint64_t make_kernel_flags(const struct base_config *config)
 {
 	uint64_t flags = 0;
@@ -683,10 +959,10 @@ static int find_bitmap_index(unsigned int needle, unsigned int num_lines,
 	return -1;
 }
 
-static int set_kernel_output_values(uint64_t *mask, uint64_t *vals,
-				    struct gpiod_line_config *config,
-				    unsigned int num_lines,
-				    const unsigned int *offsets)
+static void set_kernel_output_values(uint64_t *mask, uint64_t *vals,
+				     struct gpiod_line_config *config,
+				     unsigned int num_lines,
+				     const unsigned int *offsets)
 {
 	struct override_config *override;
 	unsigned int i;
@@ -695,44 +971,69 @@ static int set_kernel_output_values(uint64_t *mask, uint64_t *vals,
 	gpiod_line_mask_zero(mask);
 	gpiod_line_mask_zero(vals);
 
-	for (i = 0; i < config->num_overrides; i++) {
-		override = &config->overrides[i];
+	if (config->defaults.direction == GPIOD_LINE_DIRECTION_OUTPUT) {
+		/*
+		 * Default direction is output - assign the default output
+		 * value to all lines. Overrides that may set some lines to
+		 * input will be handled later and may re-assign the output
+		 * values.
+		 */
+		for (i = 0; i < num_lines; i++) {
+			gpiod_line_mask_set_bit(mask, i);
+			gpiod_line_mask_assign_bit(vals, i,
+						   config->defaults.value);
+		}
+	} else {
+		/*
+		 * Default output value is not output. Iterate over overrides
+		 * and set the default output value for those that override the
+		 * direction to output. Don't touch the ones which override
+		 * the output value.
+		 */
+		for (i = 0; i < GPIO_V2_LINES_MAX; i++) {
+			override = &config->overrides[i];
+
+			if (override->base.direction !=
+			    GPIOD_LINE_DIRECTION_OUTPUT ||
+			    !(override->override_flags &
+			      OVERRIDE_FLAG_DIRECTION) ||
+			    (override->override_flags &
+			     OVERRIDE_FLAG_OUTPUT_VALUE))
+				continue;
 
-		if (override->value_set) {
 			idx = find_bitmap_index(override->offset,
 						num_lines, offsets);
-			if (idx < 0) {
-				errno = EINVAL;
-				return -1;
-			}
+			if (idx < 0)
+				continue;
 
 			gpiod_line_mask_set_bit(mask, idx);
 			gpiod_line_mask_assign_bit(vals, idx,
-						   !!override->value);
+						   !!config->defaults.value);
 		}
 	}
 
-	return 0;
-}
+	/*
+	 * Finally iterate over the overrides again and set the overridden
+	 * output values.
+	 */
+	for (i = 0; i < GPIO_V2_LINES_MAX; i++) {
+		override = &config->overrides[i];
 
-static bool base_config_flags_are_equal(struct base_config *base,
-					struct override_config *override)
-{
-	if (((override->override_flags & OVERRIDE_FLAG_DIRECTION) &&
-	     base->direction != override->base.direction) ||
-	    ((override->override_flags & OVERRIDE_FLAG_EDGE) &&
-	     base->edge != override->base.edge) ||
-	    ((override->override_flags & OVERRIDE_FLAG_DRIVE) &&
-	     base->drive != override->base.drive) ||
-	    ((override->override_flags & OVERRIDE_FLAG_BIAS) &&
-	     base->bias != override->base.bias) ||
-	    ((override->override_flags & OVERRIDE_FLAG_ACTIVE_LOW) &&
-	     base->active_low != override->base.active_low) ||
-	    ((override->override_flags & OVERRIDE_FLAG_CLOCK) &&
-	     base->clock != override->base.clock))
-		return false;
+		if (!(override->override_flags & OVERRIDE_FLAG_OUTPUT_VALUE))
+			continue;
 
-	return true;
+		if (config->defaults.direction != GPIOD_LINE_DIRECTION_OUTPUT &&
+		    (!(override->override_flags & OVERRIDE_FLAG_DIRECTION) ||
+		     override->base.direction != GPIOD_LINE_DIRECTION_OUTPUT))
+			continue;
+
+		idx = find_bitmap_index(override->offset, num_lines, offsets);
+		if (idx < 0)
+			continue;
+
+		gpiod_line_mask_set_bit(mask, idx);
+		gpiod_line_mask_assign_bit(vals, idx, !!override->base.value);
+	}
 }
 
 static bool override_config_flags_are_equal(struct override_config *a,
@@ -771,16 +1072,6 @@ static void set_base_config_flags(struct gpio_v2_line_attribute *attr,
 	attr->flags = make_kernel_flags(&base);
 }
 
-static bool base_debounce_period_is_equal(struct base_config *base,
-					  struct override_config *override)
-{
-	if ((override->override_flags & OVERRIDE_FLAG_DEBOUNCE_PERIOD) &&
-	    base->debounce_period_us != override->base.debounce_period_us)
-		return false;
-
-	return true;
-}
-
 static bool override_config_debounce_period_is_equal(struct override_config *a,
 						     struct override_config *b)
 {
@@ -801,36 +1092,40 @@ set_base_config_debounce_period(struct gpio_v2_line_attribute *attr,
 	attr->debounce_period_us = override->base.debounce_period_us;
 }
 
-static int set_kernel_attr_mask(uint64_t *out, const uint64_t *in,
-				unsigned int num_lines,
-				const unsigned int *offsets,
-				const struct gpiod_line_config *config)
+static void set_kernel_attr_mask(uint64_t *out, const uint64_t *in,
+				 unsigned int num_lines,
+				 const unsigned int *offsets,
+				 struct gpiod_line_config *config)
 {
+	struct override_config *override;
 	unsigned int i, j;
 	int off;
 
 	gpiod_line_mask_zero(out);
 
-	for (i = 0; i < config->num_overrides; i++) {
-		if (!gpiod_line_mask_test_bit(in, i))
+	for (i = 0; i < GPIO_V2_LINES_MAX; i++) {
+		override = &config->overrides[i];
+
+		if (!override_used(override) ||
+		    !gpiod_line_mask_test_bit(in, i))
 			continue;
 
 		for (j = 0, off = -1; j < num_lines; j++) {
-			if (offsets[j] == config->overrides[i].offset) {
+			if (offsets[j] == override->offset) {
 				off = j;
 				break;
 			}
 		}
 
-		if (off < 0) {
-			errno = EINVAL;
-			return -1;
-		}
+		/*
+		 * Overridden offsets that are not in the list of offsets to
+		 * request (or already requested) are silently ignored.
+		 */
+		if (off < 0)
+			continue;
 
 		gpiod_line_mask_set_bit(out, off);
 	}
-
-	return 0;
 }
 
 static int process_overrides(struct gpiod_line_config *config,
@@ -852,11 +1147,18 @@ static int process_overrides(struct gpiod_line_config *config,
 	struct override_config *current, *next;
 	unsigned int i, j;
 
-	for (i = 0; i < config->num_overrides; i++) {
-		if (gpiod_line_mask_test_bit(&processed, i))
+	for (i = 0; i < GPIO_V2_LINES_MAX; i++) {
+		current = &config->overrides[i];
+
+		if (!override_used(current) ||
+		    gpiod_line_mask_test_bit(&processed, i))
 			continue;
 
-		current = &config->overrides[i];
+		if (*attr_idx == GPIO_V2_LINE_NUM_ATTRS_MAX) {
+			errno = E2BIG;
+			return -1;
+		}
+
 		gpiod_line_mask_set_bit(&processed, i);
 
 		if (defaults_equal_func(&config->defaults, current))
@@ -865,12 +1167,13 @@ static int process_overrides(struct gpiod_line_config *config,
 		marked = 0;
 		gpiod_line_mask_set_bit(&marked, i);
 
-		for (j = i + 1; j < config->num_overrides; j++) {
-			if (gpiod_line_mask_test_bit(&processed, j))
-				continue;
-
+		for (j = i + 1; j < GPIO_V2_LINES_MAX; j++) {
 			next = &config->overrides[j];
 
+			if (!override_used(next) ||
+			    gpiod_line_mask_test_bit(&processed, j))
+				continue;
+
 			if (override_equal_func(current, next)) {
 				gpiod_line_mask_set_bit(&marked, j);
 				gpiod_line_mask_set_bit(&processed, j);
@@ -878,10 +1181,6 @@ static int process_overrides(struct gpiod_line_config *config,
 		}
 
 		attr = &cfgbuf->attrs[(*attr_idx)++];
-		if (*attr_idx == GPIO_V2_LINE_NUM_ATTRS_MAX) {
-			errno = E2BIG;
-			return -1;
-		}
 
 		set_kernel_attr_mask(&mask, &marked,
 				     num_lines, offsets, config);
@@ -892,14 +1191,31 @@ static int process_overrides(struct gpiod_line_config *config,
 	return 0;
 }
 
+static bool has_at_least_one_output_direction(struct gpiod_line_config *config)
+{
+	struct override_config *override;
+	unsigned int i;
+
+	if (config->defaults.direction == GPIOD_LINE_DIRECTION_OUTPUT)
+		return true;
+
+	for (i = 0; i < GPIO_V2_LINES_MAX; i++) {
+		override = &config->overrides[i];
+
+		if (override->base.direction == GPIOD_LINE_DIRECTION_OUTPUT)
+			return true;
+	}
+
+	return false;
+}
+
 int gpiod_line_config_to_kernel(struct gpiod_line_config *config,
 				struct gpio_v2_line_config *cfgbuf,
 				unsigned int num_lines,
 				const unsigned int *offsets)
 {
 	struct gpio_v2_line_config_attribute *attr;
-	struct override_config *override;
-	unsigned int attr_idx = 0, i;
+	unsigned int attr_idx = 0;
 	uint64_t mask, values;
 	int ret;
 
@@ -907,26 +1223,19 @@ int gpiod_line_config_to_kernel(struct gpiod_line_config *config,
 		goto err_2big;
 
 	/*
-	 * First check if we have at least one default output value configured.
+	 * First check if we have at least one line configured in output mode.
 	 * If so, let's take one attribute for the default values.
 	 */
-	for (i = 0; i < config->num_overrides; i++) {
-		override = &config->overrides[i];
-
-		if (override->value_set) {
-			attr = &cfgbuf->attrs[attr_idx++];
-			attr->attr.id = GPIO_V2_LINE_ATTR_ID_OUTPUT_VALUES;
+	if (has_at_least_one_output_direction(config)) {
+		attr = &cfgbuf->attrs[attr_idx++];
+		attr->attr.id = GPIO_V2_LINE_ATTR_ID_OUTPUT_VALUES;
 
-			ret = set_kernel_output_values(&mask, &values, config,
-						       num_lines, offsets);
-			if (ret)
-				return ret;
+		set_kernel_output_values(&mask, &values, config,
+					 num_lines, offsets);
 
-			attr->attr.values = values;
-			attr->mask = mask;
+		attr->attr.values = values;
+		attr->mask = mask;
 
-			break;
-		}
 	}
 
 	/* If we have a default debounce period - use another attribute. */
diff --git a/tools/gpioget.c b/tools/gpioget.c
index 965af3b..112257c 100644
--- a/tools/gpioget.c
+++ b/tools/gpioget.c
@@ -110,13 +110,13 @@ int main(int argc, char **argv)
 	if (!line_cfg)
 		die_perror("unable to allocate the line config structure");
 
-	gpiod_line_config_set_direction(line_cfg, direction);
+	gpiod_line_config_set_direction_default(line_cfg, direction);
 
 	if (bias)
-		gpiod_line_config_set_bias(line_cfg, bias);
+		gpiod_line_config_set_bias_default(line_cfg, bias);
 
 	if (active_low)
-		gpiod_line_config_set_active_low(line_cfg);
+		gpiod_line_config_set_active_low_default(line_cfg, true);
 
 	req_cfg = gpiod_request_config_new();
 	if (!req_cfg)
diff --git a/tools/gpiomon.c b/tools/gpiomon.c
index c09f2f5..31ea294 100644
--- a/tools/gpiomon.c
+++ b/tools/gpiomon.c
@@ -259,10 +259,10 @@ int main(int argc, char **argv)
 		die_perror("unable to allocate the line config structure");
 
 	if (bias)
-		gpiod_line_config_set_bias(line_cfg, bias);
+		gpiod_line_config_set_bias_default(line_cfg, bias);
 	if (active_low)
-		gpiod_line_config_set_active_low(line_cfg);
-	gpiod_line_config_set_edge_detection(line_cfg, edge);
+		gpiod_line_config_set_active_low_default(line_cfg, true);
+	gpiod_line_config_set_edge_detection_default(line_cfg, edge);
 
 	req_cfg = gpiod_request_config_new();
 	if (!req_cfg)
diff --git a/tools/gpioset.c b/tools/gpioset.c
index 55fcfe9..c27525a 100644
--- a/tools/gpioset.c
+++ b/tools/gpioset.c
@@ -296,12 +296,13 @@ int main(int argc, char **argv)
 		die_perror("unable to allocate the line config structure");
 
 	if (bias)
-		gpiod_line_config_set_bias(line_cfg, bias);
+		gpiod_line_config_set_bias_default(line_cfg, bias);
 	if (drive)
-		gpiod_line_config_set_drive(line_cfg, drive);
+		gpiod_line_config_set_drive_default(line_cfg, drive);
 	if (active_low)
-		gpiod_line_config_set_active_low(line_cfg);
-	gpiod_line_config_set_direction(line_cfg, GPIOD_LINE_DIRECTION_OUTPUT);
+		gpiod_line_config_set_active_low_default(line_cfg, true);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_OUTPUT);
 	gpiod_line_config_set_output_values(line_cfg, num_lines,
 					    offsets, values);
 
-- 
2.30.1


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [libgpiod v2][PATCH v3 3/3] tests: rewrite core C tests using libgpiosim
  2022-03-03  9:18 [libgpiod v2][PATCH v3 0/3] libgpiod v2: rewrite tests for the C library Bartosz Golaszewski
  2022-03-03  9:18 ` [libgpiod v2][PATCH v3 1/3] API: add an enum for line values Bartosz Golaszewski
  2022-03-03  9:18 ` [libgpiod v2][PATCH v3 2/3] line-config: expose the override logic to users Bartosz Golaszewski
@ 2022-03-03  9:18 ` Bartosz Golaszewski
  2022-03-05  5:51   ` Kent Gibson
  2 siblings, 1 reply; 8+ messages in thread
From: Bartosz Golaszewski @ 2022-03-03  9:18 UTC (permalink / raw)
  To: Kent Gibson, Linus Walleij, Andy Shevchenko
  Cc: linux-gpio, Bartosz Golaszewski

This replaces the old tests for the C API v1 based on gpio-mockup with
a test suite based on gpio-sim that covers around 95% of the libgpiod v2
codebase.

The test harness has been rebuilt and shrank significantly as well. The
libgpiosim API has been wrapped in a gobject interface.

Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
---
 configure.ac                 |    8 +-
 tests/Makefile.am            |   24 +-
 tests/gpiod-test-helpers.c   |   49 ++
 tests/gpiod-test-helpers.h   |  139 +++++
 tests/gpiod-test-sim.c       |  308 ++++++++++
 tests/gpiod-test-sim.h       |   42 ++
 tests/gpiod-test.c           |  233 +-------
 tests/gpiod-test.h           |   83 +--
 tests/gpiosim/gpiosim.c      |    1 +
 tests/mockup/Makefile.am     |   11 -
 tests/mockup/gpio-mockup.c   |  496 ----------------
 tests/mockup/gpio-mockup.h   |   36 --
 tests/tests-chip.c           |  282 ++++-----
 tests/tests-edge-event.c     |  490 +++++++++++++++
 tests/tests-event.c          |  908 ----------------------------
 tests/tests-info-event.c     |  301 ++++++++++
 tests/tests-line-config.c    |  503 ++++++++++++++++
 tests/tests-line-info.c      |  318 ++++++++++
 tests/tests-line-request.c   |  526 ++++++++++++++++
 tests/tests-line.c           | 1091 ----------------------------------
 tests/tests-misc.c           |   80 ++-
 tests/tests-request-config.c |   90 +++
 22 files changed, 2995 insertions(+), 3024 deletions(-)
 create mode 100644 tests/gpiod-test-helpers.c
 create mode 100644 tests/gpiod-test-helpers.h
 create mode 100644 tests/gpiod-test-sim.c
 create mode 100644 tests/gpiod-test-sim.h
 delete mode 100644 tests/mockup/Makefile.am
 delete mode 100644 tests/mockup/gpio-mockup.c
 delete mode 100644 tests/mockup/gpio-mockup.h
 create mode 100644 tests/tests-edge-event.c
 delete mode 100644 tests/tests-event.c
 create mode 100644 tests/tests-info-event.c
 create mode 100644 tests/tests-line-config.c
 create mode 100644 tests/tests-line-info.c
 create mode 100644 tests/tests-line-request.c
 delete mode 100644 tests/tests-line.c
 create mode 100644 tests/tests-request-config.c

diff --git a/configure.ac b/configure.ac
index cb4c1fd..f8d34ed 100644
--- a/configure.ac
+++ b/configure.ac
@@ -28,9 +28,8 @@ AC_SUBST(VERSION_STR, [$PACKAGE_VERSION$EXTRA_VERSION])
 AC_SUBST(ABI_VERSION, [4.1.2])
 # Have a separate ABI version for C++ bindings:
 AC_SUBST(ABI_CXX_VERSION, [2.1.1])
-# ABI version for libgpiomockup (we need this since it can be installed if we
+# ABI version for libgpiosim (we need this since it can be installed if we
 # enable install-tests).
-AC_SUBST(ABI_MOCKUP_VERSION, [0.1.0])
 AC_SUBST(ABI_GPIOSIM_VERSION, [0.1.0])
 
 AC_CONFIG_AUX_DIR([autostuff])
@@ -138,14 +137,14 @@ AC_DEFUN([FUNC_NOT_FOUND_TESTS],
 
 if test "x$with_tests" = xtrue
 then
-	# For libgpiomockup & libgpiosim
+	# For libgpiosim
 	AC_CHECK_FUNC([qsort], [], [FUNC_NOT_FOUND_TESTS([qsort])])
 	PKG_CHECK_MODULES([KMOD], [libkmod >= 18])
-	PKG_CHECK_MODULES([UDEV], [libudev >= 215])
 	PKG_CHECK_MODULES([MOUNT], [mount >= 2.33.1])
 
 	# For core library tests
 	PKG_CHECK_MODULES([GLIB], [glib-2.0 >= 2.50])
+	PKG_CHECK_MODULES([GOBJECT], [gobject-2.0 >= 2.50])
 
 	if test "x$with_tools" = xtrue
 	then
@@ -236,7 +235,6 @@ AC_CONFIG_FILES([Makefile
 		 lib/libgpiod.pc
 		 tools/Makefile
 		 tests/Makefile
-		 tests/mockup/Makefile
 		 tests/gpiosim/Makefile
 		 bindings/cxx/libgpiodcxx.pc
 		 bindings/Makefile
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 2b1e082..dce9a5a 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -1,24 +1,32 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 # SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <brgl@bgdev.pl>
 
-SUBDIRS = mockup gpiosim
-
+SUBDIRS = gpiosim
+ 
 AM_CFLAGS = -I$(top_srcdir)/include/ -I$(top_srcdir)/tests/gpiosim/
 AM_CFLAGS += -include $(top_builddir)/config.h
-AM_CFLAGS += -Wall -Wextra -g -std=gnu89 $(GLIB_CFLAGS)
+AM_CFLAGS += -Wall -Wextra -g -std=gnu89 $(GLIB_CFLAGS) $(GOBJECT_CFLAGS)
 AM_CFLAGS += -DG_LOG_DOMAIN=\"gpiod-test\"
 AM_CFLAGS += $(PROFILING_CFLAGS)
-AM_LDFLAGS = -pthread $(PROFILING_LDFLAGS)
+AM_LDFLAGS = -pthread
 LDADD = $(top_builddir)/lib/libgpiod.la
 LDADD += $(top_builddir)/tests/gpiosim/libgpiosim.la
-LDADD += $(GLIB_LIBS)
+LDADD += $(GLIB_LIBS) $(GOBJECT_LIBS)
 
 bin_PROGRAMS = gpiod-test
 
 gpiod_test_SOURCES =			\
 		gpiod-test.c		\
 		gpiod-test.h		\
+		gpiod-test-helpers.c	\
+		gpiod-test-helpers.h	\
+		gpiod-test-sim.c	\
+		gpiod-test-sim.h	\
 		tests-chip.c		\
-		tests-event.c		\
-		tests-line.c		\
-		tests-misc.c
+		tests-edge-event.c	\
+		tests-info-event.c	\
+		tests-line-config.c	\
+		tests-line-info.c	\
+		tests-line-request.c	\
+		tests-misc.c		\
+		tests-request-config.c
diff --git a/tests/gpiod-test-helpers.c b/tests/gpiod-test-helpers.c
new file mode 100644
index 0000000..24a6ee4
--- /dev/null
+++ b/tests/gpiod-test-helpers.c
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+/*
+ * Testing framework for the core library.
+ *
+ * This file contains functions and definitions extending the GLib unit testing
+ * framework with functionalities necessary to test the libgpiod core C API as
+ * well as the kernel-to-user-space interface.
+ */
+
+#include "gpiod-test-helpers.h"
+
+GVariant *
+gpiod_test_package_line_names(const struct gpiod_test_line_name *names)
+{
+	const struct gpiod_test_line_name *name;
+	GVariantBuilder *builder;
+	GVariant *ret;
+
+	builder = g_variant_builder_new(G_VARIANT_TYPE("a(us)"));
+
+	for (name = &names[0]; name->name; name++)
+		g_variant_builder_add(builder, "(us)",
+				      name->offset, name->name);
+
+	ret = g_variant_new("a(us)", builder);
+	g_variant_builder_unref(builder);
+
+	return ret;
+}
+
+GVariant *gpiod_test_package_hogs(const struct gpiod_test_hog *hogs)
+{
+	const struct gpiod_test_hog *hog;
+	GVariantBuilder *builder;
+	GVariant *ret;
+
+	builder = g_variant_builder_new(G_VARIANT_TYPE("a(usi)"));
+
+	for (hog = &hogs[0]; hog->name; hog++)
+		g_variant_builder_add(builder, "(usi)",
+				      hog->offset, hog->name, hog->direction);
+
+	ret = g_variant_new("a(usi)", builder);
+	g_variant_builder_unref(builder);
+
+	return ret;
+}
diff --git a/tests/gpiod-test-helpers.h b/tests/gpiod-test-helpers.h
new file mode 100644
index 0000000..4aa4202
--- /dev/null
+++ b/tests/gpiod-test-helpers.h
@@ -0,0 +1,139 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#ifndef __GPIOD_TEST_HELPERS_H__
+#define __GPIOD_TEST_HELPERS_H__
+
+#include <errno.h>
+#include <glib.h>
+#include <gpiod.h>
+
+#include "gpiod-test-sim.h"
+
+/*
+ * These typedefs are needed to make g_autoptr work - it doesn't accept
+ * regular 'struct typename' syntax.
+ */
+
+typedef struct gpiod_chip struct_gpiod_chip;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_chip, gpiod_chip_close);
+
+typedef struct gpiod_line_info struct_gpiod_line_info;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_line_info, gpiod_line_info_free);
+
+typedef struct gpiod_info_event struct_gpiod_info_event;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_info_event, gpiod_info_event_free);
+
+typedef struct gpiod_line_config struct_gpiod_line_config;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_line_config, gpiod_line_config_free);
+
+typedef struct gpiod_request_config struct_gpiod_request_config;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_request_config,
+			      gpiod_request_config_free);
+
+typedef struct gpiod_line_request struct_gpiod_line_request;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_line_request,
+			      gpiod_line_request_release);
+
+typedef struct gpiod_edge_event struct_gpiod_edge_event;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_edge_event, gpiod_edge_event_free);
+
+typedef struct gpiod_edge_event_buffer struct_gpiod_edge_event_buffer;
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(struct_gpiod_edge_event_buffer,
+			      gpiod_edge_event_buffer_free);
+
+#define gpiod_test_return_if_failed() \
+	do { \
+		if (g_test_failed()) \
+			return; \
+	} while (0)
+
+#define gpiod_test_join_thread_and_return_if_failed(_thread) \
+	do { \
+		if (g_test_failed()) { \
+			g_thread_join(_thread); \
+			return; \
+		} \
+	} while (0)
+
+#define gpiod_test_open_chip_or_fail(_path) \
+	({ \
+		struct gpiod_chip *_chip = gpiod_chip_open((_path)); \
+		g_assert_nonnull(_chip); \
+		gpiod_test_return_if_failed(); \
+		_chip; \
+	})
+
+#define gpiod_test_get_line_info_or_fail(_chip, _offset) \
+	({ \
+		struct gpiod_line_info *_info = \
+				gpiod_chip_get_line_info((_chip), (_offset)); \
+		g_assert_nonnull(_info); \
+		gpiod_test_return_if_failed(); \
+		_info; \
+	})
+
+#define gpiod_test_create_line_config_or_fail() \
+	({ \
+		struct gpiod_line_config *_config = \
+				gpiod_line_config_new(); \
+		g_assert_nonnull(_config); \
+		gpiod_test_return_if_failed(); \
+		_config; \
+	})
+
+#define gpiod_test_create_edge_event_buffer_or_fail(_capacity) \
+	({ \
+		struct gpiod_edge_event_buffer *_buffer = \
+				gpiod_edge_event_buffer_new(_capacity); \
+		g_assert_nonnull(_buffer); \
+		gpiod_test_return_if_failed(); \
+		_buffer; \
+	})
+
+#define gpiod_test_create_request_config_or_fail() \
+	({ \
+		struct gpiod_request_config *_config = \
+				gpiod_request_config_new(); \
+		g_assert_nonnull(_config); \
+		gpiod_test_return_if_failed(); \
+		_config; \
+	})
+
+#define gpiod_test_request_lines_or_fail(_chip, _req_cfg, _line_cfg) \
+	({ \
+		struct gpiod_line_request *_request = \
+			gpiod_chip_request_lines((_chip), \
+						 (_req_cfg), (_line_cfg)); \
+		g_assert_nonnull(_request); \
+		gpiod_test_return_if_failed(); \
+		_request; \
+	})
+
+#define gpiod_test_reconfigure_lines_or_fail(_request, _line_cfg) \
+	do { \
+		gint ret = gpiod_line_request_reconfigure_lines((_request), \
+								(_line_cfg)); \
+		g_assert_cmpint(ret, ==, 0); \
+		gpiod_test_return_if_failed(); \
+	} while (0)
+
+#define gpiod_test_expect_errno(_expected) \
+	g_assert_cmpint((_expected), ==, errno)
+
+struct gpiod_test_line_name {
+	guint offset;
+	const gchar *name;
+};
+
+struct gpiod_test_hog {
+	guint offset;
+	const gchar *name;
+	GPIOSimHogDir direction;
+};
+
+GVariant *
+gpiod_test_package_line_names(const struct gpiod_test_line_name *names);
+GVariant *gpiod_test_package_hogs(const struct gpiod_test_hog *hogs);
+
+#endif /* __GPIOD_TEST_HELPERS_H__ */
diff --git a/tests/gpiod-test-sim.c b/tests/gpiod-test-sim.c
new file mode 100644
index 0000000..fd4b8cc
--- /dev/null
+++ b/tests/gpiod-test-sim.c
@@ -0,0 +1,308 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#include <errno.h>
+#include <gpiosim.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "gpiod-test-sim.h"
+
+struct _GPIOSimChip {
+	GObject parent_instance;
+	struct gpiosim_bank *bank;
+};
+
+enum {
+	G_GPIOSIM_CHIP_PROP_DEV_PATH = 1,
+	G_GPIOSIM_CHIP_PROP_NAME,
+	G_GPIOSIM_CHIP_PROP_NUM_LINES,
+	G_GPIOSIM_CHIP_PROP_LABEL,
+	G_GPIOSIM_CHIP_PROP_LINE_NAMES,
+	G_GPIOSIM_CHIP_PROP_HOGS,
+};
+
+static struct gpiosim_ctx *sim_ctx;
+static pid_t pid;
+static guint sim_id;
+
+G_DEFINE_TYPE(GPIOSimChip, g_gpiosim_chip, G_TYPE_OBJECT);
+
+static void g_gpiosim_ctx_unref(void)
+{
+	gpiosim_ctx_unref(sim_ctx);
+}
+
+static void g_gpiosim_ctx_init(void)
+{
+	sim_ctx = gpiosim_ctx_new();
+	if (!sim_ctx)
+		g_error("Unable to initialize libgpiosim: %s",
+			g_strerror(errno));
+
+	atexit(g_gpiosim_ctx_unref);
+	pid = getpid();
+}
+
+static void g_gpiosim_chip_constructed(GObject *obj)
+{
+	GPIOSimChip *self = G_GPIOSIM_CHIP(obj);
+	struct gpiosim_dev *dev;
+	gint ret;
+
+	dev = gpiosim_bank_get_dev(self->bank);
+	ret = gpiosim_dev_enable(dev);
+	gpiosim_dev_unref(dev);
+	if (ret)
+		g_error("Error while trying to enable the simulated GPIO device: %s",
+			g_strerror(errno));
+}
+
+static void g_gpiosim_chip_get_property(GObject *obj, guint prop_id,
+					GValue *val, GParamSpec *pspec)
+{
+	GPIOSimChip *self = G_GPIOSIM_CHIP(obj);
+
+	switch (prop_id) {
+	case G_GPIOSIM_CHIP_PROP_DEV_PATH:
+		g_value_set_static_string(val,
+				gpiosim_bank_get_dev_path(self->bank));
+		break;
+	case G_GPIOSIM_CHIP_PROP_NAME:
+		g_value_set_static_string(val,
+				gpiosim_bank_get_chip_name(self->bank));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
+		break;
+	}
+}
+
+static void g_gpiosim_chip_set_property(GObject *obj, guint prop_id,
+					const GValue *val, GParamSpec *pspec)
+{
+	GPIOSimChip *self = G_GPIOSIM_CHIP(obj);
+	gint ret, vdir, dir;
+	GVariantIter *iter;
+	GVariant *variant;
+	guint offset;
+	gchar *name;
+
+	switch (prop_id) {
+	case G_GPIOSIM_CHIP_PROP_NUM_LINES:
+		ret = gpiosim_bank_set_num_lines(self->bank,
+						 g_value_get_uint(val));
+		if (ret)
+			g_error("Unable to set the number of lines exposed by the simulated chip: %s",
+				g_strerror(errno));
+		break;
+	case G_GPIOSIM_CHIP_PROP_LABEL:
+		ret = gpiosim_bank_set_label(self->bank,
+					     g_value_get_string(val));
+		if (ret)
+			g_error("Unable to set the label of the simulated chip: %s",
+				g_strerror(errno));
+		break;
+	case G_GPIOSIM_CHIP_PROP_LINE_NAMES:
+		variant = g_value_get_variant(val);
+		if (!variant)
+			break;
+
+		iter = g_variant_iter_new(variant);
+
+		while (g_variant_iter_loop(iter, "(us)", &offset, &name)) {
+			ret = gpiosim_bank_set_line_name(self->bank,
+							 offset, name);
+			if (ret)
+				g_error("Unable to set the name of the simulated GPIO line: %s",
+					g_strerror(errno));
+		}
+
+		g_variant_iter_free(iter);
+		break;
+	case G_GPIOSIM_CHIP_PROP_HOGS:
+		variant = g_value_get_variant(val);
+		if (!variant)
+			break;
+
+		iter = g_variant_iter_new(variant);
+
+		while (g_variant_iter_loop(iter, "(usi)",
+					   &offset, &name, &vdir)) {
+			switch (vdir) {
+			case G_GPIOSIM_HOG_DIR_INPUT:
+				dir = GPIOSIM_HOG_DIR_INPUT;
+				break;
+			case G_GPIOSIM_HOG_DIR_OUTPUT_HIGH:
+				dir = GPIOSIM_HOG_DIR_OUTPUT_HIGH;
+				break;
+			case G_GPIOSIM_HOG_DIR_OUTPUT_LOW:
+				dir = GPIOSIM_HOG_DIR_OUTPUT_LOW;
+				break;
+			default:
+				g_error("Invalid hog direction value: %d",
+					vdir);
+			}
+
+			ret = gpiosim_bank_hog_line(self->bank,
+						    offset, name, dir);
+			if (ret)
+				g_error("Unable to hog the simulated GPIO line: %s",
+					g_strerror(errno));
+		}
+
+		g_variant_iter_free(iter);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
+		break;
+	}
+}
+
+static void g_gpiosim_chip_dispose(GObject *obj)
+{
+	GPIOSimChip *self = G_GPIOSIM_CHIP(obj);
+	struct gpiosim_dev *dev;
+	gint ret;
+
+	dev = gpiosim_bank_get_dev(self->bank);
+
+	if (gpiosim_dev_is_live(dev)) {
+		ret = gpiosim_dev_disable(dev);
+		if (ret)
+			g_error("Error while trying to disable the simulated GPIO device: %s",
+				g_strerror(errno));
+	}
+
+	gpiosim_dev_unref(dev);
+	sim_id--;
+}
+
+static void g_gpiosim_chip_finalize(GObject *obj)
+{
+	GPIOSimChip *self = G_GPIOSIM_CHIP(obj);
+
+	gpiosim_bank_unref(self->bank);
+
+	G_OBJECT_CLASS(g_gpiosim_chip_parent_class)->finalize(obj);
+}
+
+static void g_gpiosim_chip_class_init(GPIOSimChipClass *chip_class)
+{
+	GObjectClass *class = G_OBJECT_CLASS(chip_class);
+
+	class->constructed = g_gpiosim_chip_constructed;
+	class->get_property = g_gpiosim_chip_get_property;
+	class->set_property = g_gpiosim_chip_set_property;
+	class->dispose = g_gpiosim_chip_dispose;
+	class->finalize = g_gpiosim_chip_finalize;
+
+	g_object_class_install_property(class, G_GPIOSIM_CHIP_PROP_DEV_PATH,
+		g_param_spec_string("dev-path", "Device path",
+			"Character device filesystem path.", NULL,
+			G_PARAM_READABLE));
+
+	g_object_class_install_property(class, G_GPIOSIM_CHIP_PROP_NAME,
+		g_param_spec_string("name", "Chip name",
+			"Name of this chip device as set by the kernel.", NULL,
+			G_PARAM_READABLE));
+
+	g_object_class_install_property(class, G_GPIOSIM_CHIP_PROP_NUM_LINES,
+		g_param_spec_uint("num-lines", "Number of lines",
+			"Number of lines this simulated chip exposes.",
+			1, G_MAXUINT, 1,
+			G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+
+	g_object_class_install_property(class, G_GPIOSIM_CHIP_PROP_LABEL,
+		g_param_spec_string("label", "Chip label",
+			"Label of this simulated chip.", NULL,
+			G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+
+	g_object_class_install_property(class, G_GPIOSIM_CHIP_PROP_LINE_NAMES,
+		g_param_spec_variant("line-names", "Line names",
+			"List of names of the lines exposed by this chip",
+			(GVariantType *)"a(us)", NULL,
+			G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+
+	g_object_class_install_property(class, G_GPIOSIM_CHIP_PROP_HOGS,
+		g_param_spec_variant("hogs", "Line hogs",
+			"List of hogged lines and their directions.",
+			(GVariantType *)"a(usi)", NULL,
+			G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+}
+
+static void g_gpiosim_chip_init(GPIOSimChip *self)
+{
+	g_autofree gchar *dev_name = NULL;
+	struct gpiosim_dev *dev;
+
+	if (!sim_ctx)
+		g_gpiosim_ctx_init();
+
+	dev_name = g_strdup_printf("gpiod-test-dev.%u.%u", pid, sim_id++);
+	dev = gpiosim_dev_new(sim_ctx, dev_name);
+	if (!dev)
+		g_error("Unable to instantiate new GPIO device: %s",
+			g_strerror(errno));
+
+	self->bank = gpiosim_bank_new(dev, "bank");
+	gpiosim_dev_unref(dev);
+	if (!self->bank)
+		g_error("Unable to instantiate new GPIO bank: %s",
+			g_strerror(errno));
+}
+
+static const gchar *
+g_gpiosim_chip_get_string_prop(GPIOSimChip *self, const gchar *prop)
+{
+	GValue val = G_VALUE_INIT;
+	const gchar *str;
+
+	g_object_get_property(G_OBJECT(self), prop, &val);
+	str = g_value_get_string(&val);
+	g_value_unset(&val);
+
+	return str;
+}
+
+const gchar *g_gpiosim_chip_get_dev_path(GPIOSimChip *self)
+{
+	return g_gpiosim_chip_get_string_prop(self, "dev-path");
+}
+
+const gchar *g_gpiosim_chip_get_name(GPIOSimChip *self)
+{
+	return g_gpiosim_chip_get_string_prop(self, "name");
+}
+
+gint g_gpiosim_chip_get_value(GPIOSimChip *chip, guint offset)
+{
+	gint val;
+
+	val = gpiosim_bank_get_value(chip->bank, offset);
+	if (val < 0)
+		g_error("Unable to read the line value: %s", g_strerror(errno));
+
+	return val;
+}
+
+void g_gpiosim_chip_set_pull(GPIOSimChip *chip, guint offset, GPIOSimPull pull)
+{
+	gint ret, sim_pull;
+
+	switch (pull) {
+	case G_GPIOSIM_PULL_DOWN:
+		sim_pull = GPIOSIM_PULL_DOWN;
+		break;
+	case G_GPIOSIM_PULL_UP:
+		sim_pull = GPIOSIM_PULL_UP;
+		break;
+	default:
+		g_error("invalid pull value");
+	}
+
+	ret = gpiosim_bank_set_pull(chip->bank, offset, sim_pull);
+	if (ret)
+		g_error("Unable to set the pull setting for simulated line: %s",
+			g_strerror(errno));
+}
diff --git a/tests/gpiod-test-sim.h b/tests/gpiod-test-sim.h
new file mode 100644
index 0000000..0cc2a0b
--- /dev/null
+++ b/tests/gpiod-test-sim.h
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#ifndef __GPIOD_TEST_SIM_H__
+#define __GPIOD_TEST_SIM_H__
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+typedef enum {
+	G_GPIOSIM_PULL_UP = 1,
+	G_GPIOSIM_PULL_DOWN,
+} GPIOSimPull;
+
+typedef enum {
+	G_GPIOSIM_HOG_DIR_INPUT = 1,
+	G_GPIOSIM_HOG_DIR_OUTPUT_HIGH,
+	G_GPIOSIM_HOG_DIR_OUTPUT_LOW,
+} GPIOSimHogDir;
+
+typedef struct _GPIOSimChip GPIOSimChip;
+
+G_DECLARE_FINAL_TYPE(GPIOSimChip, g_gpiosim_chip, G_GPIOSIM, CHIP, GObject);
+
+#define G_GPIOSIM_TYPE_CHIP (g_gpiosim_chip_get_type())
+#define G_GPIOSIM_CHIP(obj) \
+	(G_TYPE_CHECK_INSTANCE_CAST((obj), G_GPIOSIM_TYPE_CHIP, GPIOSimChip))
+
+#define g_gpiosim_chip_new(...) \
+	G_GPIOSIM_CHIP(g_object_new(G_GPIOSIM_TYPE_CHIP, __VA_ARGS__))
+
+const gchar *g_gpiosim_chip_get_dev_path(GPIOSimChip *self);
+const gchar *g_gpiosim_chip_get_name(GPIOSimChip *self);
+
+gint g_gpiosim_chip_get_value(GPIOSimChip *self, guint offset);
+void g_gpiosim_chip_set_pull(GPIOSimChip *self, guint offset, GPIOSimPull pull);
+
+G_END_DECLS
+
+#endif /* __GPIOD_TEST_SIM_H__ */
diff --git a/tests/gpiod-test.c b/tests/gpiod-test.c
index aa9eaa4..df546a1 100644
--- a/tests/gpiod-test.c
+++ b/tests/gpiod-test.c
@@ -2,8 +2,6 @@
 // SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <brgl@bgdev.pl>
 
 #include <errno.h>
-#include <glib/gstdio.h>
-#include <gpiosim.h>
 #include <linux/version.h>
 #include <stdio.h>
 #include <sys/utsname.h>
@@ -12,28 +10,13 @@
 #include "gpiod-test.h"
 
 #define MIN_KERNEL_MAJOR	5
-#define MIN_KERNEL_MINOR	10
+#define MIN_KERNEL_MINOR	16
 #define MIN_KERNEL_RELEASE	0
 #define MIN_KERNEL_VERSION	KERNEL_VERSION(MIN_KERNEL_MAJOR, \
 					       MIN_KERNEL_MINOR, \
 					       MIN_KERNEL_RELEASE)
 
-struct gpiod_test_event_thread {
-	GThread *id;
-	GMutex lock;
-	GCond cond;
-	gboolean should_stop;
-	guint chip_index;
-	guint line_offset;
-	guint period_ms;
-};
-
-static struct {
-	GList *tests;
-	struct gpiosim_ctx *gpiosim;
-	GPtrArray *sim_chips;
-	GPtrArray *sim_banks;
-} globals;
+static GList *tests;
 
 static void check_kernel(void)
 {
@@ -63,103 +46,16 @@ static void check_kernel(void)
 	return;
 }
 
-static void remove_gpiosim_chip(gpointer data)
-{
-	struct gpiosim_dev *dev = data;
-	gint ret;
-
-	ret = gpiosim_dev_disable(dev);
-	if (ret)
-		g_error("unable to uncommit a simulated GPIO device: %s",
-			g_strerror(errno));
-
-	gpiosim_dev_unref(dev);
-}
-
-static void remove_gpiosim_bank(gpointer data)
-{
-	struct gpiosim_bank *bank = data;
-
-	gpiosim_bank_unref(bank);
-}
-
 static void test_func_wrapper(gconstpointer data)
 {
-	const _GpiodTestCase *test = data;
-	struct gpiosim_bank *sim_bank;
-	struct gpiosim_dev *sim_dev;
-	gchar *line_name, *label;
-	gchar chip_idx;
-	guint i, j;
-	gint ret;
-
-	globals.sim_chips = g_ptr_array_new_full(test->num_chips,
-						 remove_gpiosim_chip);
-	globals.sim_banks = g_ptr_array_new_full(test->num_chips,
-						 remove_gpiosim_bank);
-
-	for (i = 0; i < test->num_chips; i++) {
-		chip_idx = i + 65;
-
-		sim_dev = gpiosim_dev_new(globals.gpiosim, NULL);
-		if (!sim_dev)
-			g_error("unable to create a simulated GPIO chip: %s",
-				g_strerror(errno));
-
-		sim_bank = gpiosim_bank_new(sim_dev, NULL);
-		if (!sim_bank)
-			g_error("unable to create a simulated GPIO bank: %s",
-				g_strerror(errno));
-
-		label = g_strdup_printf("gpio-mockup-%c", chip_idx);
-		ret = gpiosim_bank_set_label(sim_bank, label);
-		g_free(label);
-		if (ret)
-			g_error("unable to set simulated chip label: %s",
-				g_strerror(errno));
-
-		ret = gpiosim_bank_set_num_lines(sim_bank, test->chip_sizes[i]);
-		if (ret)
-			g_error("unable to set the number of lines for a simulated chip: %s",
-				g_strerror(errno));
-
-		if (test->flags & GPIOD_TEST_FLAG_NAMED_LINES) {
-			for (j = 0; j < test->chip_sizes[i]; j++) {
-				line_name = g_strdup_printf("gpio-mockup-%c-%u",
-							    chip_idx, j);
-
-				ret = gpiosim_bank_set_line_name(sim_bank, j,
-								 line_name);
-				g_free(line_name);
-				if (ret)
-					g_error("unable to set the line names for a simulated bank: %s",
-						g_strerror(errno));
-			}
-		}
-
-		ret = gpiosim_dev_enable(sim_dev);
-		if (ret)
-			g_error("unable to commit the simulated GPIO device: %s",
-				g_strerror(errno));
-
-		g_ptr_array_add(globals.sim_chips, sim_dev);
-		g_ptr_array_add(globals.sim_banks, sim_bank);
-	}
+	const struct _gpiod_test_case *test = data;
 
 	test->func();
-
-	g_ptr_array_unref(globals.sim_banks);
-	g_ptr_array_unref(globals.sim_chips);
-}
-
-static void unref_gpiosim(void)
-{
-	gpiosim_ctx_unref(globals.gpiosim);
 }
 
 static void add_test_from_list(gpointer element, gpointer data G_GNUC_UNUSED)
 {
-	_GpiodTestCase *test = element;
+	struct _gpiod_test_case *test = element;
 
 	g_test_add_data_func(test->path, test, test_func_wrapper);
 }
@@ -170,128 +66,17 @@ int main(gint argc, gchar **argv)
 	g_test_set_nonfatal_assertions();
 
 	g_debug("running libgpiod test suite");
-	g_debug("%u tests registered", g_list_length(globals.tests));
-
-	/*
-	 * Setup libpiosim first so that it runs its own kernel version
-	 * check before we tell the user our local requirements are met as
-	 * well.
-	 */
-	globals.gpiosim = gpiosim_ctx_new();
-	if (!globals.gpiosim)
-		g_error("unable to initialize gpiosim library: %s",
-			g_strerror(errno));
-	atexit(unref_gpiosim);
+	g_debug("%u tests registered", g_list_length(tests));
 
 	check_kernel();
 
-	g_list_foreach(globals.tests, add_test_from_list, NULL);
-	g_list_free(globals.tests);
+	g_list_foreach(tests, add_test_from_list, NULL);
+	g_list_free(tests);
 
 	return g_test_run();
 }
 
-void _gpiod_test_register(_GpiodTestCase *test)
-{
-	globals.tests = g_list_append(globals.tests, test);
-}
-
-const gchar *gpiod_test_chip_path(guint idx)
-{
-	struct gpiosim_bank *bank = g_ptr_array_index(globals.sim_banks, idx);
-
-	return gpiosim_bank_get_dev_path(bank);
-}
-
-const gchar *gpiod_test_chip_name(guint idx)
-{
-	struct gpiosim_bank *bank = g_ptr_array_index(globals.sim_banks, idx);
-
-	return gpiosim_bank_get_chip_name(bank);
-}
-
-gint gpiod_test_chip_get_value(guint chip_index, guint line_offset)
-{
-	struct gpiosim_bank *bank = g_ptr_array_index(globals.sim_banks,
-						      chip_index);
-	gint ret;
-
-	ret = gpiosim_bank_get_value(bank, line_offset);
-	if (ret < 0)
-		g_error("unable to read line value from gpiosim: %s",
-			g_strerror(errno));
-
-	return ret;
-}
-
-void gpiod_test_chip_set_pull(guint chip_index, guint line_offset, gint pull)
-{
-	struct gpiosim_bank *bank = g_ptr_array_index(globals.sim_banks,
-						      chip_index);
-	gint ret;
-
-	ret = gpiosim_bank_set_pull(bank, line_offset,
-				    pull ? GPIOSIM_PULL_UP : GPIOSIM_PULL_DOWN);
-	if (ret)
-		g_error("unable to set line pull in gpiosim: %s",
-			g_strerror(errno));
-}
-
-static gpointer event_worker_func(gpointer data)
-{
-	GpiodTestEventThread *thread = data;
-	gboolean signalled;
-	gint64 end_time;
-	gint i;
-
-	for (i = 0;; i++) {
-		g_mutex_lock(&thread->lock);
-		if (thread->should_stop) {
-			g_mutex_unlock(&thread->lock);
-			break;
-		}
-
-		end_time = g_get_monotonic_time() + thread->period_ms * 1000;
-
-		signalled = g_cond_wait_until(&thread->cond,
-					      &thread->lock, end_time);
-		if (!signalled)
-			gpiod_test_chip_set_pull(thread->chip_index,
-						 thread->line_offset, i % 2);
-
-		g_mutex_unlock(&thread->lock);
-	}
-
-	return NULL;
-}
-
-GpiodTestEventThread *
-gpiod_test_start_event_thread(guint chip_index, guint line_offset, guint period_ms)
+void _gpiod_test_register(struct _gpiod_test_case *test)
 {
-	GpiodTestEventThread *thread = g_malloc0(sizeof(*thread));
-
-	g_mutex_init(&thread->lock);
-	g_cond_init(&thread->cond);
-
-	thread->chip_index = chip_index;
-	thread->line_offset = line_offset;
-	thread->period_ms = period_ms;
-
-	thread->id = g_thread_new("event-worker", event_worker_func, thread);
-
-	return thread;
-}
-
-void gpiod_test_stop_event_thread(GpiodTestEventThread *thread)
-{
-	g_mutex_lock(&thread->lock);
-	thread->should_stop = TRUE;
-	g_cond_broadcast(&thread->cond);
-	g_mutex_unlock(&thread->lock);
-
-	(void)g_thread_join(thread->id);
-
-	g_mutex_clear(&thread->lock);
-	g_cond_clear(&thread->cond);
-	g_free(thread);
+	tests = g_list_append(tests, test);
 }
diff --git a/tests/gpiod-test.h b/tests/gpiod-test.h
index 1f2a677..6a84162 100644
--- a/tests/gpiod-test.h
+++ b/tests/gpiod-test.h
@@ -13,86 +13,33 @@
 #define __GPIOD_TEST_H__
 
 #include <glib.h>
-#include <gpiod.h>
-
-/*
- * These typedefs are needed to make g_autoptr work - it doesn't accept
- * regular 'struct typename' syntax.
- */
-typedef struct gpiod_chip gpiod_chip_struct;
-typedef struct gpiod_line_bulk gpiod_line_bulk_struct;
-
-G_DEFINE_AUTOPTR_CLEANUP_FUNC(gpiod_chip_struct, gpiod_chip_unref);
-G_DEFINE_AUTOPTR_CLEANUP_FUNC(gpiod_line_bulk_struct, gpiod_line_bulk_free);
 
 /* These are private definitions and should not be used directly. */
-typedef void (*_gpiod_test_func)(void);
 
-typedef struct _gpiod_test_case _GpiodTestCase;
 struct _gpiod_test_case {
 	const gchar *path;
-	_gpiod_test_func func;
-
-	guint num_chips;
-	guint *chip_sizes;
-	gint flags;
+	void (*func)(void);
 };
 
-void _gpiod_test_register(_GpiodTestCase *test);
+void _gpiod_test_register(struct _gpiod_test_case *test);
 
 #define _GPIOD_TEST_PATH(_name) \
 		"/gpiod/" GPIOD_TEST_GROUP "/" G_STRINGIFY(_name)
 
-enum {
-	/* Dummy lines for this test case should have names assigned. */
-	GPIOD_TEST_FLAG_NAMED_LINES = (1 << 0),
-};
-
 /*
- * Register a test case function. The last argument is the array of numbers
- * of lines per simulated chip.
+ * Register a test case function.
  */
-#define GPIOD_TEST_CASE(_name, _flags, ...)				\
-	static void _name(void);					\
-	static guint _##_name##_chip_sizes[] = __VA_ARGS__;		\
-	static _GpiodTestCase _##_name##_test_case = {			\
-		.path = _GPIOD_TEST_PATH(_name),			\
-		.func = _name,						\
-		.num_chips = G_N_ELEMENTS(_##_name##_chip_sizes),	\
-		.chip_sizes = _##_name##_chip_sizes,			\
-		.flags = _flags,					\
-	};								\
-	static __attribute__((constructor)) void			\
-	_##_name##_test_register(void)					\
-	{								\
-		_gpiod_test_register(&_##_name##_test_case);		\
-	}								\
-	static void _name(void)
-
-#define GPIOD_TEST_CONSUMER "gpiod-test"
-
-#define gpiod_test_return_if_failed()					\
-	do {								\
-		if (g_test_failed())					\
-			return;						\
-	} while (0)
-
-/* Wrappers around libgpiomockup helpers. */
-const gchar *gpiod_test_chip_path(guint idx);
-const gchar *gpiod_test_chip_name(guint idx);
-gint gpiod_test_chip_get_value(guint chip_index, guint line_offset);
-void gpiod_test_chip_set_pull(guint chip_index, guint line_offset, gint pull);
-
-/* Helpers for triggering line events in a separate thread. */
-struct gpiod_test_event_thread;
-typedef struct gpiod_test_event_thread GpiodTestEventThread;
-
-GpiodTestEventThread *
-gpiod_test_start_event_thread(guint chip_index,
-			      guint line_offset, guint period_ms);
-void gpiod_test_stop_event_thread(GpiodTestEventThread *thread);
-
-G_DEFINE_AUTOPTR_CLEANUP_FUNC(GpiodTestEventThread,
-			      gpiod_test_stop_event_thread);
+#define GPIOD_TEST_CASE(_name) \
+	static void _gpiod_test_func_##_name(void); \
+	static struct _gpiod_test_case _##_name##_test_case = { \
+		.path = _GPIOD_TEST_PATH(_name), \
+		.func = _gpiod_test_func_##_name, \
+	}; \
+	static __attribute__((constructor)) void \
+	_##_name##_test_register(void) \
+	{ \
+		_gpiod_test_register(&_##_name##_test_case); \
+	} \
+	static void _gpiod_test_func_##_name(void)
 
 #endif /* __GPIOD_TEST_H__ */
diff --git a/tests/gpiosim/gpiosim.c b/tests/gpiosim/gpiosim.c
index 338fbc1..eab01c6 100644
--- a/tests/gpiosim/gpiosim.c
+++ b/tests/gpiosim/gpiosim.c
@@ -736,6 +736,7 @@ static void bank_release(struct refcount *ref)
 	unsigned int i;
 	char buf[64];
 
+	/* FIXME should be based on dirent because num_lines can change. */
 	for (i = 0; i < bank->num_lines; i++) {
 		snprintf(buf, sizeof(buf), "line%u/hog", i);
 		unlinkat(bank->cfs_dir_fd, buf, AT_REMOVEDIR);
diff --git a/tests/mockup/Makefile.am b/tests/mockup/Makefile.am
deleted file mode 100644
index 36cd397..0000000
--- a/tests/mockup/Makefile.am
+++ /dev/null
@@ -1,11 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0-or-later
-# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-lib_LTLIBRARIES = libgpiomockup.la
-
-libgpiomockup_la_SOURCES = gpio-mockup.c gpio-mockup.h
-libgpiomockup_la_CFLAGS = -Wall -Wextra -g -fvisibility=hidden -std=gnu89
-libgpiomockup_la_CFLAGS += -include $(top_builddir)/config.h
-libgpiomockup_la_CFLAGS += $(KMOD_CFLAGS) $(UDEV_CFLAGS)
-libgpiomockup_la_LDFLAGS = -version-info $(subst .,:,$(ABI_MOCKUP_VERSION))
-libgpiomockup_la_LDFLAGS += $(KMOD_LIBS) $(UDEV_LIBS)
diff --git a/tests/mockup/gpio-mockup.c b/tests/mockup/gpio-mockup.c
deleted file mode 100644
index eba26d3..0000000
--- a/tests/mockup/gpio-mockup.c
+++ /dev/null
@@ -1,496 +0,0 @@
-// SPDX-License-Identifier: LGPL-2.1-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-#include <errno.h>
-#include <libkmod.h>
-#include <libudev.h>
-#include <linux/version.h>
-#include <poll.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/utsname.h>
-#include <unistd.h>
-
-#include "gpio-mockup.h"
-
-#define EXPORT			__attribute__((visibility("default")))
-/*
- * The gpio-mockup features (including the debugfs interface) we're using
- * in this library have first been released in the linux kernel version below.
- */
-#define MIN_KERNEL_VERSION	KERNEL_VERSION(5, 10, 0)
-
-struct gpio_mockup_chip {
-	char *name;
-	char *path;
-	unsigned int num;
-};
-
-struct gpio_mockup {
-	struct gpio_mockup_chip **chips;
-	unsigned int num_chips;
-	struct kmod_ctx *kmod;
-	struct kmod_module *module;
-	int refcount;
-};
-
-static void free_chip(struct gpio_mockup_chip *chip)
-{
-	free(chip->name);
-	free(chip->path);
-	free(chip);
-}
-
-static bool check_kernel_version(void)
-{
-	unsigned int major, minor, release;
-	struct utsname un;
-	int rv;
-
-	rv = uname(&un);
-	if (rv)
-		return false;
-
-	rv = sscanf(un.release, "%u.%u.%u", &major, &minor, &release);
-	if (rv != 3) {
-		errno = EFAULT;
-		return false;
-	}
-
-	if (KERNEL_VERSION(major, minor, release) < MIN_KERNEL_VERSION) {
-		errno = EOPNOTSUPP;
-		return false;
-	}
-
-	return true;
-}
-
-EXPORT struct gpio_mockup *gpio_mockup_new(void)
-{
-	struct gpio_mockup *ctx;
-	const char *modpath;
-	int rv;
-
-	if (!check_kernel_version())
-		goto err_out;
-
-	ctx = malloc(sizeof(*ctx));
-	if (!ctx)
-		goto err_out;
-
-	memset(ctx, 0, sizeof(*ctx));
-	ctx->refcount = 1;
-
-	ctx->kmod = kmod_new(NULL, NULL);
-	if (!ctx->kmod)
-		goto err_free_kmod;
-
-	rv = kmod_module_new_from_name(ctx->kmod, "gpio-mockup", &ctx->module);
-	if (rv)
-		goto err_unref_module;
-
-	/* First see if we can find the module. */
-	modpath = kmod_module_get_path(ctx->module);
-	if (!modpath) {
-		errno = ENOENT;
-		goto err_unref_module;
-	}
-
-	/*
-	 * Then see if we can freely load and unload it. If it's already
-	 * loaded - no problem, we'll remove it next anyway.
-	 */
-	rv = kmod_module_probe_insert_module(ctx->module,
-					     KMOD_PROBE_IGNORE_LOADED,
-					     "gpio_mockup_ranges=-1,4",
-					     NULL, NULL, NULL);
-	if (rv)
-		goto err_unref_module;
-
-	/* We need to check that the gpio-mockup debugfs directory exists. */
-	rv = access("/sys/kernel/debug/gpio-mockup", R_OK | W_OK);
-	if (rv)
-		goto err_unref_module;
-
-	rv = kmod_module_remove_module(ctx->module, 0);
-	if (rv)
-		goto err_unref_module;
-
-	return ctx;
-
-err_unref_module:
-	kmod_unref(ctx->kmod);
-err_free_kmod:
-	free(ctx);
-err_out:
-	return NULL;
-}
-
-EXPORT void gpio_mockup_ref(struct gpio_mockup *ctx)
-{
-	ctx->refcount++;
-}
-
-EXPORT void gpio_mockup_unref(struct gpio_mockup *ctx)
-{
-	ctx->refcount--;
-
-	if (ctx->refcount == 0) {
-		if (ctx->chips)
-			gpio_mockup_remove(ctx);
-
-		kmod_module_unref(ctx->module);
-		kmod_unref(ctx->kmod);
-		free(ctx);
-	}
-}
-
-static char *make_module_param_string(unsigned int num_chips,
-				      const unsigned int *num_lines, int flags)
-{
-	char *params, *new;
-	unsigned int i;
-	int rv;
-
-	params = strdup("gpio_mockup_ranges=");
-	if (!params)
-		return NULL;
-
-	for (i = 0; i < num_chips; i++) {
-		rv = asprintf(&new, "%s-1,%u,", params, num_lines[i]);
-		free(params);
-		if (rv < 0)
-			return NULL;
-
-		params = new;
-	}
-	params[strlen(params) - 1] = '\0'; /* Remove the last comma. */
-
-	if (flags & GPIO_MOCKUP_FLAG_NAMED_LINES) {
-		rv = asprintf(&new, "%s gpio_mockup_named_lines", params);
-		free(params);
-		if (rv < 0)
-			return NULL;
-
-		params = new;
-	}
-
-	return params;
-}
-
-static bool devpath_is_mockup(const char *devpath)
-{
-	static const char mockup_devpath[] = "/devices/platform/gpio-mockup";
-
-	return !strncmp(devpath, mockup_devpath, sizeof(mockup_devpath) - 1);
-}
-
-static int chipcmp(const void *c1, const void *c2)
-{
-	const struct gpio_mockup_chip *chip1, *chip2;
-
-	chip1 = *(const struct gpio_mockup_chip **)c1;
-	chip2 = *(const struct gpio_mockup_chip **)c2;
-
-	return chip1->num > chip2->num;
-}
-
-static struct gpio_mockup_chip *make_chip(const char *sysname,
-					  const char *devnode)
-{
-	struct gpio_mockup_chip *chip;
-	int rv;
-
-	chip = malloc(sizeof(*chip));
-	if (!chip)
-		return NULL;
-
-	chip->name = strdup(sysname);
-	if (!chip->name) {
-		free(chip);
-		return NULL;
-	}
-
-	chip->path = strdup(devnode);
-	if (!chip->path) {
-		free(chip->name);
-		free(chip);
-		return NULL;
-	}
-
-	rv = sscanf(sysname, "gpiochip%u", &chip->num);
-	if (rv != 1) {
-		errno = EINVAL;
-		free(chip->path);
-		free(chip->name);
-		free(chip);
-		return NULL;
-	}
-
-	return chip;
-}
-
-EXPORT int gpio_mockup_probe(struct gpio_mockup *ctx, unsigned int num_chips,
-			     const unsigned int *chip_sizes, int flags)
-{
-	const char *devpath, *devnode, *sysname, *action;
-	struct gpio_mockup_chip *chip;
-	struct udev_monitor *monitor;
-	unsigned int i, detected = 0;
-	struct udev_device *dev;
-	struct udev *udev_ctx;
-	struct pollfd pfd;
-	char *params;
-	int rv;
-
-	if (ctx->chips) {
-		errno = EBUSY;
-		goto err_out;
-	}
-
-	if (num_chips < 1) {
-		errno = EINVAL;
-		goto err_out;
-	}
-
-	udev_ctx = udev_new();
-	if (!udev_ctx)
-		goto err_out;
-
-	monitor = udev_monitor_new_from_netlink(udev_ctx, "udev");
-	if (!monitor)
-		goto err_unref_udev;
-
-	rv = udev_monitor_filter_add_match_subsystem_devtype(monitor,
-							     "gpio", NULL);
-	if (rv < 0)
-		goto err_unref_monitor;
-
-	rv = udev_monitor_enable_receiving(monitor);
-	if (rv < 0)
-		goto err_unref_monitor;
-
-	params = make_module_param_string(num_chips, chip_sizes, flags);
-	if (!params)
-		goto err_unref_monitor;
-
-	rv = kmod_module_probe_insert_module(ctx->module,
-					     KMOD_PROBE_FAIL_ON_LOADED,
-					     params, NULL, NULL, NULL);
-	free(params);
-	if (rv)
-		goto err_unref_monitor;
-
-	ctx->chips = calloc(num_chips, sizeof(struct gpio_mockup_chip *));
-	if (!ctx->chips)
-		goto err_remove_module;
-
-	ctx->num_chips = num_chips;
-
-	pfd.fd = udev_monitor_get_fd(monitor);
-	pfd.events = POLLIN | POLLPRI;
-
-	while (num_chips > detected) {
-		rv = poll(&pfd, 1, 5000);
-		if (rv < 0) {
-			goto err_free_chips;
-		} if (rv == 0) {
-			errno = EAGAIN;
-			goto err_free_chips;
-		}
-
-		dev = udev_monitor_receive_device(monitor);
-		if (!dev)
-			goto err_free_chips;
-
-		devpath = udev_device_get_devpath(dev);
-		devnode = udev_device_get_devnode(dev);
-		sysname = udev_device_get_sysname(dev);
-		action = udev_device_get_action(dev);
-
-		if (!devpath || !devnode || !sysname ||
-		    !devpath_is_mockup(devpath) || strcmp(action, "add") != 0) {
-			udev_device_unref(dev);
-			continue;
-		}
-
-		chip = make_chip(sysname, devnode);
-		if (!chip)
-			goto err_free_chips;
-
-		ctx->chips[detected++] = chip;
-		udev_device_unref(dev);
-	}
-
-	udev_monitor_unref(monitor);
-	udev_unref(udev_ctx);
-
-	/*
-	 * We can't assume that the order in which the mockup gpiochip
-	 * devices are created will be deterministic, yet we want the
-	 * index passed to the test_chip_*() functions to correspond to the
-	 * order in which the chips were defined in the TEST_DEFINE()
-	 * macro.
-	 *
-	 * Once all gpiochips are there, sort them by chip number.
-	 */
-	qsort(ctx->chips, ctx->num_chips, sizeof(*ctx->chips), chipcmp);
-
-	return 0;
-
-err_free_chips:
-	for (i = 0; i < detected; i++)
-		free_chip(ctx->chips[i]);
-	free(ctx->chips);
-err_remove_module:
-	kmod_module_remove_module(ctx->module, 0);
-err_unref_monitor:
-	udev_monitor_unref(monitor);
-err_unref_udev:
-	udev_unref(udev_ctx);
-err_out:
-	return -1;
-}
-
-EXPORT int gpio_mockup_remove(struct gpio_mockup *ctx)
-{
-	unsigned int i;
-	int rv;
-
-	if (!ctx->chips) {
-		errno = ENODEV;
-		return -1;
-	}
-
-	rv = kmod_module_remove_module(ctx->module, 0);
-	if (rv)
-		return -1;
-
-	for (i = 0; i < ctx->num_chips; i++)
-		free_chip(ctx->chips[i]);
-	free(ctx->chips);
-	ctx->chips = NULL;
-	ctx->num_chips = 0;
-
-	return 0;
-}
-
-static bool index_valid(struct gpio_mockup *ctx, unsigned int idx)
-{
-	if (!ctx->chips) {
-		errno = ENODEV;
-		return false;
-	}
-
-	if (idx >= ctx->num_chips) {
-		errno = EINVAL;
-		return false;
-	}
-
-	return true;
-}
-
-EXPORT const char *
-gpio_mockup_chip_name(struct gpio_mockup *ctx, unsigned int idx)
-{
-	if (!index_valid(ctx, idx))
-		return NULL;
-
-	return ctx->chips[idx]->name;
-}
-
-EXPORT const char *
-gpio_mockup_chip_path(struct gpio_mockup *ctx, unsigned int idx)
-{
-	if (!index_valid(ctx, idx))
-		return NULL;
-
-	return ctx->chips[idx]->path;
-}
-
-EXPORT int gpio_mockup_chip_num(struct gpio_mockup *ctx, unsigned int idx)
-{
-	if (!index_valid(ctx, idx))
-		return -1;
-
-	return ctx->chips[idx]->num;
-}
-
-static int debugfs_open(unsigned int chip_num,
-			unsigned int line_offset, int flags)
-{
-	char *path;
-	int fd, rv;
-
-	rv = asprintf(&path, "/sys/kernel/debug/gpio-mockup/gpiochip%u/%u",
-		      chip_num, line_offset);
-	if (rv < 0)
-		return -1;
-
-	fd = open(path, flags);
-	free(path);
-
-	return fd;
-}
-
-EXPORT int gpio_mockup_get_value(struct gpio_mockup *ctx,
-				 unsigned int chip_idx,
-				 unsigned int line_offset)
-{
-	ssize_t rd;
-	char buf;
-	int fd;
-
-	if (!index_valid(ctx, chip_idx))
-		return -1;
-
-	fd = debugfs_open(ctx->chips[chip_idx]->num, line_offset, O_RDONLY);
-	if (fd < 0)
-		return fd;
-
-	rd = read(fd, &buf, 1);
-	close(fd);
-	if (rd < 0)
-		return rd;
-	if (rd != 1) {
-		errno = ENOTTY;
-		return -1;
-	}
-	if (buf != '0' && buf != '1') {
-		errno = EIO;
-		return -1;
-	}
-
-	return buf == '0' ? 0 : 1;
-}
-
-EXPORT int gpio_mockup_set_pull(struct gpio_mockup *ctx,
-				unsigned int chip_idx,
-				unsigned int line_offset, int pull)
-{
-	char buf[2];
-	ssize_t wr;
-	int fd;
-
-	if (!index_valid(ctx, chip_idx))
-		return -1;
-
-	fd = debugfs_open(ctx->chips[chip_idx]->num, line_offset, O_WRONLY);
-	if (fd < 0)
-		return fd;
-
-	buf[0] = pull ? '1' : '0';
-	buf[1] = '\n';
-
-	wr = write(fd, &buf, sizeof(buf));
-	close(fd);
-	if (wr < 0)
-		return wr;
-	if (wr != sizeof(buf)) {
-		errno = EAGAIN;
-		return -1;
-	}
-
-	return 0;
-}
diff --git a/tests/mockup/gpio-mockup.h b/tests/mockup/gpio-mockup.h
deleted file mode 100644
index 4a55032..0000000
--- a/tests/mockup/gpio-mockup.h
+++ /dev/null
@@ -1,36 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-/* SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com> */
-
-#ifndef __GPIO_MOCKUP_H__
-#define __GPIO_MOCKUP_H__
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-struct gpio_mockup;
-
-#define GPIO_MOCKUP_FLAG_NAMED_LINES	(1 << 0)
-
-struct gpio_mockup *gpio_mockup_new(void);
-void gpio_mockup_ref(struct gpio_mockup *ctx);
-void gpio_mockup_unref(struct gpio_mockup *ctx);
-
-int gpio_mockup_probe(struct gpio_mockup *ctx, unsigned int num_chips,
-		      const unsigned int *chip_sizes, int flags);
-int gpio_mockup_remove(struct gpio_mockup *ctx);
-
-const char *gpio_mockup_chip_name(struct gpio_mockup *ctx, unsigned int idx);
-const char *gpio_mockup_chip_path(struct gpio_mockup *ctx, unsigned int idx);
-int gpio_mockup_chip_num(struct gpio_mockup *ctx, unsigned int idx);
-
-int gpio_mockup_get_value(struct gpio_mockup *ctx,
-			  unsigned int chip_idx, unsigned int line_offset);
-int gpio_mockup_set_pull(struct gpio_mockup *ctx, unsigned int chip_idx,
-			 unsigned int line_offset, int pull);
-
-#ifdef __cplusplus
-} /* extern "C" */
-#endif
-
-#endif /* __GPIO_MOCKUP_H__ */
diff --git a/tests/tests-chip.c b/tests/tests-chip.c
index 46fb8d2..09906e3 100644
--- a/tests/tests-chip.c
+++ b/tests/tests-chip.c
@@ -2,229 +2,171 @@
 // SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
 
 #include <errno.h>
+#include <glib.h>
+#include <gpiod.h>
 
 #include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+#include "gpiod-test-sim.h"
 
 #define GPIOD_TEST_GROUP "chip"
 
-GPIOD_TEST_CASE(is_gpiochip_good, 0, { 8 })
+GPIOD_TEST_CASE(open_chip_good)
 {
-	g_assert_true(gpiod_is_gpiochip_device(gpiod_test_chip_path(0)));
-}
-
-GPIOD_TEST_CASE(is_gpiochip_bad, 0, { 8 })
-{
-	g_assert_false(gpiod_is_gpiochip_device("/dev/null"));
-}
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new(NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
 
-GPIOD_TEST_CASE(is_gpiochip_nonexistent, 0, { 8 })
-{
-	g_assert_false(gpiod_is_gpiochip_device("/dev/nonexistent_gpiochip"));
+	chip = gpiod_chip_open(g_gpiosim_chip_get_dev_path(sim));
+	g_assert_nonnull(chip);
 }
 
-GPIOD_TEST_CASE(open_good, 0, { 8 })
+GPIOD_TEST_CASE(open_chip_nonexistent)
 {
-	g_autoptr(gpiod_chip_struct) chip = NULL;
+	g_autoptr(struct_gpiod_chip) chip = NULL;
 
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
+	chip = gpiod_chip_open("/dev/nonexistent");
+	g_assert_null(chip);
+	gpiod_test_expect_errno(ENOENT);
 }
 
-GPIOD_TEST_CASE(open_nonexistent, 0, { 8 })
+GPIOD_TEST_CASE(open_chip_not_a_character_device)
 {
-	struct gpiod_chip *chip;
+	g_autoptr(struct_gpiod_chip) chip = NULL;
 
-	chip = gpiod_chip_open("/dev/nonexistent_gpiochip");
+	chip = gpiod_chip_open("/tmp");
 	g_assert_null(chip);
-	g_assert_cmpint(errno, ==, ENOENT);
+	gpiod_test_expect_errno(ENOTTY);
 }
 
-GPIOD_TEST_CASE(open_notty, 0, { 8 })
+GPIOD_TEST_CASE(open_chip_not_a_gpio_device)
 {
-	struct gpiod_chip *chip;
+	g_autoptr(struct_gpiod_chip) chip = NULL;
 
 	chip = gpiod_chip_open("/dev/null");
 	g_assert_null(chip);
-	g_assert_cmpint(errno, ==, ENOTTY);
+	gpiod_test_expect_errno(ENODEV);
 }
 
-GPIOD_TEST_CASE(get_name, 0, { 8, 8, 8})
+GPIOD_TEST_CASE(get_chip_name)
 {
-	g_autoptr(gpiod_chip_struct) chip0 = NULL;
-	g_autoptr(gpiod_chip_struct) chip1 = NULL;
-	g_autoptr(gpiod_chip_struct) chip2 = NULL;
-
-	chip0 = gpiod_chip_open(gpiod_test_chip_path(0));
-	chip1 = gpiod_chip_open(gpiod_test_chip_path(1));
-	chip2 = gpiod_chip_open(gpiod_test_chip_path(2));
-
-	g_assert_nonnull(chip0);
-	g_assert_nonnull(chip1);
-	g_assert_nonnull(chip2);
-	gpiod_test_return_if_failed();
-
-	g_assert_cmpstr(gpiod_chip_get_name(chip0), ==,
-			gpiod_test_chip_name(0));
-	g_assert_cmpstr(gpiod_chip_get_name(chip1), ==,
-			gpiod_test_chip_name(1));
-	g_assert_cmpstr(gpiod_chip_get_name(chip2), ==,
-			gpiod_test_chip_name(2));
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new(NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+	g_assert_cmpstr(gpiod_chip_get_name(chip), ==,
+			g_gpiosim_chip_get_name(sim));
 }
 
-GPIOD_TEST_CASE(get_label, 0, { 8, 8, 8})
+GPIOD_TEST_CASE(get_chip_label)
 {
-	g_autoptr(gpiod_chip_struct) chip0 = NULL;
-	g_autoptr(gpiod_chip_struct) chip1 = NULL;
-	g_autoptr(gpiod_chip_struct) chip2 = NULL;
-
-	chip0 = gpiod_chip_open(gpiod_test_chip_path(0));
-	chip1 = gpiod_chip_open(gpiod_test_chip_path(1));
-	chip2 = gpiod_chip_open(gpiod_test_chip_path(2));
-
-	g_assert_nonnull(chip0);
-	g_assert_nonnull(chip1);
-	g_assert_nonnull(chip2);
-	gpiod_test_return_if_failed();
-
-	g_assert_cmpstr(gpiod_chip_get_label(chip0), ==, "gpio-mockup-A");
-	g_assert_cmpstr(gpiod_chip_get_label(chip1), ==, "gpio-mockup-B");
-	g_assert_cmpstr(gpiod_chip_get_label(chip2), ==, "gpio-mockup-C");
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("label", "foobar",
+							NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+	g_assert_cmpstr(gpiod_chip_get_label(chip), ==, "foobar");
 }
 
-GPIOD_TEST_CASE(num_lines, 0, { 1, 4, 8, 16, 32 })
+GPIOD_TEST_CASE(get_chip_path)
 {
-	g_autoptr(gpiod_chip_struct) chip0 = NULL;
-	g_autoptr(gpiod_chip_struct) chip1 = NULL;
-	g_autoptr(gpiod_chip_struct) chip2 = NULL;
-	g_autoptr(gpiod_chip_struct) chip3 = NULL;
-	g_autoptr(gpiod_chip_struct) chip4 = NULL;
-
-	chip0 = gpiod_chip_open(gpiod_test_chip_path(0));
-	chip1 = gpiod_chip_open(gpiod_test_chip_path(1));
-	chip2 = gpiod_chip_open(gpiod_test_chip_path(2));
-	chip3 = gpiod_chip_open(gpiod_test_chip_path(3));
-	chip4 = gpiod_chip_open(gpiod_test_chip_path(4));
-
-	g_assert_nonnull(chip0);
-	g_assert_nonnull(chip1);
-	g_assert_nonnull(chip2);
-	g_assert_nonnull(chip3);
-	g_assert_nonnull(chip4);
-	gpiod_test_return_if_failed();
-
-	g_assert_cmpuint(gpiod_chip_get_num_lines(chip0), ==, 1);
-	g_assert_cmpuint(gpiod_chip_get_num_lines(chip1), ==, 4);
-	g_assert_cmpuint(gpiod_chip_get_num_lines(chip2), ==, 8);
-	g_assert_cmpuint(gpiod_chip_get_num_lines(chip3), ==, 16);
-	g_assert_cmpuint(gpiod_chip_get_num_lines(chip4), ==, 32);
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new(NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	const gchar *path = g_gpiosim_chip_get_dev_path(sim);
+
+	chip = gpiod_test_open_chip_or_fail(path);
+
+	g_assert_cmpstr(gpiod_chip_get_path(chip), ==, path);
 }
 
-GPIOD_TEST_CASE(get_line, 0, { 16 })
+GPIOD_TEST_CASE(get_num_lines)
 {
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 16, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
 
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
 
-	line = gpiod_chip_get_line(chip, 3);
-	g_assert_nonnull(line);
-	g_assert_cmpuint(gpiod_line_offset(line), ==, 3);
+	g_assert_cmpuint(gpiod_chip_get_num_lines(chip), ==, 16);
 }
 
-GPIOD_TEST_CASE(get_lines, 0, { 16 })
+GPIOD_TEST_CASE(get_fd)
 {
-	struct gpiod_line *line0, *line1, *line2, *line3;
-	g_autoptr(gpiod_line_bulk_struct) bulk = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	guint offsets[4];
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new(NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
 
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	offsets[0] = 1;
-	offsets[1] = 3;
-	offsets[2] = 4;
-	offsets[3] = 7;
-
-	bulk = gpiod_chip_get_lines(chip, offsets, 4);
-	g_assert_nonnull(bulk);
-	gpiod_test_return_if_failed();
-	g_assert_cmpuint(gpiod_line_bulk_num_lines(bulk), ==, 4);
-	gpiod_test_return_if_failed();
-
-	line0 = gpiod_line_bulk_get_line(bulk, 0);
-	line1 = gpiod_line_bulk_get_line(bulk, 1);
-	line2 = gpiod_line_bulk_get_line(bulk, 2);
-	line3 = gpiod_line_bulk_get_line(bulk, 3);
-
-	g_assert_cmpuint(gpiod_line_offset(line0), ==, 1);
-	g_assert_cmpuint(gpiod_line_offset(line1), ==, 3);
-	g_assert_cmpuint(gpiod_line_offset(line2), ==, 4);
-	g_assert_cmpuint(gpiod_line_offset(line3), ==, 7);
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+	g_assert_cmpint(gpiod_chip_get_fd(chip), >=, 0);
 }
 
-GPIOD_TEST_CASE(get_all_lines, 0, { 4 })
+GPIOD_TEST_CASE(find_line_bad)
 {
-	struct gpiod_line *line0, *line1, *line2, *line3;
-	g_autoptr(gpiod_line_bulk_struct) bulk = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	bulk = gpiod_chip_get_all_lines(chip);
-	g_assert_nonnull(bulk);
-	gpiod_test_return_if_failed();
-	g_assert_cmpuint(gpiod_line_bulk_num_lines(bulk), ==, 4);
-	gpiod_test_return_if_failed();
-
-	line0 = gpiod_line_bulk_get_line(bulk, 0);
-	line1 = gpiod_line_bulk_get_line(bulk, 1);
-	line2 = gpiod_line_bulk_get_line(bulk, 2);
-	line3 = gpiod_line_bulk_get_line(bulk, 3);
-
-	g_assert_cmpuint(gpiod_line_offset(line0), ==, 0);
-	g_assert_cmpuint(gpiod_line_offset(line1), ==, 1);
-	g_assert_cmpuint(gpiod_line_offset(line2), ==, 2);
-	g_assert_cmpuint(gpiod_line_offset(line3), ==, 3);
+	static const struct gpiod_test_line_name names[] = {
+		{ .offset = 1, .name = "foo", },
+		{ .offset = 2, .name = "bar", },
+		{ .offset = 4, .name = "baz", },
+		{ .offset = 5, .name = "xyz", },
+		{ }
+	};
+
+	g_autoptr(GPIOSimChip) sim = NULL;
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+
+	sim = g_gpiosim_chip_new(
+			"num-lines", 8,
+			"line-names", gpiod_test_package_line_names(names),
+			 NULL);
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+	g_assert_cmpint(gpiod_chip_find_line(chip, "nonexistent"), ==, -1);
+	gpiod_test_expect_errno(ENOENT);
 }
 
-GPIOD_TEST_CASE(find_line_good, GPIOD_TEST_FLAG_NAMED_LINES, { 8, 8, 8 })
+GPIOD_TEST_CASE(find_line_good)
 {
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	int offset;
+	static const struct gpiod_test_line_name names[] = {
+		{ .offset = 1, .name = "foo", },
+		{ .offset = 2, .name = "bar", },
+		{ .offset = 4, .name = "baz", },
+		{ .offset = 5, .name = "xyz", },
+		{ }
+	};
 
-	chip = gpiod_chip_open(gpiod_test_chip_path(1));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
+	g_autoptr(GPIOSimChip) sim = NULL;
+	g_autoptr(struct_gpiod_chip) chip = NULL;
 
-	offset = gpiod_chip_find_line(chip, "gpio-mockup-B-4");
-	g_assert_cmpint(offset, ==, 4);
-	gpiod_test_return_if_failed();
+	sim = g_gpiosim_chip_new(
+			"num-lines", 8,
+			"line-names", gpiod_test_package_line_names(names),
+			NULL);
 
-	line = gpiod_chip_get_line(chip, 4);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
 
-	g_assert_cmpstr(gpiod_line_name(line), ==, "gpio-mockup-B-4");
+	g_assert_cmpint(gpiod_chip_find_line(chip, "baz"), ==, 4);
 }
 
-GPIOD_TEST_CASE(find_line_unique_not_found,
-		GPIOD_TEST_FLAG_NAMED_LINES, { 8, 8, 8 })
+/* Verify that for duplicated line names, the first one is returned. */
+GPIOD_TEST_CASE(find_line_duplicate)
 {
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	int offset;
+	static const struct gpiod_test_line_name names[] = {
+		{ .offset = 1, .name = "foo", },
+		{ .offset = 2, .name = "baz", },
+		{ .offset = 4, .name = "baz", },
+		{ .offset = 5, .name = "xyz", },
+		{ }
+	};
 
-	chip = gpiod_chip_open(gpiod_test_chip_path(1));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
+	g_autoptr(GPIOSimChip) sim = NULL;
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+
+	sim = g_gpiosim_chip_new(
+			"num-lines", 8,
+			"line-names", gpiod_test_package_line_names(names),
+			NULL);
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
 
-	offset = gpiod_chip_find_line(chip, "nonexistent");
-	g_assert_cmpint(offset, ==, -1);
-	g_assert_cmpint(errno, ==, ENOENT);
+	g_assert_cmpint(gpiod_chip_find_line(chip, "baz"), ==, 2);
 }
diff --git a/tests/tests-edge-event.c b/tests/tests-edge-event.c
new file mode 100644
index 0000000..34bd02d
--- /dev/null
+++ b/tests/tests-edge-event.c
@@ -0,0 +1,490 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#include <glib.h>
+#include <gpiod.h>
+#include <poll.h>
+
+#include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+#include "gpiod-test-sim.h"
+
+#define GPIOD_TEST_GROUP "edge-event"
+
+GPIOD_TEST_CASE(edge_event_buffer_capacity)
+{
+	g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+
+	buffer = gpiod_test_create_edge_event_buffer_or_fail(32);
+
+	g_assert_cmpuint(gpiod_edge_event_buffer_get_capacity(buffer), ==, 32);
+}
+
+GPIOD_TEST_CASE(edge_event_buffer_max_capacity)
+{
+	g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+
+	buffer = gpiod_test_create_edge_event_buffer_or_fail(16 * 64 * 2);
+
+	g_assert_cmpuint(gpiod_edge_event_buffer_get_capacity(buffer),
+			 ==, 16 * 64);
+}
+
+GPIOD_TEST_CASE(edge_event_wait_timeout)
+{
+	static const guint offset = 4;
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	gint ret;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 1, &offset);
+	gpiod_line_config_set_edge_detection_default(line_cfg,
+						     GPIOD_LINE_EDGE_BOTH);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	ret = gpiod_line_request_edge_event_wait(request, 1000000);
+	g_assert_cmpint(ret, ==, 0);
+}
+
+GPIOD_TEST_CASE(cannot_request_lines_in_output_mode_with_edge_detection)
+{
+	static const guint offset = 4;
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 1, &offset);
+	gpiod_line_config_set_edge_detection_default(line_cfg,
+						     GPIOD_LINE_EDGE_BOTH);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_OUTPUT);
+
+	request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+	g_assert_null(request);
+	gpiod_test_expect_errno(EINVAL);
+}
+
+static gpointer falling_and_rising_edge_events(gpointer data)
+{
+	GPIOSimChip *sim = data;
+
+	g_usleep(50);
+
+	g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_UP);
+
+	g_usleep(50);
+
+	g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_DOWN);
+
+	return NULL;
+}
+
+GPIOD_TEST_CASE(read_both_events)
+{
+	static const guint offset = 2;
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	g_autoptr(GThread) thread = NULL;
+	g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+	struct gpiod_edge_event *event;
+	guint64 ts_rising, ts_falling;
+	gint ret;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+	buffer = gpiod_test_create_edge_event_buffer_or_fail(64);
+
+	gpiod_request_config_set_offsets(req_cfg, 1, &offset);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_INPUT);
+	gpiod_line_config_set_edge_detection_default(line_cfg,
+						     GPIOD_LINE_EDGE_BOTH);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	thread = g_thread_new("request-release",
+			      falling_and_rising_edge_events, sim);
+	g_thread_ref(thread);
+
+	/* First event. */
+
+	ret = gpiod_line_request_edge_event_wait(request, 1000000000);
+	g_assert_cmpint(ret, >, 0);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	ret = gpiod_line_request_edge_event_read(request, buffer, 1);
+	g_assert_cmpint(ret, ==, 1);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpuint(gpiod_edge_event_buffer_get_num_events(buffer), ==, 1);
+	event = gpiod_edge_event_buffer_get_event(buffer, 0);
+	g_assert_nonnull(event);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpint(gpiod_edge_event_get_event_type(event),
+			==, GPIOD_EDGE_EVENT_RISING_EDGE);
+	g_assert_cmpuint(gpiod_edge_event_get_line_offset(event), ==, 2);
+	ts_rising = gpiod_edge_event_get_timestamp(event);
+
+	/* Second event. */
+
+	ret = gpiod_line_request_edge_event_wait(request, 1000000000);
+	g_assert_cmpint(ret, >, 0);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	ret = gpiod_line_request_edge_event_read(request, buffer, 1);
+	g_assert_cmpint(ret, ==, 1);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpuint(gpiod_edge_event_buffer_get_num_events(buffer), ==, 1);
+	event = gpiod_edge_event_buffer_get_event(buffer, 0);
+	g_assert_nonnull(event);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpint(gpiod_edge_event_get_event_type(event),
+			==, GPIOD_EDGE_EVENT_FALLING_EDGE);
+	g_assert_cmpuint(gpiod_edge_event_get_line_offset(event), ==, 2);
+	ts_falling = gpiod_edge_event_get_timestamp(event);
+
+	g_thread_join(thread);
+
+	g_assert_cmpuint(ts_falling, >, ts_rising);
+}
+
+GPIOD_TEST_CASE(read_rising_edge_event)
+{
+	static const guint offset = 2;
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	g_autoptr(GThread) thread = NULL;
+	g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+	struct gpiod_edge_event *event;
+	gint ret;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+	buffer = gpiod_test_create_edge_event_buffer_or_fail(64);
+
+	gpiod_request_config_set_offsets(req_cfg, 1, &offset);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_INPUT);
+	gpiod_line_config_set_edge_detection_default(line_cfg,
+						     GPIOD_LINE_EDGE_RISING);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	thread = g_thread_new("edge-generator",
+			      falling_and_rising_edge_events, sim);
+	g_thread_ref(thread);
+
+	/* First event. */
+
+	ret = gpiod_line_request_edge_event_wait(request, 1000000000);
+	g_assert_cmpint(ret, >, 0);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	ret = gpiod_line_request_edge_event_read(request, buffer, 1);
+	g_assert_cmpint(ret, ==, 1);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpuint(gpiod_edge_event_buffer_get_num_events(buffer), ==, 1);
+	event = gpiod_edge_event_buffer_get_event(buffer, 0);
+	g_assert_nonnull(event);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpint(gpiod_edge_event_get_event_type(event),
+			==, GPIOD_EDGE_EVENT_RISING_EDGE);
+	g_assert_cmpuint(gpiod_edge_event_get_line_offset(event), ==, 2);
+
+	/* Second event. */
+
+	ret = gpiod_line_request_edge_event_wait(request, 1000000);
+	g_assert_cmpint(ret, ==, 0); /* Time-out. */
+
+	g_thread_join(thread);
+}
+
+GPIOD_TEST_CASE(read_falling_edge_event)
+{
+	static const guint offset = 2;
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	g_autoptr(GThread) thread = NULL;
+	g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+	struct gpiod_edge_event *event;
+	gint ret;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+	buffer = gpiod_test_create_edge_event_buffer_or_fail(64);
+
+	gpiod_request_config_set_offsets(req_cfg, 1, &offset);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_INPUT);
+	gpiod_line_config_set_edge_detection_default(line_cfg,
+						     GPIOD_LINE_EDGE_FALLING);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	thread = g_thread_new("request-release",
+			      falling_and_rising_edge_events, sim);
+	g_thread_ref(thread);
+
+	/* First event is the second generated. */
+
+	ret = gpiod_line_request_edge_event_wait(request, 1000000000);
+	g_assert_cmpint(ret, >, 0);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	ret = gpiod_line_request_edge_event_read(request, buffer, 1);
+	g_assert_cmpint(ret, ==, 1);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpuint(gpiod_edge_event_buffer_get_num_events(buffer), ==, 1);
+	event = gpiod_edge_event_buffer_get_event(buffer, 0);
+	g_assert_nonnull(event);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpint(gpiod_edge_event_get_event_type(event),
+			==, GPIOD_EDGE_EVENT_FALLING_EDGE);
+	g_assert_cmpuint(gpiod_edge_event_get_line_offset(event), ==, 2);
+
+	/* No more events. */
+
+	ret = gpiod_line_request_edge_event_wait(request, 1000000);
+	g_assert_cmpint(ret, ==, 0); /* Time-out. */
+
+	g_thread_join(thread);
+}
+
+GPIOD_TEST_CASE(read_rising_edge_event_polled)
+{
+	static const guint offset = 2;
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	g_autoptr(GThread) thread = NULL;
+	g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+	struct gpiod_edge_event *event;
+	struct timespec ts;
+	struct pollfd pfd;
+	gint ret, fd;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+	buffer = gpiod_test_create_edge_event_buffer_or_fail(64);
+
+	gpiod_request_config_set_offsets(req_cfg, 1, &offset);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_INPUT);
+	gpiod_line_config_set_edge_detection_default(line_cfg,
+						     GPIOD_LINE_EDGE_RISING);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	thread = g_thread_new("edge-generator",
+			      falling_and_rising_edge_events, sim);
+	g_thread_ref(thread);
+
+	/* First event. */
+
+	fd = gpiod_line_request_get_fd(request);
+
+	memset(&pfd, 0, sizeof(pfd));
+	pfd.fd = fd;
+	pfd.events = POLLIN | POLLPRI;
+
+	ts.tv_sec = 1;
+	ts.tv_nsec = 0;
+
+	ret = ppoll(&pfd, 1, &ts, NULL);
+	g_assert_cmpint(ret, >, 0);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	ret = gpiod_line_request_edge_event_read(request, buffer, 1);
+	g_assert_cmpint(ret, ==, 1);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpuint(gpiod_edge_event_buffer_get_num_events(buffer), ==, 1);
+	event = gpiod_edge_event_buffer_get_event(buffer, 0);
+	g_assert_nonnull(event);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpint(gpiod_edge_event_get_event_type(event),
+			==, GPIOD_EDGE_EVENT_RISING_EDGE);
+	g_assert_cmpuint(gpiod_edge_event_get_line_offset(event), ==, 2);
+
+	/* Second event. */
+
+	ret = gpiod_line_request_edge_event_wait(request, 1000000);
+	g_assert_cmpint(ret, ==, 0); /* Time-out. */
+
+	g_thread_join(thread);
+}
+
+static gpointer rising_edge_events_on_two_offsets(gpointer data)
+{
+	GPIOSimChip *sim = data;
+
+	g_usleep(50);
+
+	g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_UP);
+
+	g_usleep(50);
+
+	g_gpiosim_chip_set_pull(sim, 3, G_GPIOSIM_PULL_UP);
+
+	return NULL;
+}
+
+GPIOD_TEST_CASE(seqno)
+{
+	static const guint offsets[] = { 2, 3 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	g_autoptr(GThread) thread = NULL;
+	g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+	struct gpiod_edge_event *event;
+	gint ret;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+	buffer = gpiod_test_create_edge_event_buffer_or_fail(64);
+
+	gpiod_request_config_set_offsets(req_cfg, 2, offsets);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_INPUT);
+	gpiod_line_config_set_edge_detection_default(line_cfg,
+						     GPIOD_LINE_EDGE_BOTH);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	thread = g_thread_new("request-release",
+			      rising_edge_events_on_two_offsets, sim);
+	g_thread_ref(thread);
+
+	/* First event. */
+
+	ret = gpiod_line_request_edge_event_wait(request, 1000000000);
+	g_assert_cmpint(ret, >, 0);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	ret = gpiod_line_request_edge_event_read(request, buffer, 1);
+	g_assert_cmpint(ret, ==, 1);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpuint(gpiod_edge_event_buffer_get_num_events(buffer), ==, 1);
+	event = gpiod_edge_event_buffer_get_event(buffer, 0);
+	g_assert_nonnull(event);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpuint(gpiod_edge_event_get_line_offset(event), ==, 2);
+	g_assert_cmpuint(gpiod_edge_event_get_global_seqno(event), ==, 1);
+	g_assert_cmpuint(gpiod_edge_event_get_line_seqno(event), ==, 1);
+
+	/* Second event. */
+
+	ret = gpiod_line_request_edge_event_wait(request, 1000000000);
+	g_assert_cmpint(ret, >, 0);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	ret = gpiod_line_request_edge_event_read(request, buffer, 1);
+	g_assert_cmpint(ret, ==, 1);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpuint(gpiod_edge_event_buffer_get_num_events(buffer), ==, 1);
+	event = gpiod_edge_event_buffer_get_event(buffer, 0);
+	g_assert_nonnull(event);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpuint(gpiod_edge_event_get_line_offset(event), ==, 3);
+	g_assert_cmpuint(gpiod_edge_event_get_global_seqno(event), ==, 2);
+	g_assert_cmpuint(gpiod_edge_event_get_line_seqno(event), ==, 1);
+}
+
+GPIOD_TEST_CASE(event_copy)
+{
+	static const guint offset = 2;
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	g_autoptr(GThread) thread = NULL;
+	g_autoptr(struct_gpiod_edge_event_buffer) buffer = NULL;
+	g_autoptr(struct_gpiod_edge_event) copy = NULL;
+	struct gpiod_edge_event *event;
+	gint ret;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+	buffer = gpiod_test_create_edge_event_buffer_or_fail(64);
+
+	gpiod_request_config_set_offsets(req_cfg, 1, &offset);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_INPUT);
+	gpiod_line_config_set_edge_detection_default(line_cfg,
+						     GPIOD_LINE_EDGE_BOTH);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_UP);
+
+	ret = gpiod_line_request_edge_event_wait(request, 1000000000);
+	g_assert_cmpint(ret, >, 0);
+	gpiod_test_return_if_failed();
+
+	ret = gpiod_line_request_edge_event_read(request, buffer, 1);
+	g_assert_cmpint(ret, ==, 1);
+	gpiod_test_return_if_failed();
+
+	event = gpiod_edge_event_buffer_get_event(buffer, 0);
+	g_assert_nonnull(event);
+	gpiod_test_return_if_failed();
+
+	copy = gpiod_edge_event_copy(event);
+	g_assert_nonnull(copy);
+	g_assert_true(copy != event);
+}
diff --git a/tests/tests-event.c b/tests/tests-event.c
deleted file mode 100644
index 53d3e8c..0000000
--- a/tests/tests-event.c
+++ /dev/null
@@ -1,908 +0,0 @@
-// SPDX-License-Identifier: GPL-2.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-#include <errno.h>
-#include <unistd.h>
-
-#include "gpiod-test.h"
-
-#define GPIOD_TEST_GROUP "event"
-
-GPIOD_TEST_CASE(rising_edge_good, 0, { 8 })
-{
-	g_autoptr(GpiodTestEventThread) ev_thread = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct timespec ts = { 1, 0 };
-	struct gpiod_line_event ev;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 7);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_rising_edge_events(line, GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	ev_thread = gpiod_test_start_event_thread(0, 7, 100);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_RISING_EDGE);
-}
-
-GPIOD_TEST_CASE(falling_edge_good, 0, { 8 })
-{
-	g_autoptr(GpiodTestEventThread) ev_thread = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct timespec ts = { 1, 0 };
-	struct gpiod_line_event ev;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 7);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_falling_edge_events(line,
-						     GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	ev_thread = gpiod_test_start_event_thread(0, 7, 100);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_FALLING_EDGE);
-}
-
-GPIOD_TEST_CASE(rising_edge_ignore_falling, 0, { 8 })
-{
-	g_autoptr(GpiodTestEventThread) ev_thread = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct timespec ts = { 1, 0 };
-	struct gpiod_line_event ev[3];
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 7);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_rising_edge_events(line, GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	ev_thread = gpiod_test_start_event_thread(0, 7, 100);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-	ret = gpiod_line_event_read(line, &ev[0]);
-	g_assert_cmpint(ret, ==, 0);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-	ret = gpiod_line_event_read(line, &ev[1]);
-	g_assert_cmpint(ret, ==, 0);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-	ret = gpiod_line_event_read(line, &ev[2]);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev[0].event_type, ==, GPIOD_LINE_EVENT_RISING_EDGE);
-	g_assert_cmpint(ev[1].event_type, ==, GPIOD_LINE_EVENT_RISING_EDGE);
-	g_assert_cmpint(ev[2].event_type, ==, GPIOD_LINE_EVENT_RISING_EDGE);
-}
-
-GPIOD_TEST_CASE(both_edges, 0, { 8 })
-{
-	g_autoptr(GpiodTestEventThread) ev_thread = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct timespec ts = { 1, 0 };
-	struct gpiod_line_event ev;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 7);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_both_edges_events(line, GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	ev_thread = gpiod_test_start_event_thread(0, 7, 100);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_RISING_EDGE);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_FALLING_EDGE);
-}
-
-GPIOD_TEST_CASE(both_edges_active_low, 0, { 8 })
-{
-	g_autoptr(GpiodTestEventThread) ev_thread = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct timespec ts = { 1, 0 };
-	struct gpiod_line_event ev;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 7);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_both_edges_events_flags(line,
-		GPIOD_TEST_CONSUMER, GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	ev_thread = gpiod_test_start_event_thread(0, 7, 100);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_FALLING_EDGE);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_RISING_EDGE);
-}
-
-GPIOD_TEST_CASE(both_edges_bias_disable, 0, { 8 })
-{
-	g_autoptr(GpiodTestEventThread) ev_thread = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct timespec ts = { 1, 0 };
-	struct gpiod_line_event ev;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 7);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_both_edges_events_flags(line,
-		GPIOD_TEST_CONSUMER, GPIOD_LINE_REQUEST_FLAG_BIAS_DISABLED);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	ev_thread = gpiod_test_start_event_thread(0, 7, 100);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_RISING_EDGE);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_FALLING_EDGE);
-}
-
-GPIOD_TEST_CASE(both_edges_bias_pull_down, 0, { 8 })
-{
-	g_autoptr(GpiodTestEventThread) ev_thread = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct timespec ts = { 1, 0 };
-	struct gpiod_line_event ev;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 7);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_both_edges_events_flags(line,
-		GPIOD_TEST_CONSUMER, GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_DOWN);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	ev_thread = gpiod_test_start_event_thread(0, 7, 100);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_RISING_EDGE);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_FALLING_EDGE);
-}
-
-GPIOD_TEST_CASE(both_edges_bias_pull_up, 0, { 8 })
-{
-	g_autoptr(GpiodTestEventThread) ev_thread = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct timespec ts = { 1, 0 };
-	struct gpiod_line_event ev;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 7);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_both_edges_events_flags(line,
-		GPIOD_TEST_CONSUMER, GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	ev_thread = gpiod_test_start_event_thread(0, 7, 100);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_FALLING_EDGE);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_RISING_EDGE);
-}
-
-GPIOD_TEST_CASE(falling_edge_active_low, 0, { 8 })
-{
-	g_autoptr(GpiodTestEventThread) ev_thread = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct timespec ts = { 1, 0 };
-	struct gpiod_line_event ev;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 7);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_falling_edge_events_flags(line,
-		GPIOD_TEST_CONSUMER, GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	ev_thread = gpiod_test_start_event_thread(0, 7, 100);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_FALLING_EDGE);
-}
-
-GPIOD_TEST_CASE(get_value, 0, { 8 })
-{
-	g_autoptr(GpiodTestEventThread) ev_thread = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct timespec ts = { 1, 0 };
-	struct gpiod_line_event ev;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 7);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	gpiod_test_chip_set_pull(0, 7, 1);
-
-	ret = gpiod_line_request_falling_edge_events(line, GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_get_value(line);
-	g_assert_cmpint(ret, ==, 1);
-
-	ev_thread = gpiod_test_start_event_thread(0, 7, 100);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_FALLING_EDGE);
-}
-
-GPIOD_TEST_CASE(get_value_active_low, 0, { 8 })
-{
-	g_autoptr(GpiodTestEventThread) ev_thread = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct timespec ts = { 1, 0 };
-	struct gpiod_line_event ev;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 7);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	gpiod_test_chip_set_pull(0, 7, 1);
-
-	ret = gpiod_line_request_falling_edge_events_flags(line,
-		GPIOD_TEST_CONSUMER, GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_get_value(line);
-	g_assert_cmpint(ret, ==, 0);
-
-	ev_thread = gpiod_test_start_event_thread(0, 7, 100);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_FALLING_EDGE);
-}
-
-GPIOD_TEST_CASE(get_values, 0, { 8 })
-{
-	g_autoptr(gpiod_line_bulk_struct) bulk = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint i, ret, vals[8];
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	bulk = gpiod_line_bulk_new(8);
-	g_assert_nonnull(bulk);
-	gpiod_test_return_if_failed();
-
-	for (i = 0; i < 8; i++) {
-		line = gpiod_chip_get_line(chip, i);
-		g_assert_nonnull(line);
-		gpiod_test_return_if_failed();
-
-		gpiod_line_bulk_add_line(bulk, line);
-		gpiod_test_chip_set_pull(0, i, 1);
-	}
-
-	ret = gpiod_line_request_bulk_rising_edge_events(bulk,
-							 GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	memset(vals, 0, sizeof(vals));
-	ret = gpiod_line_get_value_bulk(bulk, vals);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(vals[0], ==, 1);
-	g_assert_cmpint(vals[1], ==, 1);
-	g_assert_cmpint(vals[2], ==, 1);
-	g_assert_cmpint(vals[3], ==, 1);
-	g_assert_cmpint(vals[4], ==, 1);
-	g_assert_cmpint(vals[5], ==, 1);
-	g_assert_cmpint(vals[6], ==, 1);
-	g_assert_cmpint(vals[7], ==, 1);
-
-	gpiod_test_chip_set_pull(0, 1, 0);
-	gpiod_test_chip_set_pull(0, 3, 0);
-	gpiod_test_chip_set_pull(0, 4, 0);
-	gpiod_test_chip_set_pull(0, 7, 0);
-
-	memset(vals, 0, sizeof(vals));
-	ret = gpiod_line_get_value_bulk(bulk, vals);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(vals[0], ==, 1);
-	g_assert_cmpint(vals[1], ==, 0);
-	g_assert_cmpint(vals[2], ==, 1);
-	g_assert_cmpint(vals[3], ==, 0);
-	g_assert_cmpint(vals[4], ==, 0);
-	g_assert_cmpint(vals[5], ==, 1);
-	g_assert_cmpint(vals[6], ==, 1);
-	g_assert_cmpint(vals[7], ==, 0);
-}
-
-GPIOD_TEST_CASE(get_values_active_low, 0, { 8 })
-{
-	g_autoptr(gpiod_line_bulk_struct) bulk = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint i, ret, vals[8];
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	bulk = gpiod_line_bulk_new(8);
-	g_assert_nonnull(bulk);
-	gpiod_test_return_if_failed();
-
-	for (i = 0; i < 8; i++) {
-		line = gpiod_chip_get_line(chip, i);
-		g_assert_nonnull(line);
-		gpiod_test_return_if_failed();
-
-		gpiod_line_bulk_add_line(bulk, line);
-		gpiod_test_chip_set_pull(0, i, 0);
-	}
-
-	ret = gpiod_line_request_bulk_rising_edge_events_flags(bulk,
-			GPIOD_TEST_CONSUMER, GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	memset(vals, 0, sizeof(vals));
-	ret = gpiod_line_get_value_bulk(bulk, vals);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(vals[0], ==, 1);
-	g_assert_cmpint(vals[1], ==, 1);
-	g_assert_cmpint(vals[2], ==, 1);
-	g_assert_cmpint(vals[3], ==, 1);
-	g_assert_cmpint(vals[4], ==, 1);
-	g_assert_cmpint(vals[5], ==, 1);
-	g_assert_cmpint(vals[6], ==, 1);
-	g_assert_cmpint(vals[7], ==, 1);
-
-	gpiod_test_chip_set_pull(0, 1, 1);
-	gpiod_test_chip_set_pull(0, 3, 1);
-	gpiod_test_chip_set_pull(0, 4, 1);
-	gpiod_test_chip_set_pull(0, 7, 1);
-
-	memset(vals, 0, sizeof(vals));
-	ret = gpiod_line_get_value_bulk(bulk, vals);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(vals[0], ==, 1);
-	g_assert_cmpint(vals[1], ==, 0);
-	g_assert_cmpint(vals[2], ==, 1);
-	g_assert_cmpint(vals[3], ==, 0);
-	g_assert_cmpint(vals[4], ==, 0);
-	g_assert_cmpint(vals[5], ==, 1);
-	g_assert_cmpint(vals[6], ==, 1);
-	g_assert_cmpint(vals[7], ==, 0);
-}
-
-GPIOD_TEST_CASE(wait_multiple, 0, { 8 })
-{
-	g_autoptr(GpiodTestEventThread) ev_thread = NULL;
-	g_autoptr(gpiod_line_bulk_struct) ev_bulk = NULL;
-	g_autoptr(gpiod_line_bulk_struct) bulk = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct timespec ts = { 1, 0 };
-	struct gpiod_line_event ev;
-	struct gpiod_line *line;
-	gint ret, i;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	bulk = gpiod_line_bulk_new(8);
-	ev_bulk = gpiod_line_bulk_new(8);
-	g_assert_nonnull(bulk);
-	g_assert_nonnull(ev_bulk);
-	gpiod_test_return_if_failed();
-
-	for (i = 1; i < 8; i++) {
-		line = gpiod_chip_get_line(chip, i);
-		g_assert_nonnull(line);
-		gpiod_test_return_if_failed();
-
-		gpiod_line_bulk_add_line(bulk, line);
-	}
-
-	ret = gpiod_line_request_bulk_rising_edge_events(bulk,
-							 GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	ev_thread = gpiod_test_start_event_thread(0, 4, 100);
-
-	ret = gpiod_line_event_wait_bulk(bulk, &ts, ev_bulk);
-	g_assert_cmpint(ret, ==, 1);
-
-	g_assert_cmpuint(gpiod_line_bulk_num_lines(ev_bulk), ==, 1);
-	line = gpiod_line_bulk_get_line(ev_bulk, 0);
-	g_assert_cmpuint(gpiod_line_offset(line), ==, 4);
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_RISING_EDGE);
-	g_assert_cmpint(ev.offset, ==, 4);
-}
-
-GPIOD_TEST_CASE(get_fd_when_values_requested, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint ret, fd;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 3);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_input(line, GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	fd = gpiod_line_event_get_fd(line);
-	g_assert_cmpint(fd, ==, -1);
-	g_assert_cmpint(errno, ==, EPERM);
-}
-
-GPIOD_TEST_CASE(request_bulk_fail, 0, { 8 })
-{
-	g_autoptr(gpiod_line_bulk_struct) bulk = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint ret, i;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 7);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_input(line, GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	bulk = gpiod_line_bulk_new(8);
-	g_assert_nonnull(bulk);
-	gpiod_test_return_if_failed();
-
-	for (i = 0; i < 8; i++) {
-		line = gpiod_chip_get_line(chip, i);
-		g_assert_nonnull(line);
-		gpiod_test_return_if_failed();
-		gpiod_line_bulk_add_line(bulk, line);
-	}
-
-	ret = gpiod_line_request_bulk_both_edges_events(bulk,
-							GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, -1);
-	g_assert_cmpint(errno, ==, EBUSY);
-}
-
-GPIOD_TEST_CASE(invalid_fd, 0, { 8 })
-{
-	g_autoptr(gpiod_line_bulk_struct) ev_bulk = NULL;
-	g_autoptr(gpiod_line_bulk_struct) bulk = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct timespec ts = { 1, 0 };
-	struct gpiod_line *line;
-	gint ret, fd;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 7);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_both_edges_events(line, GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	fd = gpiod_line_event_get_fd(line);
-	close(fd);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, -1);
-	g_assert_cmpint(errno, ==, EINVAL);
-
-	bulk = gpiod_line_bulk_new(1);
-	ev_bulk = gpiod_line_bulk_new(1);
-	g_assert_nonnull(bulk);
-	g_assert_nonnull(ev_bulk);
-
-	/*
-	 * The single line variant calls gpiod_line_event_wait_bulk() with the
-	 * event_bulk argument set to NULL, so test this use case explicitly
-	 * as well.
-	 */
-	gpiod_line_bulk_add_line(bulk, line);
-	ret = gpiod_line_event_wait_bulk(bulk, &ts, ev_bulk);
-	g_assert_cmpint(ret, ==, -1);
-	g_assert_cmpint(errno, ==, EINVAL);
-}
-
-GPIOD_TEST_CASE(read_events_individually, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct timespec ts = { 1, 0 };
-	struct gpiod_line_event ev;
-	struct gpiod_line *line;
-	gint ret;
-	guint i;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 7);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_both_edges_events_flags(line,
-		GPIOD_TEST_CONSUMER, GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	/* generate multiple events */
-	for (i = 0; i < 3; i++) {
-		gpiod_test_chip_set_pull(0, 7, i & 1);
-		usleep(10000);
-	}
-
-	/* read them individually... */
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-	if (!ret)
-		return;
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_FALLING_EDGE);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-	if (!ret)
-		return;
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_RISING_EDGE);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-	if (!ret)
-		return;
-
-	ret = gpiod_line_event_read(line, &ev);
-	g_assert_cmpint(ret, ==, 0);
-
-	g_assert_cmpint(ev.event_type, ==, GPIOD_LINE_EVENT_FALLING_EDGE);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 0);
-}
-
-GPIOD_TEST_CASE(read_multiple_events, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line_event events[5];
-	struct timespec ts = { 1, 0 };
-	struct gpiod_line *line;
-	gint ret;
-	guint i;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 4);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_both_edges_events(line, GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	/* generate multiple events */
-	for (i = 0; i < 7; i++) {
-		gpiod_test_chip_set_pull(0, 4, !(i & 1));
-		/*
-		 * We sleep for a short period of time here and in other
-		 * test cases for multiple events to let the kernel service
-		 * each simulated interrupt. Otherwise we'd risk triggering
-		 * an interrupt while the previous one is still being
-		 * handled.
-		 */
-		usleep(10000);
-	}
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-	if (!ret)
-		return;
-
-	/* read a chunk */
-	ret = gpiod_line_event_read_multiple(line, events, 3);
-	g_assert_cmpint(ret, ==, 3);
-
-	g_assert_cmpint(events[0].event_type, ==,
-			GPIOD_LINE_EVENT_RISING_EDGE);
-	g_assert_cmpint(events[1].event_type, ==,
-			GPIOD_LINE_EVENT_FALLING_EDGE);
-	g_assert_cmpint(events[2].event_type, ==,
-			GPIOD_LINE_EVENT_RISING_EDGE);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-	if (!ret)
-		return;
-
-	/*
-	 * read the remainder
-	 * - note the attempt to read more than are available
-	 */
-	ret = gpiod_line_event_read_multiple(line, events, 5);
-	g_assert_cmpint(ret, ==, 4);
-
-	g_assert_cmpint(events[0].event_type, ==,
-			GPIOD_LINE_EVENT_FALLING_EDGE);
-	g_assert_cmpint(events[1].event_type, ==,
-			GPIOD_LINE_EVENT_RISING_EDGE);
-	g_assert_cmpint(events[2].event_type, ==,
-			GPIOD_LINE_EVENT_FALLING_EDGE);
-	g_assert_cmpint(events[3].event_type, ==,
-			GPIOD_LINE_EVENT_RISING_EDGE);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 0);
-}
-
-GPIOD_TEST_CASE(read_multiple_events_fd, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line_event events[5];
-	struct timespec ts = { 1, 0 };
-	struct gpiod_line *line;
-	gint ret, fd;
-	guint i;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 4);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_both_edges_events(line, GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	/* generate multiple events */
-	for (i = 0; i < 7; i++) {
-		gpiod_test_chip_set_pull(0, 4, !(i & 1));
-		usleep(10000);
-	}
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-	if (!ret)
-		return;
-
-	fd = gpiod_line_event_get_fd(line);
-	g_assert_cmpint(fd, >=, 0);
-
-	/* read a chunk */
-	ret = gpiod_line_event_read_fd_multiple(fd, events, 3);
-	g_assert_cmpint(ret, ==, 3);
-
-	g_assert_cmpint(events[0].event_type, ==,
-			GPIOD_LINE_EVENT_RISING_EDGE);
-	g_assert_cmpint(events[1].event_type, ==,
-			GPIOD_LINE_EVENT_FALLING_EDGE);
-	g_assert_cmpint(events[2].event_type, ==,
-			GPIOD_LINE_EVENT_RISING_EDGE);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 1);
-	if (!ret)
-		return;
-
-	/*
-	 * read the remainder
-	 * - note the attempt to read more than are available
-	 */
-	ret = gpiod_line_event_read_fd_multiple(fd, events, 5);
-	g_assert_cmpint(ret, ==, 4);
-
-	g_assert_cmpint(events[0].event_type, ==,
-			GPIOD_LINE_EVENT_FALLING_EDGE);
-	g_assert_cmpint(events[1].event_type, ==,
-			GPIOD_LINE_EVENT_RISING_EDGE);
-	g_assert_cmpint(events[2].event_type, ==,
-			GPIOD_LINE_EVENT_FALLING_EDGE);
-	g_assert_cmpint(events[3].event_type, ==,
-			GPIOD_LINE_EVENT_RISING_EDGE);
-
-	ret = gpiod_line_event_wait(line, &ts);
-	g_assert_cmpint(ret, ==, 0);
-}
diff --git a/tests/tests-info-event.c b/tests/tests-info-event.c
new file mode 100644
index 0000000..8129f16
--- /dev/null
+++ b/tests/tests-info-event.c
@@ -0,0 +1,301 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#include <glib.h>
+#include <gpiod.h>
+#include <poll.h>
+
+#include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+#include "gpiod-test-sim.h"
+
+#define GPIOD_TEST_GROUP "info-event"
+
+GPIOD_TEST_CASE(watching_info_events_returns_line_info)
+{
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_line_info) info = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+	info = gpiod_chip_watch_line_info(chip, 3);
+	g_assert_nonnull(info);
+	g_assert_cmpuint(gpiod_line_info_get_offset(info), ==, 3);
+}
+
+GPIOD_TEST_CASE(try_offset_out_of_range)
+{
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_line_info) info = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+	info = gpiod_chip_watch_line_info(chip, 10);
+	g_assert_null(info);
+	gpiod_test_expect_errno(EINVAL);
+}
+
+GPIOD_TEST_CASE(event_timeout)
+{
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_line_info) info = NULL;
+	gint ret;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+	info = gpiod_chip_watch_line_info(chip, 6);
+	g_assert_nonnull(info);
+	gpiod_test_return_if_failed();
+
+	ret = gpiod_chip_info_event_wait(chip, 100000000);
+	g_assert_cmpint(ret, ==, 0);
+}
+
+struct request_ctx {
+	struct gpiod_chip *chip;
+	struct gpiod_request_config *req_cfg;
+	struct gpiod_line_config *line_cfg;
+};
+
+static gpointer request_release_line(gpointer data)
+{
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	struct request_ctx *ctx = data;
+	gint ret;
+
+	g_usleep(50);
+
+	request = gpiod_chip_request_lines(ctx->chip,
+					   ctx->req_cfg, ctx->line_cfg);
+	g_assert_nonnull(request);
+	if (g_test_failed())
+		return NULL;
+
+	g_usleep(50);
+
+	gpiod_line_config_set_direction_default(ctx->line_cfg,
+						GPIOD_LINE_DIRECTION_OUTPUT);
+
+	ret = gpiod_line_request_reconfigure_lines(request, ctx->line_cfg);
+	g_assert_cmpint(ret, ==, 0);
+	if (g_test_failed())
+		return NULL;
+
+	g_usleep(50);
+
+	gpiod_line_request_release(request);
+	request = NULL;
+
+	return NULL;
+}
+
+GPIOD_TEST_CASE(request_reconfigure_release_events)
+{
+	static const guint offset = 3;
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_line_info) info = NULL;
+	g_autoptr(struct_gpiod_info_event) request_event = NULL;
+	g_autoptr(struct_gpiod_info_event) reconfigure_event = NULL;
+	g_autoptr(struct_gpiod_info_event) release_event = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(GThread) thread = NULL;
+	struct gpiod_line_info *request_info, *reconfigure_info, *release_info;
+	guint64 request_ts, reconfigure_ts, release_ts;
+	struct request_ctx ctx;
+	gint ret;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 1, &offset);
+
+	info = gpiod_chip_watch_line_info(chip, 3);
+	g_assert_nonnull(info);
+	gpiod_test_return_if_failed();
+
+	g_assert_false(gpiod_line_info_is_used(info));
+
+	ctx.chip = chip;
+	ctx.req_cfg = req_cfg;
+	ctx.line_cfg = line_cfg;
+
+	thread = g_thread_new("request-release", request_release_line, &ctx);
+	g_thread_ref(thread);
+
+	ret = gpiod_chip_info_event_wait(chip, 1000000000);
+	g_assert_cmpint(ret, >, 0);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	request_event = gpiod_chip_info_event_read(chip);
+	g_assert_nonnull(request_event);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpint(gpiod_info_event_get_event_type(request_event), ==,
+			GPIOD_INFO_EVENT_LINE_REQUESTED);
+
+	request_info = gpiod_info_event_get_line_info(request_event);
+
+	g_assert_cmpuint(gpiod_line_info_get_offset(request_info), ==, 3);
+	g_assert_true(gpiod_line_info_is_used(request_info));
+	g_assert_cmpint(gpiod_line_info_get_direction(request_info), ==,
+			GPIOD_LINE_DIRECTION_INPUT);
+
+	ret = gpiod_chip_info_event_wait(chip, 1000000000);
+	g_assert_cmpint(ret, >, 0);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	reconfigure_event = gpiod_chip_info_event_read(chip);
+	g_assert_nonnull(reconfigure_event);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpint(gpiod_info_event_get_event_type(reconfigure_event), ==,
+			GPIOD_INFO_EVENT_LINE_CONFIG_CHANGED);
+
+	reconfigure_info = gpiod_info_event_get_line_info(reconfigure_event);
+
+	g_assert_cmpuint(gpiod_line_info_get_offset(reconfigure_info), ==, 3);
+	g_assert_true(gpiod_line_info_is_used(reconfigure_info));
+	g_assert_cmpint(gpiod_line_info_get_direction(reconfigure_info), ==,
+			GPIOD_LINE_DIRECTION_OUTPUT);
+
+	ret = gpiod_chip_info_event_wait(chip, 1000000000);
+	g_assert_cmpint(ret, >, 0);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	release_event = gpiod_chip_info_event_read(chip);
+	g_assert_nonnull(release_event);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpint(gpiod_info_event_get_event_type(release_event), ==,
+			GPIOD_INFO_EVENT_LINE_RELEASED);
+
+	release_info = gpiod_info_event_get_line_info(release_event);
+
+	g_assert_cmpuint(gpiod_line_info_get_offset(release_info), ==, 3);
+	g_assert_false(gpiod_line_info_is_used(release_info));
+
+	g_thread_join(thread);
+
+	request_ts = gpiod_info_event_get_timestamp(request_event);
+	reconfigure_ts = gpiod_info_event_get_timestamp(reconfigure_event);
+	release_ts = gpiod_info_event_get_timestamp(release_event);
+
+	g_assert_cmpuint(request_ts, <, reconfigure_ts);
+	g_assert_cmpuint(reconfigure_ts, <, release_ts);
+}
+
+GPIOD_TEST_CASE(chip_fd_can_be_polled)
+{
+	static const guint offset = 3;
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_line_info) info = NULL;
+	g_autoptr(struct_gpiod_info_event) event = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	g_autoptr(GThread) thread = NULL;
+	struct gpiod_line_info *evinfo;
+	struct request_ctx ctx;
+	struct timespec ts;
+	struct pollfd pfd;
+	gint ret, fd;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 1, &offset);
+
+	info = gpiod_chip_watch_line_info(chip, 3);
+	g_assert_nonnull(info);
+	gpiod_test_return_if_failed();
+
+	g_assert_false(gpiod_line_info_is_used(info));
+
+	ctx.chip = chip;
+	ctx.req_cfg = req_cfg;
+	ctx.line_cfg = line_cfg;
+
+	thread = g_thread_new("request-release", request_release_line, &ctx);
+	g_thread_ref(thread);
+
+	fd = gpiod_chip_get_fd(chip);
+
+	memset(&pfd, 0, sizeof(pfd));
+	pfd.fd = fd;
+	pfd.events = POLLIN | POLLPRI;
+
+	ts.tv_sec = 1;
+	ts.tv_nsec = 0;
+
+	ret = ppoll(&pfd, 1, &ts, NULL);
+	g_assert_cmpint(ret, >, 0);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	event = gpiod_chip_info_event_read(chip);
+	g_assert_nonnull(event);
+	gpiod_test_join_thread_and_return_if_failed(thread);
+
+	g_assert_cmpint(gpiod_info_event_get_event_type(event), ==,
+			GPIOD_INFO_EVENT_LINE_REQUESTED);
+
+	evinfo = gpiod_info_event_get_line_info(event);
+
+	g_assert_cmpuint(gpiod_line_info_get_offset(evinfo), ==, 3);
+	g_assert_true(gpiod_line_info_is_used(evinfo));
+
+	g_thread_join(thread);
+}
+
+GPIOD_TEST_CASE(unwatch_and_check_that_no_events_are_generated)
+{
+	static const guint offset = 3;
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_line_info) info = NULL;
+	g_autoptr(struct_gpiod_info_event) event = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	gint ret;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 1, &offset);
+
+	info = gpiod_chip_watch_line_info(chip, 3);
+	g_assert_nonnull(info);
+	gpiod_test_return_if_failed();
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	ret = gpiod_chip_info_event_wait(chip, 100000000);
+	g_assert_cmpint(ret, >, 0);
+	gpiod_test_return_if_failed();
+
+	event = gpiod_chip_info_event_read(chip);
+	g_assert_nonnull(event);
+	gpiod_test_return_if_failed();
+
+	ret = gpiod_chip_unwatch_line_info(chip, 3);
+	g_assert_cmpint(ret, ==, 0);
+	gpiod_test_return_if_failed();
+
+	gpiod_line_request_release(request);
+	request = NULL;
+
+	ret = gpiod_chip_info_event_wait(chip, 100000000);
+	g_assert_cmpint(ret, ==, 0);
+}
diff --git a/tests/tests-line-config.c b/tests/tests-line-config.c
new file mode 100644
index 0000000..d919016
--- /dev/null
+++ b/tests/tests-line-config.c
@@ -0,0 +1,503 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#include <errno.h>
+#include <glib.h>
+#include <gpiod.h>
+#include <stdint.h>
+
+#include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+#include "gpiod-test-sim.h"
+
+#define GPIOD_TEST_GROUP "line-config"
+
+GPIOD_TEST_CASE(default_config)
+{
+	g_autoptr(struct_gpiod_line_config) config = NULL;
+
+	config = gpiod_test_create_line_config_or_fail();
+
+	g_assert_cmpint(gpiod_line_config_get_direction_default(config), ==,
+			GPIOD_LINE_DIRECTION_AS_IS);
+	g_assert_cmpint(gpiod_line_config_get_edge_detection_default(config),
+			==, GPIOD_LINE_EDGE_NONE);
+	g_assert_cmpint(gpiod_line_config_get_bias_default(config), ==,
+			GPIOD_LINE_BIAS_AS_IS);
+	g_assert_cmpint(gpiod_line_config_get_drive_default(config), ==,
+			GPIOD_LINE_DRIVE_PUSH_PULL);
+	g_assert_false(gpiod_line_config_get_active_low_default(config));
+	g_assert_cmpuint(
+		gpiod_line_config_get_debounce_period_us_default(config), ==,
+		0);
+	g_assert_cmpint(gpiod_line_config_get_event_clock_default(config), ==,
+			GPIOD_LINE_EVENT_CLOCK_MONOTONIC);
+	g_assert_cmpint(gpiod_line_config_get_output_value_default(config), ==,
+			GPIOD_LINE_VALUE_INACTIVE);
+	g_assert_cmpuint(gpiod_line_config_get_num_overrides(config),
+			 ==, 0);
+}
+
+GPIOD_TEST_CASE(set_defaults_at_object_construction)
+{
+	g_autoptr(struct_gpiod_line_config) config = NULL;
+
+	/*
+	 * These are wrong and wouldn't work but let's just test that every
+	 * property can be set.
+	 */
+	config = gpiod_line_config_new_defaults(
+			GPIOD_LINE_CONFIG_PROP_DIRECTION,
+			GPIOD_LINE_DIRECTION_OUTPUT,
+			GPIOD_LINE_CONFIG_PROP_EDGE,
+			GPIOD_LINE_EDGE_BOTH,
+			GPIOD_LINE_CONFIG_PROP_BIAS,
+			GPIOD_LINE_BIAS_PULL_DOWN,
+			GPIOD_LINE_CONFIG_PROP_DRIVE,
+			GPIOD_LINE_DRIVE_OPEN_DRAIN,
+			GPIOD_LINE_CONFIG_PROP_ACTIVE_LOW,
+			true,
+			GPIOD_LINE_CONFIG_PROP_DEBOUNCE_PERIOD,
+			1000,
+			GPIOD_LINE_CONFIG_PROP_EVENT_CLOCK,
+			GPIOD_LINE_EVENT_CLOCK_REALTIME,
+			GPIOD_LINE_CONFIG_PROP_OUTPUT_VALUE,
+			GPIOD_LINE_VALUE_ACTIVE,
+			GPIOD_LINE_CONFIG_PROP_END);
+	g_assert_nonnull(config);
+
+	g_assert_cmpint(gpiod_line_config_get_direction_default(config), ==,
+			GPIOD_LINE_DIRECTION_OUTPUT);
+	g_assert_cmpint(gpiod_line_config_get_edge_detection_default(config),
+			==, GPIOD_LINE_EDGE_BOTH);
+	g_assert_cmpint(gpiod_line_config_get_bias_default(config), ==,
+			GPIOD_LINE_BIAS_PULL_DOWN);
+	g_assert_cmpint(gpiod_line_config_get_drive_default(config), ==,
+			GPIOD_LINE_DRIVE_OPEN_DRAIN);
+	g_assert_true(gpiod_line_config_get_active_low_default(config));
+	g_assert_cmpuint(
+		gpiod_line_config_get_debounce_period_us_default(config),
+		==, 1000);
+	g_assert_cmpint(gpiod_line_config_get_event_clock_default(config), ==,
+			GPIOD_LINE_EVENT_CLOCK_REALTIME);
+	g_assert_cmpint(gpiod_line_config_get_output_value_default(config), ==,
+			GPIOD_LINE_VALUE_ACTIVE);
+}
+
+GPIOD_TEST_CASE(defaults_are_used_for_non_overridden_offsets)
+{
+	g_autoptr(struct_gpiod_line_config) config = NULL;
+
+	config = gpiod_test_create_line_config_or_fail();
+
+	g_assert_cmpint(gpiod_line_config_get_direction_offset(config, 4), ==,
+			GPIOD_LINE_DIRECTION_AS_IS);
+	g_assert_cmpint(gpiod_line_config_get_edge_detection_offset(config, 4),
+			==, GPIOD_LINE_EDGE_NONE);
+	g_assert_cmpint(gpiod_line_config_get_bias_offset(config, 4), ==,
+			GPIOD_LINE_BIAS_AS_IS);
+	g_assert_cmpint(gpiod_line_config_get_drive_offset(config, 4), ==,
+			GPIOD_LINE_DRIVE_PUSH_PULL);
+	g_assert_false(gpiod_line_config_get_active_low_offset(config, 4));
+	g_assert_cmpuint(
+		gpiod_line_config_get_debounce_period_us_offset(config, 4), ==,
+		0);
+	g_assert_cmpint(gpiod_line_config_get_event_clock_offset(config, 4),
+			==, GPIOD_LINE_EVENT_CLOCK_MONOTONIC);
+	g_assert_cmpint(gpiod_line_config_get_output_value_offset(config, 4),
+			==, GPIOD_LINE_VALUE_INACTIVE);
+	g_assert_cmpuint(gpiod_line_config_get_num_overrides(config),
+			 ==, 0);
+}
+
+GPIOD_TEST_CASE(set_and_clear_direction_override)
+{
+	g_autoptr(struct_gpiod_line_config) config = NULL;
+
+	config = gpiod_test_create_line_config_or_fail();
+
+	g_assert_cmpint(gpiod_line_config_get_direction_default(config), ==,
+			GPIOD_LINE_DIRECTION_AS_IS);
+	gpiod_line_config_set_direction_override(config,
+						 GPIOD_LINE_DIRECTION_OUTPUT,
+						 3);
+
+	g_assert_cmpint(gpiod_line_config_get_direction_default(config), ==,
+			GPIOD_LINE_DIRECTION_AS_IS);
+	g_assert_cmpint(gpiod_line_config_get_direction_offset(config, 3), ==,
+			GPIOD_LINE_DIRECTION_OUTPUT);
+	g_assert_true(gpiod_line_config_direction_is_overridden(config, 3));
+	gpiod_line_config_clear_direction_override(config, 3);
+	g_assert_cmpint(gpiod_line_config_get_direction_offset(config, 3), ==,
+			GPIOD_LINE_DIRECTION_AS_IS);
+	g_assert_false(gpiod_line_config_direction_is_overridden(config, 3));
+}
+
+GPIOD_TEST_CASE(invalid_direction)
+{
+	g_autoptr(struct_gpiod_line_config) config = NULL;
+
+	config = gpiod_test_create_line_config_or_fail();
+
+	gpiod_line_config_set_direction_default(config, INT32_MAX);
+	g_assert_cmpint(gpiod_line_config_get_direction_default(config),
+			==, GPIOD_LINE_DIRECTION_AS_IS);
+}
+
+GPIOD_TEST_CASE(set_and_clear_edge_detection_override)
+{
+	g_autoptr(struct_gpiod_line_config) config = NULL;
+
+	config = gpiod_test_create_line_config_or_fail();
+
+	g_assert_cmpint(gpiod_line_config_get_edge_detection_default(config),
+			==, GPIOD_LINE_EDGE_NONE);
+	gpiod_line_config_set_edge_detection_override(config,
+						GPIOD_LINE_EDGE_FALLING, 3);
+
+	g_assert_cmpint(gpiod_line_config_get_edge_detection_default(config),
+			==, GPIOD_LINE_EDGE_NONE);
+	g_assert_cmpint(gpiod_line_config_get_edge_detection_offset(config, 3),
+			==, GPIOD_LINE_EDGE_FALLING);
+	g_assert_true(gpiod_line_config_edge_detection_is_overridden(config,
+								     3));
+	gpiod_line_config_clear_edge_detection_override(config, 3);
+	g_assert_cmpint(gpiod_line_config_get_edge_detection_offset(config, 3),
+			==, GPIOD_LINE_EDGE_NONE);
+	g_assert_false(gpiod_line_config_edge_detection_is_overridden(config,
+								      3));
+}
+
+GPIOD_TEST_CASE(invalid_edge)
+{
+	g_autoptr(struct_gpiod_line_config) config = NULL;
+
+	config = gpiod_test_create_line_config_or_fail();
+
+	gpiod_line_config_set_edge_detection_default(config, INT32_MAX);
+	g_assert_cmpint(gpiod_line_config_get_edge_detection_default(config),
+			==, GPIOD_LINE_EDGE_NONE);
+}
+
+GPIOD_TEST_CASE(set_and_clear_bias_override)
+{
+	g_autoptr(struct_gpiod_line_config) config = NULL;
+
+	config = gpiod_test_create_line_config_or_fail();
+
+	g_assert_cmpint(gpiod_line_config_get_bias_default(config),
+			==, GPIOD_LINE_BIAS_AS_IS);
+	gpiod_line_config_set_bias_override(config, GPIOD_LINE_BIAS_PULL_UP, 0);
+
+	g_assert_cmpint(gpiod_line_config_get_bias_default(config),
+			==, GPIOD_LINE_BIAS_AS_IS);
+	g_assert_cmpint(gpiod_line_config_get_bias_offset(config, 0),
+			==, GPIOD_LINE_BIAS_PULL_UP);
+	g_assert_true(gpiod_line_config_bias_is_overridden(config, 0));
+	gpiod_line_config_clear_bias_override(config, 0);
+	g_assert_cmpint(gpiod_line_config_get_bias_offset(config, 0),
+			==, GPIOD_LINE_BIAS_AS_IS);
+	g_assert_false(gpiod_line_config_bias_is_overridden(config, 0));
+}
+
+GPIOD_TEST_CASE(invalid_bias)
+{
+	g_autoptr(struct_gpiod_line_config) config = NULL;
+
+	config = gpiod_test_create_line_config_or_fail();
+
+	gpiod_line_config_set_bias_default(config, INT32_MAX);
+	g_assert_cmpint(gpiod_line_config_get_bias_default(config),
+			==, GPIOD_LINE_BIAS_AS_IS);
+}
+
+GPIOD_TEST_CASE(set_and_clear_drive_override)
+{
+	g_autoptr(struct_gpiod_line_config) config = NULL;
+
+	config = gpiod_test_create_line_config_or_fail();
+
+	g_assert_cmpint(gpiod_line_config_get_drive_default(config),
+			==, GPIOD_LINE_DRIVE_PUSH_PULL);
+	gpiod_line_config_set_drive_override(config,
+					     GPIOD_LINE_DRIVE_OPEN_DRAIN, 3);
+
+	g_assert_cmpint(gpiod_line_config_get_drive_default(config),
+			==, GPIOD_LINE_DRIVE_PUSH_PULL);
+	g_assert_cmpint(gpiod_line_config_get_drive_offset(config, 3),
+			==, GPIOD_LINE_DRIVE_OPEN_DRAIN);
+	g_assert_true(gpiod_line_config_drive_is_overridden(config, 3));
+	gpiod_line_config_clear_drive_override(config, 3);
+	g_assert_cmpint(gpiod_line_config_get_drive_offset(config, 3),
+			==, GPIOD_LINE_DRIVE_PUSH_PULL);
+	g_assert_false(gpiod_line_config_drive_is_overridden(config, 3));
+}
+
+GPIOD_TEST_CASE(invalid_drive)
+{
+	g_autoptr(struct_gpiod_line_config) config = NULL;
+
+	config = gpiod_test_create_line_config_or_fail();
+
+	gpiod_line_config_set_drive_default(config, INT32_MAX);
+	g_assert_cmpint(gpiod_line_config_get_drive_default(config),
+			==, GPIOD_LINE_BIAS_AS_IS);
+}
+
+GPIOD_TEST_CASE(set_and_clear_active_low_override)
+{
+	g_autoptr(struct_gpiod_line_config) config = NULL;
+
+	config = gpiod_test_create_line_config_or_fail();
+
+	g_assert_false(gpiod_line_config_get_active_low_default(config));
+	gpiod_line_config_set_active_low_override(config, true, 3);
+
+	g_assert_false(gpiod_line_config_get_active_low_default(config));
+	g_assert_true(gpiod_line_config_get_active_low_offset(config, 3));
+	g_assert_true(gpiod_line_config_active_low_is_overridden(config, 3));
+	gpiod_line_config_clear_active_low_override(config, 3);
+	g_assert_false(gpiod_line_config_get_active_low_offset(config, 3));
+	g_assert_false(gpiod_line_config_active_low_is_overridden(config, 3));
+}
+
+GPIOD_TEST_CASE(set_and_clear_debounce_period_override)
+{
+	g_autoptr(struct_gpiod_line_config) config = NULL;
+
+	config = gpiod_test_create_line_config_or_fail();
+
+	g_assert_cmpuint(
+		gpiod_line_config_get_debounce_period_us_default(config),
+		==, 0);
+	gpiod_line_config_set_debounce_period_us_override(config, 5000, 3);
+
+	g_assert_cmpuint(
+		gpiod_line_config_get_debounce_period_us_default(config),
+		==, 0);
+	g_assert_cmpuint(
+		gpiod_line_config_get_debounce_period_us_offset(config, 3),
+		==, 5000);
+	g_assert_true(
+		gpiod_line_config_debounce_period_us_is_overridden(config, 3));
+	gpiod_line_config_clear_debounce_period_us_override(config, 3);
+	g_assert_cmpuint(
+		gpiod_line_config_get_debounce_period_us_offset(config, 3),
+		==, 0);
+	g_assert_false(
+		gpiod_line_config_debounce_period_us_is_overridden(config, 3));
+}
+
+GPIOD_TEST_CASE(set_and_clear_event_clock_override)
+{
+	g_autoptr(struct_gpiod_line_config) config = NULL;
+
+	config = gpiod_test_create_line_config_or_fail();
+
+	g_assert_cmpint(gpiod_line_config_get_event_clock_default(config),
+			==, GPIOD_LINE_EVENT_CLOCK_MONOTONIC);
+	gpiod_line_config_set_event_clock_override(config,
+					GPIOD_LINE_EVENT_CLOCK_REALTIME, 3);
+
+	g_assert_cmpint(gpiod_line_config_get_event_clock_default(config),
+			==, GPIOD_LINE_EVENT_CLOCK_MONOTONIC);
+	g_assert_cmpint(gpiod_line_config_get_event_clock_offset(config, 3),
+			==, GPIOD_LINE_EVENT_CLOCK_REALTIME);
+	g_assert_true(gpiod_line_config_event_clock_is_overridden(config, 3));
+	gpiod_line_config_clear_event_clock_override(config, 3);
+	g_assert_cmpint(gpiod_line_config_get_event_clock_offset(config, 3),
+			==, GPIOD_LINE_EVENT_CLOCK_MONOTONIC);
+	g_assert_false(gpiod_line_config_event_clock_is_overridden(config, 3));
+}
+
+GPIOD_TEST_CASE(invalid_event_clock)
+{
+	g_autoptr(struct_gpiod_line_config) config = NULL;
+
+	config = gpiod_test_create_line_config_or_fail();
+
+	gpiod_line_config_set_event_clock_default(config, INT32_MAX);
+	g_assert_cmpint(gpiod_line_config_get_event_clock_default(config),
+			==, GPIOD_LINE_EVENT_CLOCK_MONOTONIC);
+}
+
+GPIOD_TEST_CASE(set_and_clear_output_value_override)
+{
+	g_autoptr(struct_gpiod_line_config) config = NULL;
+
+	config = gpiod_test_create_line_config_or_fail();
+
+	g_assert_cmpint(gpiod_line_config_get_output_value_default(config),
+			==, GPIOD_LINE_VALUE_INACTIVE);
+	gpiod_line_config_set_output_value_override(config, 3,
+						    GPIOD_LINE_VALUE_ACTIVE);
+
+	g_assert_cmpint(gpiod_line_config_get_output_value_default(config),
+			==, GPIOD_LINE_VALUE_INACTIVE);
+	g_assert_cmpint(gpiod_line_config_get_output_value_offset(config, 3),
+			==, GPIOD_LINE_VALUE_ACTIVE);
+	g_assert_true(gpiod_line_config_output_value_is_overridden(config, 3));
+	gpiod_line_config_clear_output_value_override(config, 3);
+	g_assert_cmpint(gpiod_line_config_get_output_value_offset(config, 3),
+			==, 0);
+	g_assert_false(gpiod_line_config_output_value_is_overridden(config, 3));
+}
+
+GPIOD_TEST_CASE(set_multiple_output_values)
+{
+	static const guint offsets[] = { 3, 4, 5, 6 };
+	static const gint values[] = { GPIOD_LINE_VALUE_ACTIVE,
+				       GPIOD_LINE_VALUE_INACTIVE,
+				       GPIOD_LINE_VALUE_ACTIVE,
+				       GPIOD_LINE_VALUE_INACTIVE };
+
+	g_autoptr(struct_gpiod_line_config) config = NULL;
+	guint overridden_offsets[4], i;
+	gint overriden_props[4];
+
+	config = gpiod_test_create_line_config_or_fail();
+
+	gpiod_line_config_set_output_values(config, 4, offsets, values);
+
+	g_assert_cmpint(gpiod_line_config_get_output_value_default(config),
+			==, 0);
+
+	for (i = 0; i < 4; i++)
+		g_assert_cmpint(
+			gpiod_line_config_get_output_value_offset(config,
+								  offsets[i]),
+			==, values[i]);
+
+	g_assert_cmpuint(gpiod_line_config_get_num_overrides(config),
+			==, 4);
+	gpiod_line_config_get_overrides(config,
+					overridden_offsets, overriden_props);
+
+	for (i = 0; i < 4; i++) {
+		g_assert_cmpuint(overridden_offsets[i], ==, offsets[i]);
+		g_assert_cmpint(overriden_props[i], ==,
+				GPIOD_LINE_CONFIG_PROP_OUTPUT_VALUE);
+	}
+}
+
+GPIOD_TEST_CASE(config_too_complex)
+{
+	static guint offsets[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 16, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	line_cfg = gpiod_test_create_line_config_or_fail();
+	req_cfg = gpiod_test_create_request_config_or_fail();
+
+	/*
+	 * We need to make the line_config structure exceed the kernel's
+	 * maximum of 10 attributes.
+	 */
+	gpiod_line_config_set_direction_override(line_cfg,
+					GPIOD_LINE_DIRECTION_OUTPUT, 0);
+	gpiod_line_config_set_direction_override(line_cfg,
+					GPIOD_LINE_DIRECTION_INPUT, 1);
+	gpiod_line_config_set_edge_detection_override(line_cfg,
+						      GPIOD_LINE_EDGE_BOTH, 2);
+	gpiod_line_config_set_debounce_period_us_override(line_cfg, 1000, 2);
+	gpiod_line_config_set_active_low_override(line_cfg, true, 3);
+	gpiod_line_config_set_direction_override(line_cfg,
+					GPIOD_LINE_DIRECTION_OUTPUT, 4);
+	gpiod_line_config_set_drive_override(line_cfg,
+					     GPIOD_LINE_DRIVE_OPEN_DRAIN, 4);
+	gpiod_line_config_set_direction_override(line_cfg,
+					GPIOD_LINE_DIRECTION_OUTPUT, 8);
+	gpiod_line_config_set_drive_override(line_cfg,
+					     GPIOD_LINE_DRIVE_OPEN_SOURCE, 8);
+	gpiod_line_config_set_direction_override(line_cfg,
+						 GPIOD_LINE_DIRECTION_INPUT, 5);
+	gpiod_line_config_set_bias_override(line_cfg,
+					    GPIOD_LINE_BIAS_PULL_DOWN, 5);
+	gpiod_line_config_set_event_clock_override(line_cfg,
+					GPIOD_LINE_EVENT_CLOCK_REALTIME, 6);
+	gpiod_line_config_set_output_value_override(line_cfg, 7,
+						    GPIOD_LINE_VALUE_ACTIVE);
+
+	gpiod_request_config_set_offsets(req_cfg, 12, offsets);
+
+	request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+	g_assert_null(request);
+	gpiod_test_expect_errno(E2BIG);
+}
+
+/*
+ * This triggers the E2BIG error by exhausting the number of overrides in
+ * the line_config structure instead of making the kernel representation too
+ * complex.
+ */
+GPIOD_TEST_CASE(define_too_many_overrides)
+{
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 128, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	guint offsets[65], i;
+
+	for (i = 0; i < 65; i++)
+		offsets[i] = i;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	line_cfg = gpiod_test_create_line_config_or_fail();
+	req_cfg = gpiod_test_create_request_config_or_fail();
+
+	for (i = 0; i < 65; i++)
+		gpiod_line_config_set_direction_override(line_cfg,
+				GPIOD_LINE_DIRECTION_OUTPUT, offsets[i]);
+
+	gpiod_request_config_set_offsets(req_cfg, 64, offsets);
+
+	request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+	g_assert_null(request);
+	gpiod_test_expect_errno(E2BIG);
+}
+
+GPIOD_TEST_CASE(ignore_overrides_for_offsets_not_in_request_config)
+{
+	static guint offsets[] = { 2, 3, 4, 6, 7 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	g_autoptr(struct_gpiod_line_info) info3 = NULL;
+	g_autoptr(struct_gpiod_line_info) info4 = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	line_cfg = gpiod_test_create_line_config_or_fail();
+	req_cfg = gpiod_test_create_request_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 5, offsets);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_INPUT);
+	gpiod_line_config_set_direction_override(line_cfg,
+					GPIOD_LINE_DIRECTION_OUTPUT, 4);
+	gpiod_line_config_set_direction_override(line_cfg,
+					GPIOD_LINE_DIRECTION_OUTPUT, 5);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+	info3 = gpiod_test_get_line_info_or_fail(chip, 3);
+	info4 = gpiod_test_get_line_info_or_fail(chip, 4);
+
+	g_assert_cmpint(gpiod_line_info_get_direction(info3), ==,
+			GPIOD_LINE_DIRECTION_INPUT);
+	g_assert_cmpint(gpiod_line_info_get_direction(info4), ==,
+			GPIOD_LINE_DIRECTION_OUTPUT);
+
+	gpiod_line_config_set_direction_override(line_cfg,
+					GPIOD_LINE_DIRECTION_OUTPUT, 0);
+
+	gpiod_test_reconfigure_lines_or_fail(request, line_cfg);
+	/* Nothing to check, value successfully ignored. */
+}
diff --git a/tests/tests-line-info.c b/tests/tests-line-info.c
new file mode 100644
index 0000000..757baf6
--- /dev/null
+++ b/tests/tests-line-info.c
@@ -0,0 +1,318 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#include <errno.h>
+#include <glib.h>
+#include <gpiod.h>
+
+#include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+#include "gpiod-test-sim.h"
+
+#define GPIOD_TEST_GROUP "line-info"
+
+GPIOD_TEST_CASE(get_line_info_good)
+{
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_line_info) info = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+	info = gpiod_chip_get_line_info(chip, 3);
+	g_assert_nonnull(info);
+	g_assert_cmpuint(gpiod_line_info_get_offset(info), ==, 3);
+}
+
+GPIOD_TEST_CASE(get_line_info_offset_out_of_range)
+{
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_line_info) info = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+	info = gpiod_chip_get_line_info(chip, 8);
+	g_assert_null(info);
+	gpiod_test_expect_errno(EINVAL);
+}
+
+GPIOD_TEST_CASE(line_info_basic_properties)
+{
+	static const struct gpiod_test_line_name names[] = {
+		{ .offset = 1, .name = "foo", },
+		{ .offset = 2, .name = "bar", },
+		{ .offset = 4, .name = "baz", },
+		{ .offset = 5, .name = "xyz", },
+		{ }
+	};
+
+	static const struct gpiod_test_hog hogs[] = {
+		{
+			.offset = 3,
+			.name = "hog3",
+			.direction = G_GPIOSIM_HOG_DIR_OUTPUT_HIGH,
+		},
+		{
+			.offset = 4,
+			.name = "hog4",
+			.direction = G_GPIOSIM_HOG_DIR_OUTPUT_LOW,
+		},
+		{ }
+	};
+
+	g_autoptr(GPIOSimChip) sim = NULL;
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_line_info) info4 = NULL;
+	g_autoptr(struct_gpiod_line_info) info6 = NULL;
+
+	sim = g_gpiosim_chip_new(
+			"num-lines", 8,
+			"line-names", gpiod_test_package_line_names(names),
+			"hogs", gpiod_test_package_hogs(hogs),
+			NULL);
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	info4 = gpiod_test_get_line_info_or_fail(chip, 4);
+	info6 = gpiod_test_get_line_info_or_fail(chip, 6);
+
+	g_assert_cmpuint(gpiod_line_info_get_offset(info4), ==, 4);
+	g_assert_cmpstr(gpiod_line_info_get_name(info4), ==, "baz");
+	g_assert_cmpstr(gpiod_line_info_get_consumer(info4), ==, "hog4");
+	g_assert_true(gpiod_line_info_is_used(info4));
+	g_assert_cmpint(gpiod_line_info_get_direction(info4), ==,
+			GPIOD_LINE_DIRECTION_OUTPUT);
+	g_assert_cmpint(gpiod_line_info_get_edge_detection(info4), ==,
+			GPIOD_LINE_EDGE_NONE);
+	g_assert_false(gpiod_line_info_is_active_low(info4));
+	g_assert_cmpint(gpiod_line_info_get_bias(info4), ==,
+			GPIOD_LINE_BIAS_UNKNOWN);
+	g_assert_cmpint(gpiod_line_info_get_drive(info4), ==,
+			GPIOD_LINE_DRIVE_PUSH_PULL);
+	g_assert_cmpint(gpiod_line_info_get_event_clock(info4), ==,
+			GPIOD_LINE_EVENT_CLOCK_MONOTONIC);
+	g_assert_false(gpiod_line_info_is_debounced(info4));
+	g_assert_cmpuint(gpiod_line_info_get_debounce_period_us(info4), ==, 0);
+}
+
+GPIOD_TEST_CASE(copy_line_info)
+{
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_line_info) info = NULL;
+	g_autoptr(struct_gpiod_line_info) copy = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	info = gpiod_test_get_line_info_or_fail(chip, 3);
+
+	copy = gpiod_line_info_copy(info);
+	g_assert_nonnull(copy);
+	g_assert_true(info != copy);
+	g_assert_cmpuint(gpiod_line_info_get_offset(info), ==,
+			 gpiod_line_info_get_offset(copy));
+}
+
+GPIOD_TEST_CASE(active_high)
+{
+	static const guint offset = 5;
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	g_autoptr(struct_gpiod_line_info) info = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 1, &offset);
+	gpiod_line_config_set_active_low_default(line_cfg, true);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+	info = gpiod_chip_get_line_info(chip, 5);
+
+	g_assert_true(gpiod_line_info_is_active_low(info));
+}
+
+GPIOD_TEST_CASE(edge_detection_settings)
+{
+	static const guint offsets[] = { 0, 1, 2, 3 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	g_autoptr(struct_gpiod_line_info) info0 = NULL;
+	g_autoptr(struct_gpiod_line_info) info1 = NULL;
+	g_autoptr(struct_gpiod_line_info) info2 = NULL;
+	g_autoptr(struct_gpiod_line_info) info3 = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 4, offsets);
+	gpiod_line_config_set_edge_detection_override(line_cfg,
+						GPIOD_LINE_EDGE_RISING, 1);
+	gpiod_line_config_set_edge_detection_override(line_cfg,
+						GPIOD_LINE_EDGE_FALLING, 2);
+	gpiod_line_config_set_edge_detection_override(line_cfg,
+						GPIOD_LINE_EDGE_BOTH, 3);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+	info0 = gpiod_chip_get_line_info(chip, 0);
+	info1 = gpiod_chip_get_line_info(chip, 1);
+	info2 = gpiod_chip_get_line_info(chip, 2);
+	info3 = gpiod_chip_get_line_info(chip, 3);
+
+	g_assert_cmpint(gpiod_line_info_get_edge_detection(info0), ==,
+			GPIOD_LINE_EDGE_NONE);
+	g_assert_cmpint(gpiod_line_info_get_edge_detection(info1), ==,
+			GPIOD_LINE_EDGE_RISING);
+	g_assert_cmpint(gpiod_line_info_get_edge_detection(info2), ==,
+			GPIOD_LINE_EDGE_FALLING);
+	g_assert_cmpint(gpiod_line_info_get_edge_detection(info3), ==,
+			GPIOD_LINE_EDGE_BOTH);
+}
+
+GPIOD_TEST_CASE(bias_settings)
+{
+	static const guint offsets[] = { 0, 1, 2, 3 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	g_autoptr(struct_gpiod_line_info) info0 = NULL;
+	g_autoptr(struct_gpiod_line_info) info1 = NULL;
+	g_autoptr(struct_gpiod_line_info) info2 = NULL;
+	g_autoptr(struct_gpiod_line_info) info3 = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 4, offsets);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_OUTPUT);
+	gpiod_line_config_set_bias_override(line_cfg,
+					    GPIOD_LINE_BIAS_DISABLED, 1);
+	gpiod_line_config_set_bias_override(line_cfg,
+					    GPIOD_LINE_BIAS_PULL_DOWN, 2);
+	gpiod_line_config_set_bias_override(line_cfg,
+					    GPIOD_LINE_BIAS_PULL_UP, 3);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+	info0 = gpiod_chip_get_line_info(chip, 0);
+	info1 = gpiod_chip_get_line_info(chip, 1);
+	info2 = gpiod_chip_get_line_info(chip, 2);
+	info3 = gpiod_chip_get_line_info(chip, 3);
+
+	g_assert_cmpint(gpiod_line_info_get_bias(info0), ==,
+			GPIOD_LINE_BIAS_UNKNOWN);
+	g_assert_cmpint(gpiod_line_info_get_bias(info1), ==,
+			GPIOD_LINE_BIAS_DISABLED);
+	g_assert_cmpint(gpiod_line_info_get_bias(info2), ==,
+			GPIOD_LINE_BIAS_PULL_DOWN);
+	g_assert_cmpint(gpiod_line_info_get_bias(info3), ==,
+			GPIOD_LINE_BIAS_PULL_UP);
+}
+
+GPIOD_TEST_CASE(drive_settings)
+{
+	static const guint offsets[] = { 0, 1, 2 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	g_autoptr(struct_gpiod_line_info) info0 = NULL;
+	g_autoptr(struct_gpiod_line_info) info1 = NULL;
+	g_autoptr(struct_gpiod_line_info) info2 = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 3, offsets);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_OUTPUT);
+	gpiod_line_config_set_drive_override(line_cfg,
+					     GPIOD_LINE_DRIVE_OPEN_DRAIN, 1);
+	gpiod_line_config_set_drive_override(line_cfg,
+					     GPIOD_LINE_DRIVE_OPEN_SOURCE, 2);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+	info0 = gpiod_chip_get_line_info(chip, 0);
+	info1 = gpiod_chip_get_line_info(chip, 1);
+	info2 = gpiod_chip_get_line_info(chip, 2);
+
+	g_assert_cmpint(gpiod_line_info_get_drive(info0), ==,
+			GPIOD_LINE_DRIVE_PUSH_PULL);
+	g_assert_cmpint(gpiod_line_info_get_drive(info1), ==,
+			GPIOD_LINE_DRIVE_OPEN_DRAIN);
+	g_assert_cmpint(gpiod_line_info_get_drive(info2), ==,
+			GPIOD_LINE_DRIVE_OPEN_SOURCE);
+}
+
+GPIOD_TEST_CASE(debounce_period)
+{
+	static const guint offset = 5;
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	g_autoptr(struct_gpiod_line_info) info = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 1, &offset);
+	gpiod_line_config_set_edge_detection_default(line_cfg,
+						     GPIOD_LINE_EDGE_BOTH);
+	gpiod_line_config_set_debounce_period_us_default(line_cfg, 1000);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+	info = gpiod_chip_get_line_info(chip, 5);
+
+	g_assert_cmpuint(gpiod_line_info_get_debounce_period_us(info),
+			 ==, 1000);
+}
+
+GPIOD_TEST_CASE(event_clock)
+{
+	static const guint offsets[] = { 0, 1 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	g_autoptr(struct_gpiod_line_info) info0 = NULL;
+	g_autoptr(struct_gpiod_line_info) info1 = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 2, offsets);
+	gpiod_line_config_set_event_clock_override(line_cfg,
+					GPIOD_LINE_EVENT_CLOCK_REALTIME, 1);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+	info0 = gpiod_chip_get_line_info(chip, 0);
+	info1 = gpiod_chip_get_line_info(chip, 1);
+
+	g_assert_cmpint(gpiod_line_info_get_event_clock(info0), ==,
+			GPIOD_LINE_EVENT_CLOCK_MONOTONIC);
+	g_assert_cmpint(gpiod_line_info_get_event_clock(info1), ==,
+			GPIOD_LINE_EVENT_CLOCK_REALTIME);
+}
diff --git a/tests/tests-line-request.c b/tests/tests-line-request.c
new file mode 100644
index 0000000..8ef391d
--- /dev/null
+++ b/tests/tests-line-request.c
@@ -0,0 +1,526 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#include <glib.h>
+#include <gpiod.h>
+
+#include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+#include "gpiod-test-sim.h"
+
+#define GPIOD_TEST_GROUP "line-request"
+
+GPIOD_TEST_CASE(request_fails_with_no_offsets)
+{
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 4, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	g_assert_cmpuint(gpiod_request_config_get_num_offsets(req_cfg), ==, 0);
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+
+	request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+	g_assert_null(request);
+	gpiod_test_expect_errno(EINVAL);
+}
+
+GPIOD_TEST_CASE(request_fails_with_duplicate_offsets)
+{
+	static const guint offsets[] = { 0, 2, 2, 3 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 4, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 4, offsets);
+
+	request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+	g_assert_null(request);
+	gpiod_test_expect_errno(EBUSY);
+}
+
+GPIOD_TEST_CASE(request_fails_with_offset_out_of_bounds)
+{
+	static const guint offsets[] = { 2, 6 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 4, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 2, offsets);
+
+	request = gpiod_chip_request_lines(chip, req_cfg, line_cfg);
+	g_assert_null(request);
+	gpiod_test_expect_errno(EINVAL);
+}
+
+GPIOD_TEST_CASE(set_consumer)
+{
+	static const guint offset = 2;
+	static const gchar *const consumer = "foobar";
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 4, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	g_autoptr(struct_gpiod_line_info) info = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 1, &offset);
+	gpiod_request_config_set_consumer(req_cfg, consumer);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	info = gpiod_test_get_line_info_or_fail(chip, offset);
+
+	g_assert_cmpstr(gpiod_line_info_get_consumer(info), ==, consumer);
+}
+
+GPIOD_TEST_CASE(empty_consumer)
+{
+	static const guint offset = 2;
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 4, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	g_autoptr(struct_gpiod_line_info) info = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 1, &offset);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	info = gpiod_test_get_line_info_or_fail(chip, offset);
+
+	g_assert_cmpstr(gpiod_line_info_get_consumer(info), ==, "?");
+}
+
+GPIOD_TEST_CASE(default_output_value)
+{
+	/*
+	 * Have a hole in offsets on purpose - make sure it's not set by
+	 * accident.
+	 */
+	static const guint offsets[] = { 0, 1, 3, 4 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	guint i;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 4, offsets);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_OUTPUT);
+	gpiod_line_config_set_output_value_default(line_cfg,
+						   GPIOD_LINE_VALUE_ACTIVE);
+
+	g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_DOWN);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	for (i = 0; i < 4; i++)
+		g_assert_cmpint(g_gpiosim_chip_get_value(sim, offsets[i]),
+				==, GPIOD_LINE_VALUE_ACTIVE);
+
+	g_assert_cmpint(g_gpiosim_chip_get_value(sim, 2), ==,
+			GPIOD_LINE_VALUE_INACTIVE);
+}
+
+GPIOD_TEST_CASE(default_and_overridden_output_value)
+{
+	static const guint offsets[] = { 0, 1, 2, 3 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 4, offsets);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_OUTPUT);
+	gpiod_line_config_set_output_value_default(line_cfg, 1);
+	gpiod_line_config_set_output_value_override(line_cfg, 2, 0);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	g_assert_cmpint(g_gpiosim_chip_get_value(sim, offsets[0]),
+			==, GPIOD_LINE_VALUE_ACTIVE);
+	g_assert_cmpint(g_gpiosim_chip_get_value(sim, offsets[1]),
+			==, GPIOD_LINE_VALUE_ACTIVE);
+	g_assert_cmpint(g_gpiosim_chip_get_value(sim, offsets[2]),
+			==, GPIOD_LINE_VALUE_INACTIVE);
+	g_assert_cmpint(g_gpiosim_chip_get_value(sim, offsets[3]),
+			==, GPIOD_LINE_VALUE_ACTIVE);
+}
+
+GPIOD_TEST_CASE(read_all_values)
+{
+	static const guint offsets[] = { 0, 2, 4, 5, 7 };
+	static const gint pulls[] = { 0, 1, 0, 1, 1 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	gint ret, values[5];
+	guint i;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 5, offsets);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_INPUT);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	for (i = 0; i < 5; i++)
+		g_gpiosim_chip_set_pull(sim, offsets[i],
+			pulls[i] ? G_GPIOSIM_PULL_UP : G_GPIOSIM_PULL_DOWN);
+
+	ret = gpiod_line_request_get_values(request, values);
+	g_assert_cmpint(ret, ==, 0);
+	gpiod_test_return_if_failed();
+
+	for (i = 0; i < 5; i++)
+		g_assert_cmpint(values[i], ==, pulls[i]);
+}
+
+GPIOD_TEST_CASE(request_multiple_values_but_read_one)
+{
+	static const guint offsets[] = { 0, 2, 4, 5, 7 };
+	static const gint pulls[] = { 0, 1, 0, 1, 1 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	gint ret;
+	guint i;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 5, offsets);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_INPUT);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	for (i = 0; i < 5; i++)
+		g_gpiosim_chip_set_pull(sim, offsets[i],
+			pulls[i] ? G_GPIOSIM_PULL_UP : G_GPIOSIM_PULL_DOWN);
+
+	ret = gpiod_line_request_get_value(request, 5);
+	g_assert_cmpint(ret, ==, 1);
+}
+
+GPIOD_TEST_CASE(set_all_values)
+{
+	static const guint offsets[] = { 0, 2, 4, 5, 6 };
+	static const gint values[] = { GPIOD_LINE_VALUE_ACTIVE,
+				       GPIOD_LINE_VALUE_INACTIVE,
+				       GPIOD_LINE_VALUE_ACTIVE,
+				       GPIOD_LINE_VALUE_ACTIVE,
+				       GPIOD_LINE_VALUE_ACTIVE };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	gint ret;
+	guint i;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 5, offsets);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_OUTPUT);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	ret = gpiod_line_request_set_values(request, values);
+	g_assert_cmpint(ret, ==, 0);
+	gpiod_test_return_if_failed();
+
+	for (i = 0; i < 5; i++)
+		g_assert_cmpint(g_gpiosim_chip_get_value(sim, offsets[i]),
+				==, values[i]);
+}
+
+GPIOD_TEST_CASE(request_survives_parent_chip)
+{
+	static const guint offset = 0;
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 4, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	gint ret;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 1, &offset);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_OUTPUT);
+	gpiod_line_config_set_output_value_default(line_cfg,
+						   GPIOD_LINE_VALUE_ACTIVE);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	g_assert_cmpint(gpiod_line_request_get_value(request, offset), ==,
+			GPIOD_LINE_VALUE_ACTIVE);
+
+	gpiod_chip_close(chip);
+	chip = NULL;
+
+	ret = gpiod_line_request_set_value(request, offset,
+					   GPIOD_LINE_VALUE_ACTIVE);
+	g_assert_cmpint(ret, ==, 0);
+	gpiod_test_return_if_failed();
+
+	ret = gpiod_line_request_set_value(request, offset,
+					   GPIOD_LINE_VALUE_ACTIVE);
+	g_assert_cmpint(ret, ==, 0);
+	gpiod_test_return_if_failed();
+}
+
+GPIOD_TEST_CASE(request_with_overridden_direction)
+{
+	static const guint offsets[] = { 0, 1, 2, 3 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 4, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	g_autoptr(struct_gpiod_line_info) info0 = NULL;
+	g_autoptr(struct_gpiod_line_info) info1 = NULL;
+	g_autoptr(struct_gpiod_line_info) info2 = NULL;
+	g_autoptr(struct_gpiod_line_info) info3 = NULL;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 4, offsets);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_OUTPUT);
+	gpiod_line_config_set_direction_override(line_cfg,
+						 GPIOD_LINE_DIRECTION_INPUT, 3);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+	info0 = gpiod_test_get_line_info_or_fail(chip, 0);
+	info1 = gpiod_test_get_line_info_or_fail(chip, 1);
+	info2 = gpiod_test_get_line_info_or_fail(chip, 2);
+	info3 = gpiod_test_get_line_info_or_fail(chip, 3);
+
+	g_assert_cmpint(gpiod_line_info_get_direction(info0), ==,
+			GPIOD_LINE_DIRECTION_OUTPUT);
+	g_assert_cmpint(gpiod_line_info_get_direction(info1), ==,
+			GPIOD_LINE_DIRECTION_OUTPUT);
+	g_assert_cmpint(gpiod_line_info_get_direction(info2), ==,
+			GPIOD_LINE_DIRECTION_OUTPUT);
+	g_assert_cmpint(gpiod_line_info_get_direction(info3), ==,
+			GPIOD_LINE_DIRECTION_INPUT);
+}
+
+GPIOD_TEST_CASE(num_lines)
+{
+	static const guint offsets[] = { 0, 1, 2, 3, 7, 8, 11, 14 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 16, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	guint read_back[8], i;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 8, offsets);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	g_assert_cmpuint(gpiod_line_request_get_num_lines(request), ==, 8);
+	gpiod_test_return_if_failed();
+	gpiod_line_request_get_offsets(request, read_back);
+	for (i = 0; i < 8; i++)
+		g_assert_cmpuint(read_back[i], ==, offsets[i]);
+}
+
+GPIOD_TEST_CASE(active_low_read_value)
+{
+	static const guint offsets[] = { 2, 3 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	gint value;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 2, offsets);
+	gpiod_line_config_set_direction_override(line_cfg,
+					GPIOD_LINE_DIRECTION_INPUT, 2);
+	gpiod_line_config_set_direction_override(line_cfg,
+					GPIOD_LINE_DIRECTION_OUTPUT, 3);
+	gpiod_line_config_set_active_low_default(line_cfg, true);
+	gpiod_line_config_set_output_value_default(line_cfg,
+						   GPIOD_LINE_VALUE_ACTIVE);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	g_gpiosim_chip_set_pull(sim, 2, G_GPIOSIM_PULL_DOWN);
+	value = gpiod_line_request_get_value(request, 2);
+	g_assert_cmpint(value, ==, GPIOD_LINE_VALUE_ACTIVE);
+
+	g_assert_cmpint(g_gpiosim_chip_get_value(sim, 3), ==, 0);
+}
+
+GPIOD_TEST_CASE(reconfigure_lines)
+{
+	static const guint offsets[] = { 0, 1, 2, 3 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 4, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	gint values[4], ret;
+	guint i;
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 4, offsets);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_OUTPUT);
+
+	values[0] = 1;
+	values[1] = 0;
+	values[2] = 1;
+	values[3] = 0;
+	gpiod_line_config_set_output_values(line_cfg, 4, offsets, values);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	for (i = 0; i < 4; i++)
+		g_assert_cmpint(g_gpiosim_chip_get_value(sim, offsets[i]),
+				==, values[i]);
+
+	values[0] = 0;
+	values[1] = 1;
+	values[2] = 0;
+	values[3] = 1;
+	gpiod_line_config_set_output_values(line_cfg, 4, offsets, values);
+
+	ret = gpiod_line_request_reconfigure_lines(request, line_cfg);
+	g_assert_cmpint(ret, ==, 0);
+	gpiod_test_return_if_failed();
+
+	for (i = 0; i < 4; i++)
+		g_assert_cmpint(g_gpiosim_chip_get_value(sim, offsets[i]),
+				==, values[i]);
+}
+
+GPIOD_TEST_CASE(request_lines_with_unordered_offsets)
+{
+	static const guint offsets[] = { 5, 1, 7, 2, 0, 6 };
+
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new("num-lines", 8, NULL);
+	g_autoptr(struct_gpiod_chip) chip = NULL;
+	g_autoptr(struct_gpiod_request_config) req_cfg = NULL;
+	g_autoptr(struct_gpiod_line_config) line_cfg = NULL;
+	g_autoptr(struct_gpiod_line_request) request = NULL;
+	guint cfg_offsets[4];
+	gint values[4];
+
+	chip = gpiod_test_open_chip_or_fail(g_gpiosim_chip_get_dev_path(sim));
+	req_cfg = gpiod_test_create_request_config_or_fail();
+	line_cfg = gpiod_test_create_line_config_or_fail();
+
+	gpiod_request_config_set_offsets(req_cfg, 6, offsets);
+	gpiod_line_config_set_direction_default(line_cfg,
+						GPIOD_LINE_DIRECTION_OUTPUT);
+	gpiod_line_config_set_output_value_default(line_cfg, 1);
+
+	values[0] = 0;
+	values[1] = 1;
+	values[2] = 0;
+	values[3] = 0;
+	cfg_offsets[0] = 7;
+	cfg_offsets[1] = 1;
+	cfg_offsets[2] = 6;
+	cfg_offsets[3] = 0;
+	gpiod_line_config_set_output_values(line_cfg, 4, cfg_offsets, values);
+
+	request = gpiod_test_request_lines_or_fail(chip, req_cfg, line_cfg);
+
+	g_assert_cmpint(g_gpiosim_chip_get_value(sim, 0), ==,
+			GPIOD_LINE_VALUE_INACTIVE);
+	g_assert_cmpint(g_gpiosim_chip_get_value(sim, 1), ==,
+			GPIOD_LINE_VALUE_ACTIVE);
+	g_assert_cmpint(g_gpiosim_chip_get_value(sim, 2), ==,
+			GPIOD_LINE_VALUE_ACTIVE);
+	g_assert_cmpint(g_gpiosim_chip_get_value(sim, 5), ==,
+			GPIOD_LINE_VALUE_ACTIVE);
+	g_assert_cmpint(g_gpiosim_chip_get_value(sim, 6), ==,
+			GPIOD_LINE_VALUE_INACTIVE);
+	g_assert_cmpint(g_gpiosim_chip_get_value(sim, 7), ==,
+			GPIOD_LINE_VALUE_INACTIVE);
+}
diff --git a/tests/tests-line.c b/tests/tests-line.c
deleted file mode 100644
index 3985990..0000000
--- a/tests/tests-line.c
+++ /dev/null
@@ -1,1091 +0,0 @@
-// SPDX-License-Identifier: GPL-2.0-or-later
-// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
-
-#include <errno.h>
-#include <string.h>
-
-#include "gpiod-test.h"
-
-#define GPIOD_TEST_GROUP "line"
-
-GPIOD_TEST_CASE(request_output, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line0;
-	struct gpiod_line *line1;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line0 = gpiod_chip_get_line(chip, 2);
-	line1 = gpiod_chip_get_line(chip, 5);
-	g_assert_nonnull(line0);
-	g_assert_nonnull(line1);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_output(line0, GPIOD_TEST_CONSUMER, 0);
-	g_assert_cmpint(ret, ==, 0);
-	ret = gpiod_line_request_output(line1, GPIOD_TEST_CONSUMER, 1);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 5), ==, 1);
-}
-
-GPIOD_TEST_CASE(request_already_requested, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 0);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_input(line, GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_input(line, GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, -1);
-	g_assert_cmpint(errno, ==, EBUSY);
-}
-
-GPIOD_TEST_CASE(consumer, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 0);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	g_assert_null(gpiod_line_consumer(line));
-
-	ret = gpiod_line_request_input(line, GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpstr(gpiod_line_consumer(line), ==, GPIOD_TEST_CONSUMER);
-}
-
-GPIOD_TEST_CASE(consumer_long_string, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 0);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	g_assert_null(gpiod_line_consumer(line));
-
-	ret = gpiod_line_request_input(line,
-			"consumer string over 32 characters long");
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-	g_assert_cmpstr(gpiod_line_consumer(line), ==,
-			"consumer string over 32 charact");
-	g_assert_cmpuint(strlen(gpiod_line_consumer(line)), ==, 31);
-}
-
-GPIOD_TEST_CASE(request_bulk_output, 0, { 8, 8 })
-{
-	g_autoptr(gpiod_line_bulk_struct) bulkA = NULL;
-	g_autoptr(gpiod_line_bulk_struct) bulkB = NULL;
-	g_autoptr(gpiod_chip_struct) chipA = NULL;
-	g_autoptr(gpiod_chip_struct) chipB = NULL;
-	struct gpiod_line *lineA0, *lineA1, *lineA2, *lineA3,
-			  *lineB0, *lineB1, *lineB2, *lineB3;
-	gint valA[4], valB[4], ret;
-
-	chipA = gpiod_chip_open(gpiod_test_chip_path(0));
-	chipB = gpiod_chip_open(gpiod_test_chip_path(1));
-	g_assert_nonnull(chipA);
-	g_assert_nonnull(chipB);
-	gpiod_test_return_if_failed();
-
-	lineA0 = gpiod_chip_get_line(chipA, 0);
-	lineA1 = gpiod_chip_get_line(chipA, 1);
-	lineA2 = gpiod_chip_get_line(chipA, 2);
-	lineA3 = gpiod_chip_get_line(chipA, 3);
-	lineB0 = gpiod_chip_get_line(chipB, 0);
-	lineB1 = gpiod_chip_get_line(chipB, 1);
-	lineB2 = gpiod_chip_get_line(chipB, 2);
-	lineB3 = gpiod_chip_get_line(chipB, 3);
-
-	g_assert_nonnull(lineA0);
-	g_assert_nonnull(lineA1);
-	g_assert_nonnull(lineA2);
-	g_assert_nonnull(lineA3);
-	g_assert_nonnull(lineB0);
-	g_assert_nonnull(lineB1);
-	g_assert_nonnull(lineB2);
-	g_assert_nonnull(lineB3);
-	gpiod_test_return_if_failed();
-
-	bulkA = gpiod_line_bulk_new(4);
-	bulkB = gpiod_line_bulk_new(4);
-	g_assert_nonnull(bulkA);
-	g_assert_nonnull(bulkB);
-	gpiod_test_return_if_failed();
-
-	gpiod_line_bulk_add_line(bulkA, lineA0);
-	gpiod_line_bulk_add_line(bulkA, lineA1);
-	gpiod_line_bulk_add_line(bulkA, lineA2);
-	gpiod_line_bulk_add_line(bulkA, lineA3);
-	gpiod_line_bulk_add_line(bulkB, lineB0);
-	gpiod_line_bulk_add_line(bulkB, lineB1);
-	gpiod_line_bulk_add_line(bulkB, lineB2);
-	gpiod_line_bulk_add_line(bulkB, lineB3);
-
-	valA[0] = 1;
-	valA[1] = 0;
-	valA[2] = 0;
-	valA[3] = 1;
-	ret = gpiod_line_request_bulk_output(bulkA,
-					     GPIOD_TEST_CONSUMER, valA);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	valB[0] = 0;
-	valB[1] = 1;
-	valB[2] = 0;
-	valB[3] = 1;
-	ret = gpiod_line_request_bulk_output(bulkB,
-					     GPIOD_TEST_CONSUMER, valB);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 0), ==, 1);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 1), ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 3), ==, 1);
-
-	g_assert_cmpint(gpiod_test_chip_get_value(1, 0), ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(1, 1), ==, 1);
-	g_assert_cmpint(gpiod_test_chip_get_value(1, 2), ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(1, 3), ==, 1);
-}
-
-GPIOD_TEST_CASE(request_null_default_vals_for_output, 0, { 8 })
-{
-	g_autoptr(gpiod_line_bulk_struct) bulk = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line0, *line1, *line2;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line0 = gpiod_chip_get_line(chip, 0);
-	line1 = gpiod_chip_get_line(chip, 1);
-	line2 = gpiod_chip_get_line(chip, 2);
-
-	g_assert_nonnull(line0);
-	g_assert_nonnull(line1);
-	g_assert_nonnull(line2);
-	gpiod_test_return_if_failed();
-
-	bulk = gpiod_line_bulk_new(3);
-	g_assert_nonnull(bulk);
-	gpiod_test_return_if_failed();
-
-	gpiod_line_bulk_add_line(bulk, line0);
-	gpiod_line_bulk_add_line(bulk, line1);
-	gpiod_line_bulk_add_line(bulk, line2);
-
-	ret = gpiod_line_request_bulk_output(bulk, GPIOD_TEST_CONSUMER, NULL);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 0), ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 1), ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-}
-
-GPIOD_TEST_CASE(set_value, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 2);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_output(line, GPIOD_TEST_CONSUMER, 0);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-
-	ret = gpiod_line_set_value(line, 1);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 1);
-	ret = gpiod_line_set_value(line, 0);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-}
-
-GPIOD_TEST_CASE(set_value_bulk, 0, { 8 })
-{
-	g_autoptr(gpiod_line_bulk_struct) bulk = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line0, *line1, *line2;
-	int values[3];
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line0 = gpiod_chip_get_line(chip, 0);
-	line1 = gpiod_chip_get_line(chip, 1);
-	line2 = gpiod_chip_get_line(chip, 2);
-
-	g_assert_nonnull(line0);
-	g_assert_nonnull(line1);
-	g_assert_nonnull(line2);
-	gpiod_test_return_if_failed();
-
-	bulk = gpiod_line_bulk_new(3);
-	g_assert_nonnull(bulk);
-	gpiod_test_return_if_failed();
-
-	gpiod_line_bulk_add_line(bulk, line0);
-	gpiod_line_bulk_add_line(bulk, line1);
-	gpiod_line_bulk_add_line(bulk, line2);
-
-	values[0] = 0;
-	values[1] = 1;
-	values[2] = 2;
-
-	ret = gpiod_line_request_bulk_output(bulk,
-			GPIOD_TEST_CONSUMER, values);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 0), ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 1), ==, 1);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 1);
-
-	values[0] = 2;
-	values[1] = 1;
-	values[2] = 0;
-
-	ret = gpiod_line_set_value_bulk(bulk, values);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 0), ==, 1);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 1), ==, 1);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-
-	ret = gpiod_line_set_value_bulk(bulk, NULL);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 0), ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 1), ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-}
-
-GPIOD_TEST_CASE(set_config_bulk_null_values, 0, { 8 })
-{
-	g_autoptr(gpiod_line_bulk_struct) bulk = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line0, *line1, *line2;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line0 = gpiod_chip_get_line(chip, 0);
-	line1 = gpiod_chip_get_line(chip, 1);
-	line2 = gpiod_chip_get_line(chip, 2);
-
-	g_assert_nonnull(line0);
-	g_assert_nonnull(line1);
-	g_assert_nonnull(line2);
-	gpiod_test_return_if_failed();
-
-	bulk = gpiod_line_bulk_new(3);
-	g_assert_nonnull(bulk);
-	gpiod_test_return_if_failed();
-
-	gpiod_line_bulk_add_line(bulk, line0);
-	gpiod_line_bulk_add_line(bulk, line1);
-	gpiod_line_bulk_add_line(bulk, line2);
-
-	ret = gpiod_line_request_bulk_output(bulk, GPIOD_TEST_CONSUMER, 0);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-	g_assert_false(gpiod_line_is_active_low(line0));
-	g_assert_false(gpiod_line_is_active_low(line1));
-	g_assert_false(gpiod_line_is_active_low(line2));
-
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 0), ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 1), ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-
-	ret = gpiod_line_set_config_bulk(bulk,
-			GPIOD_LINE_REQUEST_DIRECTION_OUTPUT,
-			GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW, NULL);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_true(gpiod_line_is_active_low(line0));
-	g_assert_true(gpiod_line_is_active_low(line1));
-	g_assert_true(gpiod_line_is_active_low(line2));
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 0), ==, 1);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 1), ==, 1);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 1);
-
-	ret = gpiod_line_set_config_bulk(bulk,
-			GPIOD_LINE_REQUEST_DIRECTION_OUTPUT, 0, NULL);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_false(gpiod_line_is_active_low(line0));
-	g_assert_false(gpiod_line_is_active_low(line1));
-	g_assert_false(gpiod_line_is_active_low(line2));
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 0), ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 1), ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-}
-
-GPIOD_TEST_CASE(set_flags_active_state, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 2);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_output(line, GPIOD_TEST_CONSUMER, 1);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-	g_assert_false(gpiod_line_is_active_low(line));
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 1);
-
-	ret = gpiod_line_set_flags(line, GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_true(gpiod_line_is_active_low(line));
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-
-	ret = gpiod_line_set_flags(line, 0);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_false(gpiod_line_is_active_low(line));
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 1);
-}
-
-GPIOD_TEST_CASE(set_flags_bias, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 2);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_input(line, GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-	g_assert_cmpint(gpiod_line_bias(line), ==, GPIOD_LINE_BIAS_UNKNOWN);
-
-	ret = gpiod_line_set_flags(line,
-		GPIOD_LINE_REQUEST_FLAG_BIAS_DISABLED);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_line_bias(line), ==, GPIOD_LINE_BIAS_DISABLED);
-
-	ret = gpiod_line_set_flags(line,
-		GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_line_bias(line), ==, GPIOD_LINE_BIAS_PULL_UP);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 1);
-
-	ret = gpiod_line_set_flags(line,
-		GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_DOWN);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_line_bias(line), ==, GPIOD_LINE_BIAS_PULL_DOWN);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-}
-
-GPIOD_TEST_CASE(set_flags_drive, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 2);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_output(line, GPIOD_TEST_CONSUMER, 0);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-	g_assert_cmpint(gpiod_line_drive(line), ==, GPIOD_LINE_DRIVE_PUSH_PULL);
-
-	ret = gpiod_line_set_flags(line,
-		GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_line_drive(line), ==,
-			GPIOD_LINE_DRIVE_OPEN_DRAIN);
-
-	ret = gpiod_line_set_flags(line,
-		GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_line_drive(line), ==,
-			GPIOD_LINE_DRIVE_OPEN_SOURCE);
-}
-
-GPIOD_TEST_CASE(set_direction, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 2);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_output(line, GPIOD_TEST_CONSUMER, 0);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-	g_assert_cmpint(gpiod_line_direction(line), ==,
-			GPIOD_LINE_DIRECTION_OUTPUT);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-
-	ret = gpiod_line_set_direction_input(line);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_line_direction(line), ==,
-			GPIOD_LINE_DIRECTION_INPUT);
-
-	ret = gpiod_line_set_direction_output(line, 1);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_line_direction(line), ==,
-			GPIOD_LINE_DIRECTION_OUTPUT);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 1);
-}
-
-GPIOD_TEST_CASE(set_direction_bulk, 0, { 8 })
-{
-	g_autoptr(gpiod_line_bulk_struct) bulk = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line0, *line1, *line2;
-	int values[3];
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line0 = gpiod_chip_get_line(chip, 0);
-	line1 = gpiod_chip_get_line(chip, 1);
-	line2 = gpiod_chip_get_line(chip, 2);
-
-	g_assert_nonnull(line0);
-	g_assert_nonnull(line1);
-	g_assert_nonnull(line2);
-	gpiod_test_return_if_failed();
-
-	bulk = gpiod_line_bulk_new(3);
-	g_assert_nonnull(bulk);
-	gpiod_test_return_if_failed();
-
-	gpiod_line_bulk_add_line(bulk, line0);
-	gpiod_line_bulk_add_line(bulk, line1);
-	gpiod_line_bulk_add_line(bulk, line2);
-
-	values[0] = 0;
-	values[1] = 1;
-	values[2] = 2;
-
-	ret = gpiod_line_request_bulk_output(bulk,
-			GPIOD_TEST_CONSUMER, values);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-	g_assert_cmpint(gpiod_line_direction(line0), ==,
-			GPIOD_LINE_DIRECTION_OUTPUT);
-	g_assert_cmpint(gpiod_line_direction(line1), ==,
-			GPIOD_LINE_DIRECTION_OUTPUT);
-	g_assert_cmpint(gpiod_line_direction(line2), ==,
-			GPIOD_LINE_DIRECTION_OUTPUT);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 0), ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 1), ==, 1);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 1);
-
-	ret = gpiod_line_set_direction_input_bulk(bulk);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_line_direction(line0), ==,
-			GPIOD_LINE_DIRECTION_INPUT);
-	g_assert_cmpint(gpiod_line_direction(line1), ==,
-			GPIOD_LINE_DIRECTION_INPUT);
-	g_assert_cmpint(gpiod_line_direction(line2), ==,
-			GPIOD_LINE_DIRECTION_INPUT);
-
-	values[0] = 2;
-	values[1] = 1;
-	values[2] = 0;
-
-	ret = gpiod_line_set_direction_output_bulk(bulk, values);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_line_direction(line0), ==,
-			GPIOD_LINE_DIRECTION_OUTPUT);
-	g_assert_cmpint(gpiod_line_direction(line1), ==,
-			GPIOD_LINE_DIRECTION_OUTPUT);
-	g_assert_cmpint(gpiod_line_direction(line2), ==,
-			GPIOD_LINE_DIRECTION_OUTPUT);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 0), ==, 1);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 1), ==, 1);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-
-	ret = gpiod_line_set_direction_output_bulk(bulk, NULL);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_line_direction(line0), ==,
-			GPIOD_LINE_DIRECTION_OUTPUT);
-	g_assert_cmpint(gpiod_line_direction(line1), ==,
-			GPIOD_LINE_DIRECTION_OUTPUT);
-	g_assert_cmpint(gpiod_line_direction(line2), ==,
-			GPIOD_LINE_DIRECTION_OUTPUT);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 0), ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 1), ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-}
-
-GPIOD_TEST_CASE(output_value_caching, 0, { 8 })
-{
-	g_autoptr(gpiod_line_bulk_struct) bulk = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 2);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	/* check cached by request... */
-	ret = gpiod_line_request_output(line, GPIOD_TEST_CONSUMER, 1);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 1);
-
-	/* ...by checking cached value applied by set_flags */
-	ret = gpiod_line_set_flags(line, 0);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 1);
-
-	/* check cached by set_value */
-	ret = gpiod_line_set_value(line, 0);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-
-	ret = gpiod_line_set_flags(line, 0);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-
-	ret = gpiod_line_set_value(line, 1);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 1);
-
-	ret = gpiod_line_set_flags(line, 0);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 1);
-
-	/* check cached by set_config */
-	ret = gpiod_line_set_config(line, GPIOD_LINE_REQUEST_DIRECTION_OUTPUT,
-				    0, 0);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-
-	ret = gpiod_line_set_flags(line, 0);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-
-	/* check cached by set_value_bulk default */
-	ret = gpiod_line_set_value(line, 1);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 1);
-
-	bulk = gpiod_line_bulk_new(1);
-	g_assert_nonnull(bulk);
-	gpiod_test_return_if_failed();
-
-	gpiod_line_bulk_add_line(bulk, line);
-	ret = gpiod_line_set_value_bulk(bulk, NULL);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-
-	ret = gpiod_line_set_flags(line, 0);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 2), ==, 0);
-}
-
-GPIOD_TEST_CASE(direction, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 5);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_output(line, GPIOD_TEST_CONSUMER, 1);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-	g_assert_cmpint(gpiod_line_direction(line), ==,
-			GPIOD_LINE_DIRECTION_OUTPUT);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 5), ==, 1);
-
-	gpiod_line_release(line);
-
-	ret = gpiod_line_request_input(line, GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpint(gpiod_line_direction(line), ==,
-			GPIOD_LINE_DIRECTION_INPUT);
-}
-
-GPIOD_TEST_CASE(active_state, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 5);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_input(line, GPIOD_TEST_CONSUMER);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	g_assert_false(gpiod_line_is_active_low(line));
-
-	gpiod_line_release(line);
-
-	ret = gpiod_line_request_input_flags(line, GPIOD_TEST_CONSUMER,
-					GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	g_assert_cmpint(gpiod_line_direction(line), ==,
-			GPIOD_LINE_DIRECTION_INPUT);
-
-	gpiod_line_release(line);
-
-	ret = gpiod_line_request_output_flags(line, GPIOD_TEST_CONSUMER,
-			GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW, 0);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	g_assert_cmpint(gpiod_line_direction(line), ==,
-			GPIOD_LINE_DIRECTION_OUTPUT);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 5), ==, 1);
-
-	gpiod_line_release(line);
-
-	ret = gpiod_line_request_output_flags(line,
-			GPIOD_TEST_CONSUMER, 0, 0);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	g_assert_cmpint(gpiod_line_direction(line), ==,
-			GPIOD_LINE_DIRECTION_OUTPUT);
-	g_assert_cmpint(gpiod_test_chip_get_value(0, 5), ==, 0);
-
-}
-
-GPIOD_TEST_CASE(misc_flags, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line_request_config config;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 2);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	g_assert_false(gpiod_line_is_used(line));
-	g_assert_cmpint(gpiod_line_drive(line), ==, GPIOD_LINE_DRIVE_PUSH_PULL);
-	g_assert_cmpint(gpiod_line_bias(line), ==, GPIOD_LINE_BIAS_UNKNOWN);
-
-	config.request_type = GPIOD_LINE_REQUEST_DIRECTION_OUTPUT;
-	config.consumer = GPIOD_TEST_CONSUMER;
-	config.flags = GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN;
-
-	ret = gpiod_line_request(line, &config, 0);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	g_assert_true(gpiod_line_is_used(line));
-	g_assert_cmpint(gpiod_line_drive(line), ==,
-			GPIOD_LINE_DRIVE_OPEN_DRAIN);
-	g_assert_cmpint(gpiod_line_bias(line), ==, GPIOD_LINE_BIAS_UNKNOWN);
-	g_assert_cmpint(gpiod_line_direction(line), ==,
-			GPIOD_LINE_DIRECTION_OUTPUT);
-
-	gpiod_line_release(line);
-
-	config.flags = GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE;
-
-	ret = gpiod_line_request(line, &config, 0);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	g_assert_true(gpiod_line_is_used(line));
-	g_assert_cmpint(gpiod_line_drive(line), ==,
-			GPIOD_LINE_DRIVE_OPEN_SOURCE);
-	g_assert_cmpint(gpiod_line_bias(line), ==, GPIOD_LINE_BIAS_UNKNOWN);
-	g_assert_cmpint(gpiod_line_direction(line), ==,
-			GPIOD_LINE_DIRECTION_OUTPUT);
-
-	gpiod_line_release(line);
-}
-
-GPIOD_TEST_CASE(misc_flags_work_together, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line_request_config config;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 2);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	/*
-	 * Verify that open drain/source flags work together
-	 * with active_low.
-	 */
-
-	config.request_type = GPIOD_LINE_REQUEST_DIRECTION_OUTPUT;
-	config.consumer = GPIOD_TEST_CONSUMER;
-	config.flags = GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN |
-		       GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW;
-
-	ret = gpiod_line_request(line, &config, 0);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	g_assert_true(gpiod_line_is_used(line));
-	g_assert_cmpint(gpiod_line_drive(line), ==,
-			GPIOD_LINE_DRIVE_OPEN_DRAIN);
-	g_assert_cmpint(gpiod_line_bias(line), ==, GPIOD_LINE_BIAS_UNKNOWN);
-	g_assert_true(gpiod_line_is_active_low(line));
-	g_assert_cmpint(gpiod_line_direction(line), ==,
-			GPIOD_LINE_DIRECTION_OUTPUT);
-
-	gpiod_line_release(line);
-
-	config.flags = GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE |
-		       GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW;
-
-	ret = gpiod_line_request(line, &config, 0);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	g_assert_true(gpiod_line_is_used(line));
-	g_assert_cmpint(gpiod_line_drive(line), ==,
-			GPIOD_LINE_DRIVE_OPEN_SOURCE);
-	g_assert_cmpint(gpiod_line_bias(line), ==, GPIOD_LINE_BIAS_UNKNOWN);
-	g_assert_true(gpiod_line_is_active_low(line));
-
-	gpiod_line_release(line);
-
-	/*
-	 * Verify that pull-up/down flags work together
-	 * with active_low.
-	 */
-
-	config.request_type = GPIOD_LINE_REQUEST_DIRECTION_INPUT;
-	config.flags = GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_DOWN |
-		       GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW;
-
-	ret = gpiod_line_request(line, &config, 0);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	g_assert_true(gpiod_line_is_used(line));
-	g_assert_cmpint(gpiod_line_drive(line), ==, GPIOD_LINE_DRIVE_PUSH_PULL);
-	g_assert_cmpint(gpiod_line_bias(line), ==, GPIOD_LINE_BIAS_PULL_DOWN);
-	g_assert_true(gpiod_line_is_active_low(line));
-	g_assert_cmpint(gpiod_line_direction(line), ==,
-			GPIOD_LINE_DIRECTION_INPUT);
-
-	ret = gpiod_line_get_value(line);
-	g_assert_cmpint(ret, ==, 1);
-
-	gpiod_line_release(line);
-
-	config.flags = GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP |
-		       GPIOD_LINE_REQUEST_FLAG_ACTIVE_LOW;
-
-	ret = gpiod_line_request(line, &config, 0);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	g_assert_true(gpiod_line_is_used(line));
-	g_assert_cmpint(gpiod_line_drive(line), ==, GPIOD_LINE_DRIVE_PUSH_PULL);
-	g_assert_cmpint(gpiod_line_bias(line), ==, GPIOD_LINE_BIAS_PULL_UP);
-	g_assert_true(gpiod_line_is_active_low(line));
-	g_assert_cmpint(gpiod_line_direction(line), ==,
-			GPIOD_LINE_DIRECTION_INPUT);
-
-	ret = gpiod_line_get_value(line);
-	g_assert_cmpint(ret, ==, 0);
-
-	gpiod_line_release(line);
-}
-
-GPIOD_TEST_CASE(open_source_open_drain_input_mode, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 2);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_input_flags(line, GPIOD_TEST_CONSUMER,
-					GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN);
-	g_assert_cmpint(ret, ==, -1);
-	g_assert_cmpint(errno, ==, EINVAL);
-
-	ret = gpiod_line_request_input_flags(line, GPIOD_TEST_CONSUMER,
-					GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE);
-	g_assert_cmpint(ret, ==, -1);
-	g_assert_cmpint(errno, ==, EINVAL);
-}
-
-GPIOD_TEST_CASE(open_source_open_drain_simultaneously, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 2);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_output_flags(line, GPIOD_TEST_CONSUMER,
-					GPIOD_LINE_REQUEST_FLAG_OPEN_SOURCE |
-					GPIOD_LINE_REQUEST_FLAG_OPEN_DRAIN, 1);
-	g_assert_cmpint(ret, ==, -1);
-	g_assert_cmpint(errno, ==, EINVAL);
-}
-
-GPIOD_TEST_CASE(multiple_bias_flags, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 2);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	ret = gpiod_line_request_input_flags(line, GPIOD_TEST_CONSUMER,
-					GPIOD_LINE_REQUEST_FLAG_BIAS_DISABLED |
-					GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_DOWN);
-	g_assert_cmpint(ret, ==, -1);
-	g_assert_cmpint(errno, ==, EINVAL);
-
-	ret = gpiod_line_request_input_flags(line, GPIOD_TEST_CONSUMER,
-					GPIOD_LINE_REQUEST_FLAG_BIAS_DISABLED |
-					GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP);
-	g_assert_cmpint(ret, ==, -1);
-	g_assert_cmpint(errno, ==, EINVAL);
-
-	ret = gpiod_line_request_input_flags(line, GPIOD_TEST_CONSUMER,
-					GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_DOWN |
-					GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP);
-	g_assert_cmpint(ret, ==, -1);
-	g_assert_cmpint(errno, ==, EINVAL);
-
-	ret = gpiod_line_request_input_flags(line, GPIOD_TEST_CONSUMER,
-					GPIOD_LINE_REQUEST_FLAG_BIAS_DISABLED |
-					GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_DOWN |
-					GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP);
-	g_assert_cmpint(ret, ==, -1);
-	g_assert_cmpint(errno, ==, EINVAL);
-}
-
-
-/* Verify that the reference counting of the line fd handle works correctly. */
-GPIOD_TEST_CASE(release_one_use_another, 0, { 8 })
-{
-	g_autoptr(gpiod_line_bulk_struct) bulk = NULL;
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line *line0;
-	struct gpiod_line *line1;
-	gint ret, vals[2];
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line0 = gpiod_chip_get_line(chip, 2);
-	line1 = gpiod_chip_get_line(chip, 3);
-	g_assert_nonnull(line0);
-	g_assert_nonnull(line1);
-	gpiod_test_return_if_failed();
-
-	bulk = gpiod_line_bulk_new(2);
-	g_assert_nonnull(bulk);
-	gpiod_test_return_if_failed();
-
-	gpiod_line_bulk_add_line(bulk, line0);
-	gpiod_line_bulk_add_line(bulk, line1);
-
-	vals[0] = vals[1] = 1;
-
-	ret = gpiod_line_request_bulk_output(bulk, GPIOD_TEST_CONSUMER, vals);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-
-	gpiod_line_release(line0);
-
-	ret = gpiod_line_get_value(line0);
-	g_assert_cmpint(ret, ==, -1);
-	g_assert_cmpint(errno, ==, EPERM);
-}
-
-GPIOD_TEST_CASE(null_consumer, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line_request_config config;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 2);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	config.request_type = GPIOD_LINE_REQUEST_DIRECTION_INPUT;
-	config.consumer = NULL;
-	config.flags = 0;
-
-	ret = gpiod_line_request(line, &config, 0);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-	g_assert_cmpstr(gpiod_line_consumer(line), ==, "?");
-
-	gpiod_line_release(line);
-
-	/*
-	 * Internally we use different structures for event requests, so we
-	 * need to test that explicitly too.
-	 */
-	config.request_type = GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES;
-
-	ret = gpiod_line_request(line, &config, 0);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpstr(gpiod_line_consumer(line), ==, "?");
-}
-
-GPIOD_TEST_CASE(empty_consumer, 0, { 8 })
-{
-	g_autoptr(gpiod_chip_struct) chip = NULL;
-	struct gpiod_line_request_config config;
-	struct gpiod_line *line;
-	gint ret;
-
-	chip = gpiod_chip_open(gpiod_test_chip_path(0));
-	g_assert_nonnull(chip);
-	gpiod_test_return_if_failed();
-
-	line = gpiod_chip_get_line(chip, 2);
-	g_assert_nonnull(line);
-	gpiod_test_return_if_failed();
-
-	config.request_type = GPIOD_LINE_REQUEST_DIRECTION_INPUT;
-	config.consumer = "";
-	config.flags = 0;
-
-	ret = gpiod_line_request(line, &config, 0);
-	g_assert_cmpint(ret, ==, 0);
-	gpiod_test_return_if_failed();
-	g_assert_cmpstr(gpiod_line_consumer(line), ==, "?");
-
-	gpiod_line_release(line);
-
-	/*
-	 * Internally we use different structures for event requests, so we
-	 * need to test that explicitly too.
-	 */
-	config.request_type = GPIOD_LINE_REQUEST_EVENT_BOTH_EDGES;
-
-	ret = gpiod_line_request(line, &config, 0);
-	g_assert_cmpint(ret, ==, 0);
-	g_assert_cmpstr(gpiod_line_consumer(line), ==, "?");
-}
diff --git a/tests/tests-misc.c b/tests/tests-misc.c
index 051ab81..c473aff 100644
--- a/tests/tests-misc.c
+++ b/tests/tests-misc.c
@@ -1,25 +1,91 @@
 // SPDX-License-Identifier: GPL-2.0-or-later
 // SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
 
-#include <string.h>
+#include <errno.h>
+#include <glib.h>
+#include <gpiod.h>
+#include <unistd.h>
 
 #include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+#include "gpiod-test-sim.h"
 
 #define GPIOD_TEST_GROUP "misc"
 
-GPIOD_TEST_CASE(version_string, 0, { 1 })
+GPIOD_TEST_CASE(is_gpiochip_bad)
 {
+	g_assert_false(gpiod_is_gpiochip_device("/dev/null"));
+	g_assert_false(gpiod_is_gpiochip_device("/dev/nonexistent"));
+}
+
+GPIOD_TEST_CASE(is_gpiochip_good)
+{
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new(NULL);
+
+	g_assert_true(gpiod_is_gpiochip_device(
+			g_gpiosim_chip_get_dev_path(sim)));
+}
+
+GPIOD_TEST_CASE(is_gpiochip_link_bad)
+{
+	g_autofree gchar *link = NULL;
+	gint ret;
+
+	link = g_strdup_printf("/tmp/gpiod-test-link.%u", getpid());
+	ret = symlink("/dev/null", link);
+	g_assert_cmpint(ret, ==, 0);
+	gpiod_test_return_if_failed();
+
+	g_assert_false(gpiod_is_gpiochip_device(link));
+	ret = unlink(link);
+	g_assert_cmpint(ret, ==, 0);
+}
+
+GPIOD_TEST_CASE(is_gpiochip_link_good)
+{
+	g_autoptr(GPIOSimChip) sim = g_gpiosim_chip_new(NULL);
+	g_autofree gchar *link = NULL;
+	gint ret;
+
+	link = g_strdup_printf("/tmp/gpiod-test-link.%u", getpid());
+	ret = symlink(g_gpiosim_chip_get_dev_path(sim), link);
+	g_assert_cmpint(ret, ==, 0);
+	gpiod_test_return_if_failed();
+
+	g_assert_true(gpiod_is_gpiochip_device(link));
+	ret = unlink(link);
+	g_assert_cmpint(ret, ==, 0);
+}
+
+GPIOD_TEST_CASE(version_string)
+{
+	static const gchar *const pattern = "^[0-9][1-9]?\\.[0-9][1-9]?([\\.0-9]?|\\-devel)$";
+
+	g_autoptr(GError) err = NULL;
 	g_autoptr(GRegex) regex = NULL;
-	GError *err = NULL;
+	g_autoptr(GMatchInfo) match = NULL;
+	g_autofree gchar *res = NULL;
 	const gchar *ver;
+	gboolean ret;
 
 	ver = gpiod_version_string();
 	g_assert_nonnull(ver);
 	gpiod_test_return_if_failed();
-	g_assert_cmpuint(strlen(ver), >, 0);
 
-	regex = g_regex_new("^[0-9]+\\.[0-9]+[0-9a-zA-Z\\.-]*$", 0, 0, &err);
-	g_assert_null(err);
+	regex = g_regex_new(pattern, 0, 0, &err);
+	g_assert_nonnull(regex);
+	g_assert_no_error(err);
 	gpiod_test_return_if_failed();
-	g_assert_true(g_regex_match(regex, ver, 0, NULL));
+
+	ret = g_regex_match(regex, ver, 0, &match);
+	g_assert_true(ret);
+	gpiod_test_return_if_failed();
+
+	g_assert_true(g_match_info_matches(match));
+	res = g_match_info_fetch(match, 0);
+	g_assert_nonnull(res);
+	g_assert_cmpstr(res, ==, ver);
+	g_match_info_next(match, &err);
+	g_assert_no_error(err);
+	g_assert_false(g_match_info_matches(match));
 }
diff --git a/tests/tests-request-config.c b/tests/tests-request-config.c
new file mode 100644
index 0000000..becb414
--- /dev/null
+++ b/tests/tests-request-config.c
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#include <glib.h>
+#include <gpiod.h>
+
+#include "gpiod-test.h"
+#include "gpiod-test-helpers.h"
+
+#define GPIOD_TEST_GROUP "request-config"
+
+GPIOD_TEST_CASE(default_config)
+{
+	g_autoptr(struct_gpiod_request_config) config = NULL;
+
+	config = gpiod_test_create_request_config_or_fail();
+
+	g_assert_null(gpiod_request_config_get_consumer(config));
+	g_assert_cmpuint(gpiod_request_config_get_num_offsets(config), ==, 0);
+	g_assert_cmpuint(gpiod_request_config_get_event_buffer_size(config),
+			 ==, 0);
+}
+
+GPIOD_TEST_CASE(consumer)
+{
+	g_autoptr(struct_gpiod_request_config) config = NULL;
+
+	config = gpiod_test_create_request_config_or_fail();
+
+	gpiod_request_config_set_consumer(config, "foobar");
+	g_assert_cmpstr(gpiod_request_config_get_consumer(config),
+			==, "foobar");
+}
+
+GPIOD_TEST_CASE(offsets)
+{
+	static const guint offsets[] = { 0, 3, 4, 7 };
+
+	g_autoptr(struct_gpiod_request_config) config = NULL;
+	guint read_back[4], i;
+
+	config = gpiod_test_create_request_config_or_fail();
+
+	gpiod_request_config_set_offsets(config, 4, offsets);
+	g_assert_cmpuint(gpiod_request_config_get_num_offsets(config), ==, 4);
+	memset(read_back, 0, sizeof(read_back));
+	gpiod_request_config_get_offsets(config, read_back);
+	for (i = 0; i < 4; i++)
+		g_assert_cmpuint(read_back[i], ==, offsets[i]);
+}
+
+GPIOD_TEST_CASE(max_offsets)
+{
+	static const guint offsets_good[] = {
+		 0,  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
+	};
+
+	static const guint offsets_bad[] = {
+		 0,  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
+	};
+
+	g_autoptr(struct_gpiod_request_config) config = NULL;
+
+	config = gpiod_test_create_request_config_or_fail();
+
+	gpiod_request_config_set_offsets(config, 64, offsets_good);
+	g_assert_cmpuint(gpiod_request_config_get_num_offsets(config), ==, 64);
+
+	gpiod_request_config_set_offsets(config, 65, offsets_bad);
+	/* Should get truncated. */
+	g_assert_cmpuint(gpiod_request_config_get_num_offsets(config), ==, 64);
+}
+
+GPIOD_TEST_CASE(event_buffer_size)
+{
+	g_autoptr(struct_gpiod_request_config) config = NULL;
+
+	config = gpiod_test_create_request_config_or_fail();
+
+	gpiod_request_config_set_event_buffer_size(config, 128);
+	g_assert_cmpuint(gpiod_request_config_get_event_buffer_size(config),
+			 ==, 128);
+}
-- 
2.30.1


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* Re: [libgpiod v2][PATCH v3 1/3] API: add an enum for line values
  2022-03-03  9:18 ` [libgpiod v2][PATCH v3 1/3] API: add an enum for line values Bartosz Golaszewski
@ 2022-03-05  5:50   ` Kent Gibson
  0 siblings, 0 replies; 8+ messages in thread
From: Kent Gibson @ 2022-03-05  5:50 UTC (permalink / raw)
  To: Bartosz Golaszewski; +Cc: Linus Walleij, Andy Shevchenko, linux-gpio

On Thu, Mar 03, 2022 at 10:18:34AM +0100, Bartosz Golaszewski wrote:
> In order to explicitly stress that line values as understood by libgpiod
> are logical values, expose a two-value enum with values called: ACTIVE
> and INACTIVE that should be used whenever referring to the state of GPIO
> lines.
> 
> The value of INACTIVE is set to 0 while that of ACTIVE to 1 so that users
> can still use integers in C (where no scoped enums exist).
> 
> Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>

Works for me.

Reviewed-by: Kent Gibson <warthog618@gmail.com>


> ---
>  include/gpiod.h | 12 ++++++++++--
>  1 file changed, 10 insertions(+), 2 deletions(-)
> 
> diff --git a/include/gpiod.h b/include/gpiod.h
> index 074e395..34fdad6 100644
> --- a/include/gpiod.h
> +++ b/include/gpiod.h
> @@ -185,12 +185,20 @@ gpiod_chip_request_lines(struct gpiod_chip *chip,
>  /**
>   * @}
>   *
> - * @defgroup line_settings Line settings
> + * @defgroup line_settings Line definitions
>   * @{
>   *
> - * These defines are used both by gpiod_line_info and gpiod_line_config.
> + * These defines are used across the API.
>   */
>  
> +/**
> + * @brief Logical line state.
> + */
> +enum {
> +	GPIOD_LINE_VALUE_INACTIVE = 0,
> +	GPIOD_LINE_VALUE_ACTIVE = 1,
> +};
> +
>  /**
>   * @brief Direction settings.
>   */
> -- 
> 2.30.1
> 

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [libgpiod v2][PATCH v3 2/3] line-config: expose the override logic to users
  2022-03-03  9:18 ` [libgpiod v2][PATCH v3 2/3] line-config: expose the override logic to users Bartosz Golaszewski
@ 2022-03-05  5:51   ` Kent Gibson
  2022-03-05 20:57     ` Bartosz Golaszewski
  0 siblings, 1 reply; 8+ messages in thread
From: Kent Gibson @ 2022-03-05  5:51 UTC (permalink / raw)
  To: Bartosz Golaszewski; +Cc: Linus Walleij, Andy Shevchenko, linux-gpio

On Thu, Mar 03, 2022 at 10:18:35AM +0100, Bartosz Golaszewski wrote:
> We've already added getters for line-config but without exposing some
> parts of the internal logic of the object, the user can't really get
> the full picture and inspect the contents. This patch reworks the
> accessors further by providing access to the underlying override
> mechanism.
> 
> For every setting, we expose a getter and setter for the default value
> as well as a set of four functions for setting, getting, clearing and
> checking per-offset overrides.
> 
> An override can initially have the same value as the defaults but will
> retain the overridden value should the defaults change.
> 
> We also complete the API by providing functions that allow to retrieve
> the overridden offsets and their corresponding property types.
> 
> This way the caller can fully inspect the line_config and high-level
> language bindings can provide stringification methods.
> 
> While at it: we fix a couple bugs in the implementation of struct
> line_config and add new constructors that take a variable list of
> arguments.
> 

The variadic constructor is new for patch v3.
It bundles default constructor + default mutators, so doesn't add
functionality that wasn't already available - it just makes it
accessible via a single function call.
Is the variadic form beneficial for bindings, say Python, where you
would prefer not to be making a bunch of C calls?
Or is this just sugar?

No major objection, just curious about the rationale for adding it.

[snip]

> +/**
> + * @brief Get the total number of overridden settings currently stored by this
> + *        line config object.
> + * @param config Line config object.
> + * @return Number of individual overridden settings.
> + */
> +unsigned int
> +gpiod_line_config_get_num_overrides(struct gpiod_line_config *config);
> +
> +/**
> + * @brief Get the list of overridden offsets and the corresponding types of
> + *        overridden settings.
> + * @param config Line config object.
> + * @param offsets Array to store the overidden offsets. Must hold at least the
> + *                number of unsigned integers returned by
> + *                ::gpiod_line_config_get_output_value_offset.
> + * @param props Array to store the types of overridden settings. Must hold at
> + *              least the number of integers returned by
> + *              gpiod_line_config_get_output_value_offset.
> + */

The purpose of the offsets and props arrays is not clear.
Clarify that you are returning a list of (offset,prop), split across the
two arrays.
Replace them with a single array of (offset,prop) unless there is
a good reason to keep them separate?

Guessing it should be gpiod_line_config_get_num_overrides(), not
gpiod_line_config_get_output_value_offset() which returns 0 or 1, or
even better -1 for inputs?

[snip]

> +GPIOD_API unsigned int
> +gpiod_line_config_get_num_overrides(struct gpiod_line_config *config)
> +{
> +	struct override_config *override;
> +	unsigned int i, j, count = 0;
>  
> -	errno = ENXIO;
> +	for (i = 0; i < GPIO_V2_LINES_MAX; i++) {
> +		override = &config->overrides[i];
> +
> +		if (override_used(override)) {
> +			for (j = 0; j < NUM_OVERRIDE_FLAGS; j++) {
> +				if (override->override_flags &
> +				    override_flag_list[j])
> +					count++;
> +			}
> +		}
> +	}
> +
> +	return count;
> +}
> +

Using GPIO_V2_LINES_MAX for the size of the overrides array is
confusing, and the two should be de-coupled so you can more easily resize
the array if necessary.
Provide a NUM_OVERRIDES_MAX, or similar, and use that when referring
to the size of the overrides array.

> +static int override_flag_to_prop(int flag)
> +{
> +	switch (flag) {
> +	case OVERRIDE_FLAG_DIRECTION:
> +		return GPIOD_LINE_CONFIG_PROP_DIRECTION;
> +	case OVERRIDE_FLAG_EDGE:
> +		return GPIOD_LINE_CONFIG_PROP_EDGE;
> +	case OVERRIDE_FLAG_BIAS:
> +		return GPIOD_LINE_CONFIG_PROP_BIAS;
> +	case OVERRIDE_FLAG_DRIVE:
> +		return GPIOD_LINE_CONFIG_PROP_DRIVE;
> +	case OVERRIDE_FLAG_ACTIVE_LOW:
> +		return GPIOD_LINE_CONFIG_PROP_ACTIVE_LOW;
> +	case OVERRIDE_FLAG_DEBOUNCE_PERIOD:
> +		return GPIOD_LINE_CONFIG_PROP_DEBOUNCE_PERIOD;
> +	case OVERRIDE_FLAG_CLOCK:
> +		return GPIOD_LINE_CONFIG_PROP_EVENT_CLOCK;
> +	case OVERRIDE_FLAG_OUTPUT_VALUE:
> +		return GPIOD_LINE_CONFIG_PROP_OUTPUT_VALUE;
> +	}
> +
> +	/* Can't happen. */
>  	return -1;
>  }
>  
> +GPIOD_API void
> +gpiod_line_config_get_overrides(struct gpiod_line_config *config,
> +				unsigned int *offsets, int *props)
> +{
> +	struct override_config *override;
> +	unsigned int i, j, count = 0;
> +
> +	for (i = 0; i < GPIO_V2_LINES_MAX; i++) {
> +		override = &config->overrides[i];
> +
> +		if (override_used(override)) {
> +			for (j = 0; j < NUM_OVERRIDE_FLAGS; j++) {
> +				if (override->override_flags &
> +				    override_flag_list[j]) {
> +					offsets[count] = override->offset;
> +					props[count] = override_flag_to_prop(
> +							override_flag_list[j]);
> +					count++;
> +				}
> +			}
> +		}
> +	}
> +}
> +

Return the count?
Would be a bit redundant, as the user needs to call 
gpiod_line_config_get_num_overrides() to size the offsets and props
arrays beforehand, but the usual patten when writing into a passed array
is to return the number of elements written.

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [libgpiod v2][PATCH v3 3/3] tests: rewrite core C tests using libgpiosim
  2022-03-03  9:18 ` [libgpiod v2][PATCH v3 3/3] tests: rewrite core C tests using libgpiosim Bartosz Golaszewski
@ 2022-03-05  5:51   ` Kent Gibson
  0 siblings, 0 replies; 8+ messages in thread
From: Kent Gibson @ 2022-03-05  5:51 UTC (permalink / raw)
  To: Bartosz Golaszewski; +Cc: Linus Walleij, Andy Shevchenko, linux-gpio

On Thu, Mar 03, 2022 at 10:18:36AM +0100, Bartosz Golaszewski wrote:
> This replaces the old tests for the C API v1 based on gpio-mockup with
> a test suite based on gpio-sim that covers around 95% of the libgpiod v2
> codebase.
> 
> The test harness has been rebuilt and shrank significantly as well. The
> libgpiosim API has been wrapped in a gobject interface.
> 
> Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl>
> ---
>  configure.ac                 |    8 +-
>  tests/Makefile.am            |   24 +-
>  tests/gpiod-test-helpers.c   |   49 ++
>  tests/gpiod-test-helpers.h   |  139 +++++
>  tests/gpiod-test-sim.c       |  308 ++++++++++
>  tests/gpiod-test-sim.h       |   42 ++
>  tests/gpiod-test.c           |  233 +-------
>  tests/gpiod-test.h           |   83 +--
>  tests/gpiosim/gpiosim.c      |    1 +
>  tests/mockup/Makefile.am     |   11 -
>  tests/mockup/gpio-mockup.c   |  496 ----------------
>  tests/mockup/gpio-mockup.h   |   36 --
>  tests/tests-chip.c           |  282 ++++-----
>  tests/tests-edge-event.c     |  490 +++++++++++++++
>  tests/tests-event.c          |  908 ----------------------------
>  tests/tests-info-event.c     |  301 ++++++++++
>  tests/tests-line-config.c    |  503 ++++++++++++++++
>  tests/tests-line-info.c      |  318 ++++++++++
>  tests/tests-line-request.c   |  526 ++++++++++++++++
>  tests/tests-line.c           | 1091 ----------------------------------
>  tests/tests-misc.c           |   80 ++-
>  tests/tests-request-config.c |   90 +++
>  22 files changed, 2995 insertions(+), 3024 deletions(-)
>  create mode 100644 tests/gpiod-test-helpers.c
>  create mode 100644 tests/gpiod-test-helpers.h
>  create mode 100644 tests/gpiod-test-sim.c
>  create mode 100644 tests/gpiod-test-sim.h
>  delete mode 100644 tests/mockup/Makefile.am
>  delete mode 100644 tests/mockup/gpio-mockup.c
>  delete mode 100644 tests/mockup/gpio-mockup.h
>  create mode 100644 tests/tests-edge-event.c
>  delete mode 100644 tests/tests-event.c
>  create mode 100644 tests/tests-info-event.c
>  create mode 100644 tests/tests-line-config.c
>  create mode 100644 tests/tests-line-info.c
>  create mode 100644 tests/tests-line-request.c
>  delete mode 100644 tests/tests-line.c
>  create mode 100644 tests/tests-request-config.c
> 
> diff --git a/configure.ac b/configure.ac
> index cb4c1fd..f8d34ed 100644
> --- a/configure.ac
> +++ b/configure.ac
> @@ -28,9 +28,8 @@ AC_SUBST(VERSION_STR, [$PACKAGE_VERSION$EXTRA_VERSION])
>  AC_SUBST(ABI_VERSION, [4.1.2])
>  # Have a separate ABI version for C++ bindings:
>  AC_SUBST(ABI_CXX_VERSION, [2.1.1])
> -# ABI version for libgpiomockup (we need this since it can be installed if we
> +# ABI version for libgpiosim (we need this since it can be installed if we
>  # enable install-tests).
> -AC_SUBST(ABI_MOCKUP_VERSION, [0.1.0])
>  AC_SUBST(ABI_GPIOSIM_VERSION, [0.1.0])
>  
>  AC_CONFIG_AUX_DIR([autostuff])
> @@ -138,14 +137,14 @@ AC_DEFUN([FUNC_NOT_FOUND_TESTS],
>  
>  if test "x$with_tests" = xtrue
>  then
> -	# For libgpiomockup & libgpiosim
> +	# For libgpiosim
>  	AC_CHECK_FUNC([qsort], [], [FUNC_NOT_FOUND_TESTS([qsort])])
>  	PKG_CHECK_MODULES([KMOD], [libkmod >= 18])
> -	PKG_CHECK_MODULES([UDEV], [libudev >= 215])
>  	PKG_CHECK_MODULES([MOUNT], [mount >= 2.33.1])
>  
>  	# For core library tests
>  	PKG_CHECK_MODULES([GLIB], [glib-2.0 >= 2.50])
> +	PKG_CHECK_MODULES([GOBJECT], [gobject-2.0 >= 2.50])
>  
>  	if test "x$with_tools" = xtrue
>  	then
> @@ -236,7 +235,6 @@ AC_CONFIG_FILES([Makefile
>  		 lib/libgpiod.pc
>  		 tools/Makefile
>  		 tests/Makefile
> -		 tests/mockup/Makefile
>  		 tests/gpiosim/Makefile
>  		 bindings/cxx/libgpiodcxx.pc
>  		 bindings/Makefile
> diff --git a/tests/Makefile.am b/tests/Makefile.am
> index 2b1e082..dce9a5a 100644
> --- a/tests/Makefile.am
> +++ b/tests/Makefile.am
> @@ -1,24 +1,32 @@
>  # SPDX-License-Identifier: GPL-2.0-or-later
>  # SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <brgl@bgdev.pl>
>  
> -SUBDIRS = mockup gpiosim
> -
> +SUBDIRS = gpiosim
> + 

Whitespace error.

Other than that all good.

Good to see the randomness removed from the gpio-sim naming.
I would like to see the option removed from libgpiosim as well, but that
is outside the scope of this patch series.

Cheers,
Kent.

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [libgpiod v2][PATCH v3 2/3] line-config: expose the override logic to users
  2022-03-05  5:51   ` Kent Gibson
@ 2022-03-05 20:57     ` Bartosz Golaszewski
  0 siblings, 0 replies; 8+ messages in thread
From: Bartosz Golaszewski @ 2022-03-05 20:57 UTC (permalink / raw)
  To: Kent Gibson; +Cc: Linus Walleij, Andy Shevchenko, open list:GPIO SUBSYSTEM

On Sat, Mar 5, 2022 at 6:51 AM Kent Gibson <warthog618@gmail.com> wrote:
>
> On Thu, Mar 03, 2022 at 10:18:35AM +0100, Bartosz Golaszewski wrote:
> > We've already added getters for line-config but without exposing some
> > parts of the internal logic of the object, the user can't really get
> > the full picture and inspect the contents. This patch reworks the
> > accessors further by providing access to the underlying override
> > mechanism.
> >
> > For every setting, we expose a getter and setter for the default value
> > as well as a set of four functions for setting, getting, clearing and
> > checking per-offset overrides.
> >
> > An override can initially have the same value as the defaults but will
> > retain the overridden value should the defaults change.
> >
> > We also complete the API by providing functions that allow to retrieve
> > the overridden offsets and their corresponding property types.
> >
> > This way the caller can fully inspect the line_config and high-level
> > language bindings can provide stringification methods.
> >
> > While at it: we fix a couple bugs in the implementation of struct
> > line_config and add new constructors that take a variable list of
> > arguments.
> >
>
> The variadic constructor is new for patch v3.
> It bundles default constructor + default mutators, so doesn't add
> functionality that wasn't already available - it just makes it
> accessible via a single function call.
> Is the variadic form beneficial for bindings, say Python, where you
> would prefer not to be making a bunch of C calls?
> Or is this just sugar?
>

No, there's no benefit for the bindings as they can't possibly
construct a va_list. I very much intend to have keyword arguments in
Python but the only reason I added the new constructors is to justify
adding the GPIOD_LINE_CONFIG_PROP_* enum because without them it would
only be used for retrieving the override list.

> No major objection, just curious about the rationale for adding it.
>

Actually looking at them now, I think they don't make much sense and I
think about dropping them from the next iteration. It's not like the
enum increases the executable size really.

> [snip]
>
> > +/**
> > + * @brief Get the total number of overridden settings currently stored by this
> > + *        line config object.
> > + * @param config Line config object.
> > + * @return Number of individual overridden settings.
> > + */
> > +unsigned int
> > +gpiod_line_config_get_num_overrides(struct gpiod_line_config *config);
> > +
> > +/**
> > + * @brief Get the list of overridden offsets and the corresponding types of
> > + *        overridden settings.
> > + * @param config Line config object.
> > + * @param offsets Array to store the overidden offsets. Must hold at least the
> > + *                number of unsigned integers returned by
> > + *                ::gpiod_line_config_get_output_value_offset.
> > + * @param props Array to store the types of overridden settings. Must hold at
> > + *              least the number of integers returned by
> > + *              gpiod_line_config_get_output_value_offset.
> > + */
>
> The purpose of the offsets and props arrays is not clear.
> Clarify that you are returning a list of (offset,prop), split across the
> two arrays.
> Replace them with a single array of (offset,prop) unless there is
> a good reason to keep them separate?

The only (but good) reason is to not introduce another data structure.
Especially a public one. This function is not going to be used a lot I
suppose so I don't care if it's awkward to use.

>
> Guessing it should be gpiod_line_config_get_num_overrides(), not
> gpiod_line_config_get_output_value_offset() which returns 0 or 1, or
> even better -1 for inputs?
>

Yep, just a wrong copy/paste.

> [snip]
>
> > +GPIOD_API unsigned int
> > +gpiod_line_config_get_num_overrides(struct gpiod_line_config *config)
> > +{
> > +     struct override_config *override;
> > +     unsigned int i, j, count = 0;
> >
> > -     errno = ENXIO;
> > +     for (i = 0; i < GPIO_V2_LINES_MAX; i++) {
> > +             override = &config->overrides[i];
> > +
> > +             if (override_used(override)) {
> > +                     for (j = 0; j < NUM_OVERRIDE_FLAGS; j++) {
> > +                             if (override->override_flags &
> > +                                 override_flag_list[j])
> > +                                     count++;
> > +                     }
> > +             }
> > +     }
> > +
> > +     return count;
> > +}
> > +
>
> Using GPIO_V2_LINES_MAX for the size of the overrides array is
> confusing, and the two should be de-coupled so you can more easily resize
> the array if necessary.
> Provide a NUM_OVERRIDES_MAX, or similar, and use that when referring
> to the size of the overrides array.
>

Makes sense.

> > +static int override_flag_to_prop(int flag)
> > +{
> > +     switch (flag) {
> > +     case OVERRIDE_FLAG_DIRECTION:
> > +             return GPIOD_LINE_CONFIG_PROP_DIRECTION;
> > +     case OVERRIDE_FLAG_EDGE:
> > +             return GPIOD_LINE_CONFIG_PROP_EDGE;
> > +     case OVERRIDE_FLAG_BIAS:
> > +             return GPIOD_LINE_CONFIG_PROP_BIAS;
> > +     case OVERRIDE_FLAG_DRIVE:
> > +             return GPIOD_LINE_CONFIG_PROP_DRIVE;
> > +     case OVERRIDE_FLAG_ACTIVE_LOW:
> > +             return GPIOD_LINE_CONFIG_PROP_ACTIVE_LOW;
> > +     case OVERRIDE_FLAG_DEBOUNCE_PERIOD:
> > +             return GPIOD_LINE_CONFIG_PROP_DEBOUNCE_PERIOD;
> > +     case OVERRIDE_FLAG_CLOCK:
> > +             return GPIOD_LINE_CONFIG_PROP_EVENT_CLOCK;
> > +     case OVERRIDE_FLAG_OUTPUT_VALUE:
> > +             return GPIOD_LINE_CONFIG_PROP_OUTPUT_VALUE;
> > +     }
> > +
> > +     /* Can't happen. */
> >       return -1;
> >  }
> >
> > +GPIOD_API void
> > +gpiod_line_config_get_overrides(struct gpiod_line_config *config,
> > +                             unsigned int *offsets, int *props)
> > +{
> > +     struct override_config *override;
> > +     unsigned int i, j, count = 0;
> > +
> > +     for (i = 0; i < GPIO_V2_LINES_MAX; i++) {
> > +             override = &config->overrides[i];
> > +
> > +             if (override_used(override)) {
> > +                     for (j = 0; j < NUM_OVERRIDE_FLAGS; j++) {
> > +                             if (override->override_flags &
> > +                                 override_flag_list[j]) {
> > +                                     offsets[count] = override->offset;
> > +                                     props[count] = override_flag_to_prop(
> > +                                                     override_flag_list[j]);
> > +                                     count++;
> > +                             }
> > +                     }
> > +             }
> > +     }
> > +}
> > +
>
> Return the count?
> Would be a bit redundant, as the user needs to call
> gpiod_line_config_get_num_overrides() to size the offsets and props
> arrays beforehand, but the usual patten when writing into a passed array
> is to return the number of elements written.
>

So there are two typical approaches: take the size of the passed
buffer as argument and return the number of elements actually written
or define the required size of the buffer (in this case: the size
returned by the get_num_overrides func) and don't return that number.
I don't think it would be of any use so let's not return a value
nobody would check.

> Cheers,
> Kent.

Bart

^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2022-03-05 20:57 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2022-03-03  9:18 [libgpiod v2][PATCH v3 0/3] libgpiod v2: rewrite tests for the C library Bartosz Golaszewski
2022-03-03  9:18 ` [libgpiod v2][PATCH v3 1/3] API: add an enum for line values Bartosz Golaszewski
2022-03-05  5:50   ` Kent Gibson
2022-03-03  9:18 ` [libgpiod v2][PATCH v3 2/3] line-config: expose the override logic to users Bartosz Golaszewski
2022-03-05  5:51   ` Kent Gibson
2022-03-05 20:57     ` Bartosz Golaszewski
2022-03-03  9:18 ` [libgpiod v2][PATCH v3 3/3] tests: rewrite core C tests using libgpiosim Bartosz Golaszewski
2022-03-05  5:51   ` Kent Gibson

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).