new file mode 100644
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+gpiod-cxx-test
new file mode 100644
@@ -0,0 +1,31 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+AM_CPPFLAGS = -I$(top_srcdir)/bindings/cxx/ -I$(top_srcdir)/include
+AM_CPPFLAGS += -I$(top_srcdir)/tests/gpiosim/
+AM_CPPFLAGS += -Wall -Wextra -g -std=gnu++17 $(CATCH2_CFLAGS)
+AM_CPPFLAGS += $(PROFILING_CFLAGS)
+AM_LDFLAGS = -lgpiodcxx -L$(top_builddir)/bindings/cxx/
+AM_LDFLAGS += -lgpiosim -L$(top_builddir)/tests/gpiosim/
+AM_LDFLAGS += $(PROFILING_LDFLAGS)
+AM_LDFLAGS += -pthread
+
+bin_PROGRAMS = gpiod-cxx-test
+
+gpiod_cxx_test_SOURCES = \
+ check-kernel.cpp \
+ gpiod-cxx-test-main.cpp \
+ gpiosim.cpp \
+ gpiosim.hpp \
+ helpers.cpp \
+ helpers.hpp \
+ tests-chip.cpp \
+ tests-chip-info.cpp \
+ tests-edge-event.cpp \
+ tests-line.cpp \
+ tests-line-config.cpp \
+ tests-line-info.cpp \
+ tests-line-request.cpp \
+ tests-info-event.cpp \
+ tests-misc.cpp \
+ tests-request-config.cpp
new file mode 100644
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <linux/version.h>
+#include <sys/utsname.h>
+#include <system_error>
+#include <sstream>
+
+namespace {
+
+class kernel_checker
+{
+public:
+ kernel_checker(int major, int minor, int release)
+ {
+ int curr_major, curr_minor, curr_release, curr_ver, req_ver;
+ ::std::string major_str, minor_str, release_str;
+ ::utsname un;
+ int ret;
+
+ ret = ::uname(::std::addressof(un));
+ if (ret)
+ throw ::std::system_error(errno, ::std::system_category(),
+ "unable to read the kernel version");
+
+ ::std::stringstream ver_stream(::std::string(un.release));
+ ::std::getline(ver_stream, major_str, '.');
+ ::std::getline(ver_stream, minor_str, '.');
+ ::std::getline(ver_stream, release_str, '-');
+
+ curr_major = ::std::stoi(major_str, nullptr, 0);
+ curr_minor = ::std::stoi(minor_str, nullptr, 0);
+ curr_release = ::std::stoi(release_str, nullptr, 0);
+
+ curr_ver = KERNEL_VERSION(curr_major, curr_minor, curr_release);
+ req_ver = KERNEL_VERSION(major, minor, release);
+
+ if (curr_ver < req_ver)
+ throw ::std::runtime_error("kernel release must be at least: " +
+ ::std::to_string(major) + "." +
+ ::std::to_string(minor) + "." +
+ ::std::to_string(release));
+ }
+};
+
+kernel_checker require_kernel(5, 17, 0);
+
+} /* namespace */
new file mode 100644
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2017-2021 Bartosz Golaszewski <bartekgola@gmail.com>
+
+#define CATCH_CONFIG_MAIN
+#include <catch2/catch.hpp>
new file mode 100644
@@ -0,0 +1,258 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#include <functional>
+#include <map>
+#include <system_error>
+
+#include "gpiosim.h"
+#include "gpiosim.hpp"
+
+#define NORETURN __attribute__((noreturn))
+
+namespace gpiosim {
+
+namespace {
+
+const ::std::map<chip::pull, int> pull_mapping = {
+ { chip::pull::PULL_UP, GPIOSIM_PULL_UP },
+ { chip::pull::PULL_DOWN, GPIOSIM_PULL_DOWN }
+};
+
+const ::std::map<chip::hog_direction, int> hog_dir_mapping = {
+ { chip::hog_direction::INPUT, GPIOSIM_HOG_DIR_INPUT },
+ { chip::hog_direction::OUTPUT_HIGH, GPIOSIM_HOG_DIR_OUTPUT_HIGH },
+ { chip::hog_direction::OUTPUT_LOW, GPIOSIM_HOG_DIR_OUTPUT_LOW }
+};
+
+const ::std::map<int, chip::value> value_mapping = {
+ { GPIOSIM_VALUE_INACTIVE, chip::value::INACTIVE },
+ { GPIOSIM_VALUE_ACTIVE, chip::value::ACTIVE }
+};
+
+template<class gpiosim_type, void free_func(gpiosim_type*)> struct deleter
+{
+ void operator()(gpiosim_type* ptr)
+ {
+ free_func(ptr);
+ }
+};
+
+using ctx_deleter = deleter<::gpiosim_ctx, ::gpiosim_ctx_unref>;
+using dev_deleter = deleter<::gpiosim_dev, ::gpiosim_dev_unref>;
+using bank_deleter = deleter<::gpiosim_bank, ::gpiosim_bank_unref>;
+
+using ctx_ptr = ::std::unique_ptr<::gpiosim_ctx, ctx_deleter>;
+using dev_ptr = ::std::unique_ptr<::gpiosim_dev, dev_deleter>;
+using bank_ptr = ::std::unique_ptr<::gpiosim_bank, bank_deleter>;
+
+ctx_ptr sim_ctx;
+
+class sim_ctx_initializer
+{
+public:
+ sim_ctx_initializer(void)
+ {
+ sim_ctx.reset(gpiosim_ctx_new());
+ if (!sim_ctx)
+ throw ::std::system_error(errno, ::std::system_category(),
+ "unable to create the GPIO simulator context");
+ }
+};
+
+dev_ptr make_sim_dev(void)
+{
+ static sim_ctx_initializer ctx_initializer;
+
+ dev_ptr dev(::gpiosim_dev_new(sim_ctx.get()));
+ if (!dev)
+ throw ::std::system_error(errno, ::std::system_category(),
+ "failed to create a new GPIO simulator device");
+
+ return dev;
+}
+
+bank_ptr make_sim_bank(const dev_ptr& dev)
+{
+ bank_ptr bank(::gpiosim_bank_new(dev.get()));
+ if (!bank)
+ throw ::std::system_error(errno, ::std::system_category(),
+ "failed to create a new GPIO simulator bank");
+
+ return bank;
+}
+
+NORETURN void throw_invalid_type(void)
+{
+ throw ::std::logic_error("invalid type for property");
+}
+
+unsigned any_to_unsigned_int(const ::std::any& val)
+{
+ if (val.type() == typeid(int)) {
+ auto num_lines = ::std::any_cast<int>(val);
+ if (num_lines < 0)
+ throw ::std::invalid_argument("negative value not accepted");
+
+ return static_cast<unsigned int>(num_lines);
+ } else if (val.type() == typeid(unsigned int)) {
+ return ::std::any_cast<unsigned int>(val);
+ }
+
+ throw_invalid_type();
+}
+
+::std::string any_to_string(const ::std::any& val)
+{
+ if (val.type() == typeid(::std::string))
+ return ::std::any_cast<::std::string>(val);
+ else if (val.type() == typeid(const char*))
+ return ::std::any_cast<const char*>(val);
+
+ throw_invalid_type();
+}
+
+} /* namespace */
+
+struct chip::impl
+{
+ impl(void)
+ : dev(make_sim_dev()),
+ bank(make_sim_bank(this->dev)),
+ has_num_lines(false),
+ has_label(false)
+ {
+
+ }
+
+ impl(const impl& other) = delete;
+ impl(impl&& other) = delete;
+ ~impl(void) = default;
+ impl& operator=(const impl& other) = delete;
+ impl& operator=(impl&& other) = delete;
+
+ static const ::std::map<chip::property,
+ ::std::function<void (impl&,
+ const ::std::any&)>> setter_mapping;
+
+ void set_property(chip::property prop, const ::std::any& val)
+ {
+ setter_mapping.at(prop)(*this, val);
+ }
+
+ void set_num_lines(const ::std::any& val)
+ {
+ if (this->has_num_lines)
+ throw ::std::logic_error("number of lines can be set at most once");
+
+ int ret = ::gpiosim_bank_set_num_lines(this->bank.get(), any_to_unsigned_int(val));
+ if (ret)
+ throw ::std::system_error(errno, ::std::system_category(),
+ "failed to set the number of lines");
+
+ this->has_num_lines = true;
+ }
+
+ void set_label(const ::std::any& val)
+ {
+ if (this->has_label)
+ throw ::std::logic_error("label can be set at most once");
+
+ int ret = ::gpiosim_bank_set_label(this->bank.get(),
+ any_to_string(val).c_str());
+ if (ret)
+ throw ::std::system_error(errno, ::std::system_category(),
+ "failed to set the chip label");
+
+ this->has_label = true;
+ }
+
+ void set_line_name(const ::std::any& val)
+ {
+ auto name = ::std::any_cast<line_name>(val);
+
+ int ret = ::gpiosim_bank_set_line_name(this->bank.get(),
+ ::std::get<0>(name),
+ ::std::get<1>(name).c_str());
+ if (ret)
+ throw ::std::system_error(errno, ::std::system_category(),
+ "failed to set simulated line name");
+ }
+
+ void set_line_hog(const ::std::any& val)
+ {
+ auto hog = ::std::any_cast<line_hog>(val);
+
+ int ret = ::gpiosim_bank_hog_line(this->bank.get(),
+ ::std::get<0>(hog),
+ ::std::get<1>(hog).c_str(),
+ hog_dir_mapping.at(::std::get<2>(hog)));
+ if (ret)
+ throw ::std::system_error(errno, ::std::system_category(),
+ "failed to hog a simulated line");
+ }
+
+ dev_ptr dev;
+ bank_ptr bank;
+ bool has_num_lines;
+ bool has_label;
+};
+
+const ::std::map<chip::property,
+ ::std::function<void (chip::impl&,
+ const ::std::any&)>> chip::impl::setter_mapping = {
+ { chip::property::NUM_LINES, &chip::impl::set_num_lines },
+ { chip::property::LABEL, &chip::impl::set_label },
+ { chip::property::LINE_NAME, &chip::impl::set_line_name },
+ { chip::property::HOG, &chip::impl::set_line_hog }
+};
+
+chip::chip(const properties& args)
+ : _m_priv(new impl)
+{
+ int ret;
+
+ for (const auto& arg: args)
+ this->_m_priv.get()->set_property(arg.first, arg.second);
+
+ ret = ::gpiosim_dev_enable(this->_m_priv->dev.get());
+ if (ret)
+ throw ::std::system_error(errno, ::std::system_category(),
+ "failed to enable the simulated GPIO chip");
+}
+
+chip::~chip(void)
+{
+ this->_m_priv.reset(nullptr);
+}
+
+::std::filesystem::path chip::dev_path(void) const
+{
+ return ::gpiosim_bank_get_dev_path(this->_m_priv->bank.get());
+}
+
+::std::string chip::name(void) const
+{
+ return ::gpiosim_bank_get_chip_name(this->_m_priv->bank.get());
+}
+
+chip::value chip::get_value(unsigned int offset)
+{
+ int val = ::gpiosim_bank_get_value(this->_m_priv->bank.get(), offset);
+ if (val < 0)
+ throw ::std::system_error(errno, ::std::system_category(),
+ "failed to read the simulated GPIO line value");
+
+ return value_mapping.at(val);
+}
+
+void chip::set_pull(unsigned int offset, pull pull)
+{
+ int ret = ::gpiosim_bank_set_pull(this->_m_priv->bank.get(),
+ offset, pull_mapping.at(pull));
+ if (ret)
+ throw ::std::system_error(errno, ::std::system_category(),
+ "failed to set the pull of simulated GPIO line");
+}
+
+} /* namespace gpiosim */
new file mode 100644
@@ -0,0 +1,69 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#ifndef __GPIOD_CXX_GPIOSIM_HPP__
+#define __GPIOD_CXX_GPIOSIM_HPP__
+
+#include <any>
+#include <filesystem>
+#include <memory>
+#include <tuple>
+#include <utility>
+#include <vector>
+
+namespace gpiosim {
+
+class chip
+{
+public:
+ enum class property {
+ NUM_LINES = 1,
+ LABEL,
+ LINE_NAME,
+ HOG
+ };
+
+ enum class pull {
+ PULL_UP = 1,
+ PULL_DOWN
+ };
+
+ enum class hog_direction {
+ INPUT = 1,
+ OUTPUT_HIGH,
+ OUTPUT_LOW
+ };
+
+ enum class value {
+ INACTIVE = 0,
+ ACTIVE = 1
+ };
+
+ using line_name = ::std::tuple<unsigned int, ::std::string>;
+ using line_hog = ::std::tuple<unsigned int, ::std::string, hog_direction>;
+ using properties = ::std::vector<::std::pair<property, ::std::any>>;
+
+ explicit chip(const properties& args = properties());
+ chip(const chip& other) = delete;
+ chip(chip&& other) = delete;
+ ~chip(void);
+
+ chip& operator=(const chip& other) = delete;
+ chip& operator=(chip&& other) = delete;
+
+ ::std::filesystem::path dev_path(void) const;
+ ::std::string name(void) const;
+
+ value get_value(unsigned int offset);
+ void set_pull(unsigned int offset, pull pull);
+
+private:
+
+ struct impl;
+
+ ::std::unique_ptr<impl> _m_priv;
+};
+
+} /* namespace gpiosim */
+
+#endif /* __GPIOD_CXX_GPIOSIM_HPP__ */
new file mode 100644
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include "helpers.hpp"
+
+system_error_matcher::system_error_matcher(int expected_errno)
+ : _m_cond(::std::system_category().default_error_condition(expected_errno))
+{
+
+}
+
+::std::string system_error_matcher::describe(void) const
+{
+ return "matches: errno " + ::std::to_string(this->_m_cond.value());
+}
+
+bool system_error_matcher::match(const ::std::system_error& error) const
+{
+ return error.code().value() == this->_m_cond.value();
+}
+
+regex_matcher::regex_matcher(const ::std::string& pattern)
+ : _m_pattern(pattern),
+ _m_repr("matches: regex \"" + pattern + "\"")
+{
+
+}
+
+::std::string regex_matcher::describe(void) const
+{
+ return this->_m_repr;
+}
+
+bool regex_matcher::match(const ::std::string& str) const
+{
+ return ::std::regex_match(str, this->_m_pattern);
+}
new file mode 100644
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: LGPL-3.0-or-later */
+/* SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl> */
+
+#ifndef __GPIOD_CXX_TEST_HELPERS_HPP__
+#define __GPIOD_CXX_TEST_HELPERS_HPP__
+
+#include <catch2/catch.hpp>
+#include <regex>
+#include <string>
+#include <sstream>
+#include <system_error>
+
+class system_error_matcher : public Catch::MatcherBase<::std::system_error>
+{
+public:
+ explicit system_error_matcher(int expected_errno);
+ ::std::string describe(void) const override;
+ bool match(const ::std::system_error& error) const override;
+
+private:
+ ::std::error_condition _m_cond;
+};
+
+class regex_matcher : public Catch::MatcherBase<::std::string>
+{
+public:
+ explicit regex_matcher(const ::std::string& pattern);
+ ::std::string describe(void) const override;
+ bool match(const ::std::string& str) const override;
+
+private:
+ ::std::regex _m_pattern;
+ ::std::string _m_repr;
+};
+
+template<class T> class stringify_matcher : public Catch::MatcherBase<T>
+{
+public:
+ explicit stringify_matcher(const ::std::string& expected) : _m_expected(expected)
+ {
+
+ }
+
+ ::std::string describe(void) const override
+ {
+ return "equals " + this->_m_expected;
+ }
+
+ bool match(const T& obj) const override
+ {
+ ::std::stringstream buf;
+
+ buf << obj;
+
+ return buf.str() == this->_m_expected;
+ }
+
+private:
+ ::std::string _m_expected;
+};
+
+#endif /* __GPIOD_CXX_TEST_HELPERS_HPP__ */
new file mode 100644
@@ -0,0 +1,109 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <gpiod.hpp>
+#include <sstream>
+
+#include "gpiosim.hpp"
+#include "helpers.hpp"
+
+using property = ::gpiosim::chip::property;
+
+namespace {
+
+TEST_CASE("chip_info properties can be read", "[chip-info][chip]")
+{
+ ::gpiosim::chip sim({{ property::NUM_LINES, 8 }, { property::LABEL, "foobar" }});
+ ::gpiod::chip chip(sim.dev_path());
+ auto info = chip.get_info();
+
+ SECTION("get chip name")
+ {
+ REQUIRE_THAT(info.name(), Catch::Equals(sim.name()));
+ }
+
+ SECTION("get chip label")
+ {
+ REQUIRE_THAT(info.label(), Catch::Equals("foobar"));
+ }
+
+ SECTION("get num_lines")
+ {
+ REQUIRE(info.num_lines() == 8);
+ }
+}
+
+TEST_CASE("chip_info can be copied and moved", "[chip-info]")
+{
+ ::gpiosim::chip sim({{ property::NUM_LINES, 4 }, { property::LABEL, "foobar" }});
+ ::gpiod::chip chip(sim.dev_path());
+ auto info = chip.get_info();
+
+ SECTION("copy constructor works")
+ {
+ auto copy(info);
+
+ REQUIRE_THAT(copy.name(), Catch::Equals(sim.name()));
+ REQUIRE_THAT(copy.label(), Catch::Equals("foobar"));
+ REQUIRE(copy.num_lines() == 4);
+
+ REQUIRE_THAT(info.name(), Catch::Equals(sim.name()));
+ REQUIRE_THAT(info.label(), Catch::Equals("foobar"));
+ REQUIRE(info.num_lines() == 4);
+ }
+
+ SECTION("assignment operator works")
+ {
+ auto copy = chip.get_info();
+
+ copy = info;
+
+ REQUIRE_THAT(copy.name(), Catch::Equals(sim.name()));
+ REQUIRE_THAT(copy.label(), Catch::Equals("foobar"));
+ REQUIRE(copy.num_lines() == 4);
+
+ REQUIRE_THAT(info.name(), Catch::Equals(sim.name()));
+ REQUIRE_THAT(info.label(), Catch::Equals("foobar"));
+ REQUIRE(info.num_lines() == 4);
+ }
+
+ SECTION("move constructor works")
+ {
+ auto moved(std::move(info));
+
+ REQUIRE_THAT(moved.name(), Catch::Equals(sim.name()));
+ REQUIRE_THAT(moved.label(), Catch::Equals("foobar"));
+ REQUIRE(moved.num_lines() == 4);
+ }
+
+ SECTION("move assignment operator works")
+ {
+ auto moved = chip.get_info();
+
+ moved = ::std::move(info);
+
+ REQUIRE_THAT(moved.name(), Catch::Equals(sim.name()));
+ REQUIRE_THAT(moved.label(), Catch::Equals("foobar"));
+ REQUIRE(moved.num_lines() == 4);
+ }
+}
+
+TEST_CASE("stream insertion operator works for chip_info", "[chip-info]")
+{
+ ::gpiosim::chip sim({
+ { property::NUM_LINES, 4 },
+ { property::LABEL, "foobar" }
+ });
+
+ ::gpiod::chip chip(sim.dev_path());
+ auto info = chip.get_info();
+ ::std::stringstream expected;
+
+ expected << "gpiod::chip_info(name=\"" << sim.name() <<
+ "\", label=\"foobar\", num_lines=4)";
+
+ REQUIRE_THAT(info, stringify_matcher<::gpiod::chip_info>(expected.str()));
+}
+
+} /* namespace */
new file mode 100644
@@ -0,0 +1,171 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <gpiod.hpp>
+#include <sstream>
+#include <system_error>
+#include <utility>
+
+#include "gpiosim.hpp"
+#include "helpers.hpp"
+
+using property = ::gpiosim::chip::property;
+using line_name = ::gpiosim::chip::line_name;
+
+namespace {
+
+TEST_CASE("chip constructor works", "[chip]")
+{
+ SECTION("open an existing GPIO chip")
+ {
+ ::gpiosim::chip sim;
+
+ REQUIRE_NOTHROW(::gpiod::chip(sim.dev_path()));
+ }
+
+ SECTION("opening a nonexistent file fails with ENOENT")
+ {
+ REQUIRE_THROWS_MATCHES(::gpiod::chip("/dev/nonexistent"),
+ ::std::system_error, system_error_matcher(ENOENT));
+ }
+
+ SECTION("opening a file that is not a device fails with ENOTTY")
+ {
+ REQUIRE_THROWS_MATCHES(::gpiod::chip("/tmp"),
+ ::std::system_error, system_error_matcher(ENOTTY));
+ }
+
+ SECTION("opening a non-GPIO character device fails with ENODEV")
+ {
+ REQUIRE_THROWS_MATCHES(::gpiod::chip("/dev/null"),
+ ::std::system_error, system_error_matcher(ENODEV));
+ }
+
+ SECTION("move constructor")
+ {
+ ::gpiosim::chip sim({{ property::LABEL, "foobar" }});
+
+ ::gpiod::chip first(sim.dev_path());
+ REQUIRE_THAT(first.get_info().label(), Catch::Equals("foobar"));
+ ::gpiod::chip second(::std::move(first));
+ REQUIRE_THAT(second.get_info().label(), Catch::Equals("foobar"));
+ }
+}
+
+TEST_CASE("chip operators work", "[chip]")
+{
+ ::gpiosim::chip sim({{ property::LABEL, "foobar" }});
+ ::gpiod::chip chip(sim.dev_path());
+
+ SECTION("assignment operator")
+ {
+ ::gpiosim::chip moved_sim({{ property::LABEL, "moved" }});
+ ::gpiod::chip moved_chip(moved_sim.dev_path());
+
+ REQUIRE_THAT(chip.get_info().label(), Catch::Equals("foobar"));
+ chip = ::std::move(moved_chip);
+ REQUIRE_THAT(chip.get_info().label(), Catch::Equals("moved"));
+ }
+
+ SECTION("boolean operator")
+ {
+ REQUIRE(chip);
+ chip.close();
+ REQUIRE_FALSE(chip);
+ }
+}
+
+TEST_CASE("chip properties can be read", "[chip]")
+{
+ ::gpiosim::chip sim({{ property::NUM_LINES, 8 }, { property::LABEL, "foobar" }});
+ ::gpiod::chip chip(sim.dev_path());
+
+ SECTION("get device path")
+ {
+ REQUIRE_THAT(chip.path(), Catch::Equals(sim.dev_path()));
+ }
+
+ SECTION("get file descriptor")
+ {
+ REQUIRE(chip.fd() >= 0);
+ }
+}
+
+TEST_CASE("line lookup by name works", "[chip]")
+{
+ ::gpiosim::chip sim({
+ { property::NUM_LINES, 8 },
+ { property::LINE_NAME, line_name(0, "foo") },
+ { property::LINE_NAME, line_name(2, "bar") },
+ { property::LINE_NAME, line_name(3, "baz") },
+ { property::LINE_NAME, line_name(5, "xyz") }
+ });
+
+ ::gpiod::chip chip(sim.dev_path());
+
+ SECTION("lookup successful")
+ {
+ REQUIRE(chip.get_line_offset_from_name("baz") == 3);
+ }
+
+ SECTION("lookup failed")
+ {
+ REQUIRE(chip.get_line_offset_from_name("nonexistent") < 0);
+ }
+}
+
+TEST_CASE("line lookup: behavior for duplicate names", "[chip]")
+{
+ ::gpiosim::chip sim({
+ { property::NUM_LINES, 8 },
+ { property::LINE_NAME, line_name(0, "foo") },
+ { property::LINE_NAME, line_name(2, "bar") },
+ { property::LINE_NAME, line_name(3, "baz") },
+ { property::LINE_NAME, line_name(5, "bar") }
+ });
+
+ ::gpiod::chip chip(sim.dev_path());
+
+ REQUIRE(chip.get_line_offset_from_name("bar") == 2);
+}
+
+TEST_CASE("closed chip can no longer be used", "[chip]")
+{
+ ::gpiosim::chip sim;
+
+ ::gpiod::chip chip(sim.dev_path());
+ chip.close();
+ REQUIRE_THROWS_AS(chip.path(), ::gpiod::chip_closed);
+}
+
+TEST_CASE("stream insertion operator works for chip", "[chip]")
+{
+ ::gpiosim::chip sim({
+ { property::NUM_LINES, 4 },
+ { property::LABEL, "foobar" }
+ });
+
+ ::gpiod::chip chip(sim.dev_path());
+ ::std::stringstream buf;
+
+ SECTION("open chip")
+ {
+ ::std::stringstream expected;
+
+ expected << "gpiod::chip(path=" << sim.dev_path() <<
+ ", info=gpiod::chip_info(name=\"" << sim.name() <<
+ "\", label=\"foobar\", num_lines=4))";
+
+ buf << chip;
+ REQUIRE_THAT(buf.str(), Catch::Equals(expected.str()));
+ }
+
+ SECTION("closed chip")
+ {
+ chip.close();
+ REQUIRE_THAT(chip, stringify_matcher<::gpiod::chip>("gpiod::chip(closed)"));
+ }
+}
+
+} /* namespace */
new file mode 100644
@@ -0,0 +1,417 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <chrono>
+#include <gpiod.hpp>
+#include <sstream>
+#include <thread>
+#include <utility>
+
+#include "gpiosim.hpp"
+#include "helpers.hpp"
+
+using simprop = ::gpiosim::chip::property;
+using reqprop = ::gpiod::request_config::property;
+using lineprop = ::gpiod::line_config::property;
+using direction = ::gpiod::line::direction;
+using edge = ::gpiod::line::edge;
+using offsets = ::gpiod::line::offsets;
+using pull = ::gpiosim::chip::pull;
+using event_type = ::gpiod::edge_event::event_type;
+
+namespace {
+
+TEST_CASE("edge_event_buffer capacity settings work", "[edge-event]")
+{
+ SECTION("default capacity")
+ {
+ REQUIRE(::gpiod::edge_event_buffer().capacity() == 64);
+ }
+
+ SECTION("user-defined capacity")
+ {
+ REQUIRE(::gpiod::edge_event_buffer(123).capacity() == 123);
+ }
+
+ SECTION("max capacity")
+ {
+ REQUIRE(::gpiod::edge_event_buffer(16 * 64 * 2).capacity() == 1024);
+ }
+}
+
+TEST_CASE("edge_event wait timeout", "[edge-event]")
+{
+ ::gpiosim::chip sim;
+ ::gpiod::chip chip(sim.dev_path());
+
+ auto request = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 0 })}
+ }),
+ ::gpiod::line_config({
+ { lineprop::EDGE, edge::BOTH }
+ })
+ );
+
+ REQUIRE_FALSE(request.wait_edge_event(::std::chrono::milliseconds(100)));
+}
+
+TEST_CASE("output mode and edge detection don't work together", "[edge-event]")
+{
+ ::gpiosim::chip sim;
+ ::gpiod::chip chip(sim.dev_path());
+
+ REQUIRE_THROWS_AS(
+ chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 0 })}
+ }),
+ ::gpiod::line_config({
+ { lineprop::DIRECTION, direction::OUTPUT },
+ { lineprop::EDGE, edge::BOTH }
+ })
+ ),
+ ::std::invalid_argument
+ );
+}
+
+void trigger_falling_and_rising_edge(::gpiosim::chip& sim, unsigned int offset)
+{
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+ sim.set_pull(offset, pull::PULL_UP);
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+ sim.set_pull(offset, pull::PULL_DOWN);
+}
+
+void trigger_rising_edge_events_on_two_offsets(::gpiosim::chip& sim,
+ unsigned int off0, unsigned int off1)
+{
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+ sim.set_pull(off0, pull::PULL_UP);
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+ sim.set_pull(off1, pull::PULL_UP);
+}
+
+TEST_CASE("waiting for and reading edge events works", "[edge-event]")
+{
+ ::gpiosim::chip sim({{ simprop::NUM_LINES, 8 }});
+ ::gpiod::chip chip(sim.dev_path());
+ ::gpiod::edge_event_buffer buffer;
+
+ SECTION("both edge events")
+ {
+ auto request = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 2 })}
+ }),
+ ::gpiod::line_config({
+ { lineprop::EDGE, edge::BOTH }
+ })
+ );
+
+ ::std::uint64_t ts_rising, ts_falling;
+
+ ::std::thread thread(trigger_falling_and_rising_edge, ::std::ref(sim), 2);
+
+ REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+ REQUIRE(request.read_edge_event(buffer, 1) == 1);
+ REQUIRE(buffer.num_events() == 1);
+ auto event = buffer.get_event(0);
+ REQUIRE(event.type() == event_type::RISING_EDGE);
+ REQUIRE(event.line_offset() == 2);
+ ts_rising = event.timestamp_ns();
+
+ REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+ REQUIRE(request.read_edge_event(buffer, 1) == 1);
+ REQUIRE(buffer.num_events() == 1);
+ event = buffer.get_event(0);
+ REQUIRE(event.type() == event_type::FALLING_EDGE);
+ REQUIRE(event.line_offset() == 2);
+ ts_falling = event.timestamp_ns();
+
+ REQUIRE_FALSE(request.wait_edge_event(::std::chrono::milliseconds(100)));
+
+ thread.join();
+
+ REQUIRE(ts_falling > ts_rising);
+ }
+
+ SECTION("rising edge event")
+ {
+ auto request = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 6 })}
+ }),
+ ::gpiod::line_config({
+ { lineprop::EDGE, edge::RISING }
+ })
+ );
+
+ ::std::thread thread(trigger_falling_and_rising_edge, ::std::ref(sim), 6);
+
+ REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+ REQUIRE(request.read_edge_event(buffer, 1) == 1);
+ REQUIRE(buffer.num_events() == 1);
+ auto event = buffer.get_event(0);
+ REQUIRE(event.type() == event_type::RISING_EDGE);
+ REQUIRE(event.line_offset() == 6);
+
+ REQUIRE_FALSE(request.wait_edge_event(::std::chrono::milliseconds(100)));
+
+ thread.join();
+ }
+
+ SECTION("falling edge event")
+ {
+ auto request = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 7 })}
+ }),
+ ::gpiod::line_config({
+ { lineprop::EDGE, edge::FALLING }
+ })
+ );
+
+ ::std::thread thread(trigger_falling_and_rising_edge, ::std::ref(sim), 7);
+
+ REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+ REQUIRE(request.read_edge_event(buffer, 1) == 1);
+ REQUIRE(buffer.num_events() == 1);
+ auto event = buffer.get_event(0);
+ REQUIRE(event.type() == event_type::FALLING_EDGE);
+ REQUIRE(event.line_offset() == 7);
+
+ REQUIRE_FALSE(request.wait_edge_event(::std::chrono::milliseconds(100)));
+
+ thread.join();
+ }
+
+ SECTION("sequence numbers")
+ {
+ auto request = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 0, 1 })}
+ }),
+ ::gpiod::line_config({
+ { lineprop::EDGE, edge::BOTH }
+ })
+ );
+
+ ::std::thread thread(trigger_rising_edge_events_on_two_offsets, ::std::ref(sim), 0, 1);
+
+ REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+ REQUIRE(request.read_edge_event(buffer, 1) == 1);
+ REQUIRE(buffer.num_events() == 1);
+ auto event = buffer.get_event(0);
+ REQUIRE(event.type() == event_type::RISING_EDGE);
+ REQUIRE(event.line_offset() == 0);
+ REQUIRE(event.global_seqno() == 1);
+ REQUIRE(event.line_seqno() == 1);
+
+ REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+ REQUIRE(request.read_edge_event(buffer, 1) == 1);
+ REQUIRE(buffer.num_events() == 1);
+ event = buffer.get_event(0);
+ REQUIRE(event.type() == event_type::RISING_EDGE);
+ REQUIRE(event.line_offset() == 1);
+ REQUIRE(event.global_seqno() == 2);
+ REQUIRE(event.line_seqno() == 1);
+
+ thread.join();
+ }
+}
+
+TEST_CASE("reading multiple events", "[edge-event]")
+{
+ ::gpiosim::chip sim({{ simprop::NUM_LINES, 8 }});
+ ::gpiod::chip chip(sim.dev_path());
+
+ auto request = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 1 })}
+ }),
+ ::gpiod::line_config({
+ { lineprop::EDGE, edge::BOTH }
+ })
+ );
+
+ unsigned long line_seqno = 1, global_seqno = 1;
+
+ sim.set_pull(1, pull::PULL_UP);
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+ sim.set_pull(1, pull::PULL_DOWN);
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+ sim.set_pull(1, pull::PULL_UP);
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+
+ SECTION("read multiple events")
+ {
+ ::gpiod::edge_event_buffer buffer;
+
+ REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+ REQUIRE(request.read_edge_event(buffer) == 3);
+ REQUIRE(buffer.num_events() == 3);
+
+ for (const auto& event: buffer) {
+ REQUIRE(event.line_offset() == 1);
+ REQUIRE(event.line_seqno() == line_seqno++);
+ REQUIRE(event.global_seqno() == global_seqno++);
+ }
+ }
+
+ SECTION("read over capacity")
+ {
+ ::gpiod::edge_event_buffer buffer(2);
+
+ REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+ REQUIRE(request.read_edge_event(buffer) == 2);
+ REQUIRE(buffer.num_events() == 2);
+ }
+}
+
+TEST_CASE("edge_event_buffer can be moved", "[edge-event]")
+{
+ ::gpiosim::chip sim({{ simprop::NUM_LINES, 2 }});
+ ::gpiod::chip chip(sim.dev_path());
+ ::gpiod::edge_event_buffer buffer(13);
+
+ /* Get some events into the buffer. */
+ auto request = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 1 })}
+ }),
+ ::gpiod::line_config({
+ { lineprop::EDGE, edge::BOTH }
+ })
+ );
+
+ sim.set_pull(1, pull::PULL_UP);
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+ sim.set_pull(1, pull::PULL_DOWN);
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+ sim.set_pull(1, pull::PULL_UP);
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(500));
+
+ REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+ REQUIRE(request.read_edge_event(buffer) == 3);
+
+ SECTION("move constructor works")
+ {
+ auto moved(::std::move(buffer));
+ REQUIRE(moved.capacity() == 13);
+ REQUIRE(moved.num_events() == 3);
+ }
+
+ SECTION("move assignment operator works")
+ {
+ ::gpiod::edge_event_buffer moved;
+
+ moved = ::std::move(buffer);
+ REQUIRE(moved.capacity() == 13);
+ REQUIRE(moved.num_events() == 3);
+ }
+}
+
+TEST_CASE("edge_event can be copied and moved", "[edge-event]")
+{
+ ::gpiosim::chip sim;
+ ::gpiod::chip chip(sim.dev_path());
+ ::gpiod::edge_event_buffer buffer;
+
+ auto request = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 0 })}
+ }),
+ ::gpiod::line_config({
+ { lineprop::EDGE, edge::BOTH }
+ })
+ );
+
+ sim.set_pull(0, pull::PULL_UP);
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+ REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+ REQUIRE(request.read_edge_event(buffer) == 1);
+ auto event = buffer.get_event(0);
+
+ sim.set_pull(0, pull::PULL_DOWN);
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+ REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+ REQUIRE(request.read_edge_event(buffer) == 1);
+ auto copy = buffer.get_event(0);
+
+ SECTION("copy constructor works")
+ {
+ auto copy(event);
+ REQUIRE(copy.line_offset() == 0);
+ REQUIRE(copy.type() == event_type::RISING_EDGE);
+ REQUIRE(event.line_offset() == 0);
+ REQUIRE(event.type() == event_type::RISING_EDGE);
+ }
+
+ SECTION("move constructor works")
+ {
+ auto copy(::std::move(event));
+ REQUIRE(copy.line_offset() == 0);
+ REQUIRE(copy.type() == event_type::RISING_EDGE);
+ }
+
+ SECTION("assignment operator works")
+ {
+ copy = event;
+ REQUIRE(copy.line_offset() == 0);
+ REQUIRE(copy.type() == event_type::RISING_EDGE);
+ REQUIRE(event.line_offset() == 0);
+ REQUIRE(event.type() == event_type::RISING_EDGE);
+ }
+
+ SECTION("move assignment operator works")
+ {
+ copy = ::std::move(event);
+ REQUIRE(copy.line_offset() == 0);
+ REQUIRE(copy.type() == event_type::RISING_EDGE);
+ }
+}
+
+TEST_CASE("stream insertion operators work for edge_event and edge_event_buffer", "[edge-event]")
+{
+ /*
+ * This tests the stream insertion operators for both edge_event and
+ * edge_event_buffer classes.
+ */
+
+ ::gpiosim::chip sim;
+ ::gpiod::chip chip(sim.dev_path());
+ ::gpiod::edge_event_buffer buffer;
+ ::std::stringstream sbuf, expected;
+
+ auto request = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 0 })}
+ }),
+ ::gpiod::line_config({
+ { lineprop::EDGE, edge::BOTH }
+ })
+ );
+
+ sim.set_pull(0, pull::PULL_UP);
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+ sim.set_pull(0, pull::PULL_DOWN);
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(30));
+
+ REQUIRE(request.wait_edge_event(::std::chrono::seconds(1)));
+ REQUIRE(request.read_edge_event(buffer) == 2);
+
+ sbuf << buffer;
+
+ expected << "gpiod::edge_event_buffer\\(num_events=2, capacity=64, events=\\[gpiod::edge_event\\" <<
+ "(type='RISING_EDGE', timestamp=[1-9][0-9]+, line_offset=0, global_seqno=1, " <<
+ "line_seqno=1\\), gpiod::edge_event\\(type='FALLING_EDGE', timestamp=[1-9][0-9]+, " <<
+ "line_offset=0, global_seqno=2, line_seqno=2\\)\\]\\)";
+
+ REQUIRE_THAT(sbuf.str(), regex_matcher(expected.str()));
+}
+
+} /* namespace */
new file mode 100644
@@ -0,0 +1,198 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <chrono>
+#include <gpiod.hpp>
+#include <sstream>
+#include <thread>
+#include <utility>
+
+#include "gpiosim.hpp"
+#include "helpers.hpp"
+
+using simprop = ::gpiosim::chip::property;
+using reqprop = ::gpiod::request_config::property;
+using lineprop = ::gpiod::line_config::property;
+using direction = ::gpiod::line::direction;
+using event_type = ::gpiod::info_event::event_type;
+
+namespace {
+
+void request_reconfigure_release_line(::gpiod::chip& chip)
+{
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+
+ auto request = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, ::gpiod::line::offsets({ 7 }) }
+ }),
+ ::gpiod::line_config()
+ );
+
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+
+ request.reconfigure_lines(
+ ::gpiod::line_config({
+ { lineprop::DIRECTION, direction::OUTPUT }
+ })
+ );
+
+ ::std::this_thread::sleep_for(::std::chrono::milliseconds(10));
+
+ request.release();
+}
+
+TEST_CASE("Lines can be watched", "[info-event][chip]")
+{
+ ::gpiosim::chip sim({{ simprop::NUM_LINES, 8 }});
+ ::gpiod::chip chip(sim.dev_path());
+
+ SECTION("watch_line_info() returns line info")
+ {
+ auto info = chip.watch_line_info(7);
+ REQUIRE(info.offset() == 7);
+ }
+
+ SECTION("watch_line_info() fails for offset out of range")
+ {
+ REQUIRE_THROWS_AS(chip.watch_line_info(8), ::std::invalid_argument);
+ }
+
+ SECTION("waiting for event timeout")
+ {
+ chip.watch_line_info(3);
+ REQUIRE_FALSE(chip.wait_info_event(::std::chrono::milliseconds(100)));
+ }
+
+ SECTION("request-reconfigure-release events")
+ {
+ auto info = chip.watch_line_info(7);
+ ::std::uint64_t ts_req, ts_rec, ts_rel;
+
+ REQUIRE(info.direction() == direction::INPUT);
+
+ ::std::thread thread(request_reconfigure_release_line, ::std::ref(chip));
+
+ REQUIRE(chip.wait_info_event(::std::chrono::seconds(1)));
+ auto event = chip.read_info_event();
+ REQUIRE(event.type() == event_type::LINE_REQUESTED);
+ REQUIRE(event.get_line_info().direction() == direction::INPUT);
+ ts_req = event.timestamp_ns();
+
+ REQUIRE(chip.wait_info_event(::std::chrono::seconds(1)));
+ event = chip.read_info_event();
+ REQUIRE(event.type() == event_type::LINE_CONFIG_CHANGED);
+ REQUIRE(event.get_line_info().direction() == direction::OUTPUT);
+ ts_rec = event.timestamp_ns();
+
+ REQUIRE(chip.wait_info_event(::std::chrono::seconds(1)));
+ event = chip.read_info_event();
+ REQUIRE(event.type() == event_type::LINE_RELEASED);
+ ts_rel = event.timestamp_ns();
+
+ /* No more events. */
+ REQUIRE_FALSE(chip.wait_info_event(::std::chrono::milliseconds(100)));
+ thread.join();
+
+ /* Check timestamps are really monotonic. */
+ REQUIRE(ts_rel > ts_rec);
+ REQUIRE(ts_rec > ts_req);
+ }
+}
+
+TEST_CASE("info_event can be copied and moved", "[info-event]")
+{
+ ::gpiosim::chip sim;
+ ::gpiod::chip chip(sim.dev_path());
+ ::std::stringstream buf, expected;
+
+ chip.watch_line_info(0);
+
+ auto request = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, ::gpiod::line::offsets({ 0 }) }
+ }),
+ ::gpiod::line_config()
+ );
+
+ REQUIRE(chip.wait_info_event(::std::chrono::seconds(1)));
+ auto event = chip.read_info_event();
+
+ request.release();
+
+ REQUIRE(chip.wait_info_event(::std::chrono::seconds(1)));
+ auto copy = chip.read_info_event();
+
+ SECTION("copy constructor works")
+ {
+ auto copy(event);
+
+ REQUIRE(copy.type() == event_type::LINE_REQUESTED);
+ REQUIRE(copy.get_line_info().offset() == 0);
+
+ REQUIRE(event.type() == event_type::LINE_REQUESTED);
+ REQUIRE(event.get_line_info().offset() == 0);
+ }
+
+ SECTION("assignment operator works")
+ {
+ copy = event;
+
+ REQUIRE(copy.type() == event_type::LINE_REQUESTED);
+ REQUIRE(copy.get_line_info().offset() == 0);
+
+ REQUIRE(event.type() == event_type::LINE_REQUESTED);
+ REQUIRE(event.get_line_info().offset() == 0);
+ }
+
+ SECTION("move constructor works")
+ {
+ auto copy(::std::move(event));
+
+ REQUIRE(copy.type() == event_type::LINE_REQUESTED);
+ REQUIRE(copy.get_line_info().offset() == 0);
+ }
+
+ SECTION("move assignment operator works")
+ {
+ copy = ::std::move(event);
+
+ REQUIRE(copy.type() == event_type::LINE_REQUESTED);
+ REQUIRE(copy.get_line_info().offset() == 0);
+ }
+}
+
+TEST_CASE("info_event stream insertion operator works", "[info-event][line-info]")
+{
+ /*
+ * This tests the stream insertion operator for both the info_event
+ * and line_info classes.
+ */
+
+ ::gpiosim::chip sim;
+ ::gpiod::chip chip(sim.dev_path());
+ ::std::stringstream buf, expected;
+
+ chip.watch_line_info(0);
+
+ auto request = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, ::gpiod::line::offsets({ 0 }) }
+ }),
+ ::gpiod::line_config()
+ );
+
+ auto event = chip.read_info_event();
+
+ buf << event;
+
+ expected << "gpiod::info_event\\(event_type='LINE_REQUESTED', timestamp=[1-9][0-9]+, " <<
+ "line_info=gpiod::line_info\\(offset=0, name=unnamed, used=true, consumer='', " <<
+ "direction=INPUT, active_low=false, bias=UNKNOWN, drive=PUSH_PULL, " <<
+ "edge_detection=NONE, event_clock=MONOTONIC, debounced=false\\)\\)";
+
+ REQUIRE_THAT(buf.str(), regex_matcher(expected.str()));
+}
+
+} /* namespace */
new file mode 100644
@@ -0,0 +1,270 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <gpiod.hpp>
+#include <sstream>
+
+#include "helpers.hpp"
+
+using lineprop = ::gpiod::line_config::property;
+using value = ::gpiod::line::value;
+using direction = ::gpiod::line::direction;
+using edge = ::gpiod::line::edge;
+using bias = ::gpiod::line::bias;
+using drive = ::gpiod::line::drive;
+using clock_type = ::gpiod::line::clock;
+using mappings = ::gpiod::line::value_mappings;
+using offsets = ::gpiod::line::offsets;
+
+using namespace ::std::chrono_literals;
+
+namespace {
+
+TEST_CASE("line_config constructor works", "[line-config]")
+{
+ SECTION("no arguments - default values")
+ {
+ ::gpiod::line_config cfg;
+
+ REQUIRE_NOTHROW(cfg.direction_default() == direction::INPUT);
+ REQUIRE(cfg.edge_detection_default() == edge::NONE);
+ REQUIRE(cfg.bias_default() == bias::AS_IS);
+ REQUIRE(cfg.drive_default() == drive::PUSH_PULL);
+ REQUIRE_FALSE(cfg.active_low_default());
+ REQUIRE(cfg.debounce_period_default() == 0us);
+ REQUIRE(cfg.event_clock_default() == clock_type::MONOTONIC);
+ REQUIRE(cfg.output_value_default() == value::INACTIVE);
+ REQUIRE(cfg.num_overrides() == 0);
+ REQUIRE(cfg.overrides().empty());
+ }
+
+ SECTION("default values set from constructor")
+ {
+ /*
+ * These are wrong and the request would fail but we're just
+ * testing the object's behavior.
+ */
+ ::gpiod::line_config cfg({
+ { lineprop::DIRECTION, direction::OUTPUT },
+ { lineprop::EDGE, edge::FALLING },
+ { lineprop::BIAS, bias::DISABLED },
+ { lineprop::DRIVE, drive::OPEN_DRAIN },
+ { lineprop::ACTIVE_LOW, true },
+ { lineprop::DEBOUNCE_PERIOD, 3000us },
+ { lineprop::EVENT_CLOCK, clock_type::REALTIME },
+ { lineprop::OUTPUT_VALUE, value::ACTIVE }
+ });
+
+ REQUIRE_NOTHROW(cfg.direction_default() == direction::OUTPUT);
+ REQUIRE(cfg.edge_detection_default() == edge::FALLING);
+ REQUIRE(cfg.bias_default() == bias::DISABLED);
+ REQUIRE(cfg.drive_default() == drive::OPEN_DRAIN);
+ REQUIRE(cfg.active_low_default());
+ /* Test implicit conversion between duration types. */
+ REQUIRE(cfg.debounce_period_default() == 3ms);
+ REQUIRE(cfg.event_clock_default() == clock_type::REALTIME);
+ REQUIRE(cfg.output_value_default() == value::ACTIVE);
+ REQUIRE(cfg.num_overrides() == 0);
+ REQUIRE(cfg.overrides().empty());
+ }
+
+ SECTION("output value overrides can be set from constructor")
+ {
+ ::gpiod::line_config cfg({
+ {
+ lineprop::OUTPUT_VALUES, mappings({
+ { 0, value::ACTIVE },
+ { 3, value::INACTIVE },
+ { 1, value::ACTIVE }
+ })
+ }
+ });
+
+ REQUIRE(cfg.num_overrides() == 3);
+ auto overrides = cfg.overrides();
+ REQUIRE(overrides[0].first == 0);
+ REQUIRE(overrides[0].second == lineprop::OUTPUT_VALUE);
+ REQUIRE(overrides[1].first == 3);
+ REQUIRE(overrides[1].second == lineprop::OUTPUT_VALUE);
+ REQUIRE(overrides[2].first == 1);
+ REQUIRE(overrides[2].second == lineprop::OUTPUT_VALUE);
+ }
+}
+
+TEST_CASE("line_config overrides work")
+{
+ ::gpiod::line_config cfg;
+
+ SECTION("direction")
+ {
+ cfg.set_direction_default(direction::AS_IS);
+ cfg.set_direction_override(direction::INPUT, 3);
+
+ REQUIRE(cfg.direction_is_overridden(3));
+ REQUIRE(cfg.direction_offset(3) == direction::INPUT);
+ cfg.clear_direction_override(3);
+ REQUIRE_FALSE(cfg.direction_is_overridden(3));
+ REQUIRE(cfg.direction_offset(3) == direction::AS_IS);
+ }
+
+ SECTION("edge detection")
+ {
+ cfg.set_edge_detection_default(edge::NONE);
+ cfg.set_edge_detection_override(edge::BOTH, 0);
+
+ REQUIRE(cfg.edge_detection_is_overridden(0));
+ REQUIRE(cfg.edge_detection_offset(0) == edge::BOTH);
+ cfg.clear_edge_detection_override(0);
+ REQUIRE_FALSE(cfg.edge_detection_is_overridden(0));
+ REQUIRE(cfg.edge_detection_offset(0) == edge::NONE);
+ }
+
+ SECTION("bias")
+ {
+ cfg.set_bias_default(bias::AS_IS);
+ cfg.set_bias_override(bias::PULL_DOWN, 3);
+
+ REQUIRE(cfg.bias_is_overridden(3));
+ REQUIRE(cfg.bias_offset(3) == bias::PULL_DOWN);
+ cfg.clear_bias_override(3);
+ REQUIRE_FALSE(cfg.bias_is_overridden(3));
+ REQUIRE(cfg.bias_offset(3) == bias::AS_IS);
+ }
+
+ SECTION("drive")
+ {
+ cfg.set_drive_default(drive::PUSH_PULL);
+ cfg.set_drive_override(drive::OPEN_DRAIN, 4);
+
+ REQUIRE(cfg.drive_is_overridden(4));
+ REQUIRE(cfg.drive_offset(4) == drive::OPEN_DRAIN);
+ cfg.clear_drive_override(4);
+ REQUIRE_FALSE(cfg.drive_is_overridden(4));
+ REQUIRE(cfg.drive_offset(4) == drive::PUSH_PULL);
+ }
+
+ SECTION("active-low")
+ {
+ cfg.set_active_low_default(false);
+ cfg.set_active_low_override(true, 16);
+
+ REQUIRE(cfg.active_low_is_overridden(16));
+ REQUIRE(cfg.active_low_offset(16));
+ cfg.clear_active_low_override(16);
+ REQUIRE_FALSE(cfg.active_low_is_overridden(16));
+ REQUIRE_FALSE(cfg.active_low_offset(16));
+ }
+
+ SECTION("debounce period")
+ {
+ /*
+ * Test the chrono literals and implicit duration conversions
+ * too.
+ */
+
+ cfg.set_debounce_period_default(5000us);
+ cfg.set_debounce_period_override(3ms, 1);
+
+ REQUIRE(cfg.debounce_period_is_overridden(1));
+ REQUIRE(cfg.debounce_period_offset(1) == 3ms);
+ cfg.clear_debounce_period_override(1);
+ REQUIRE_FALSE(cfg.debounce_period_is_overridden(1));
+ REQUIRE(cfg.debounce_period_offset(1) == 5ms);
+ }
+
+ SECTION("event clock")
+ {
+ cfg.set_event_clock_default(clock_type::MONOTONIC);
+ cfg.set_event_clock_override(clock_type::REALTIME, 4);
+
+ REQUIRE(cfg.event_clock_is_overridden(4));
+ REQUIRE(cfg.event_clock_offset(4) == clock_type::REALTIME);
+ cfg.clear_event_clock_override(4);
+ REQUIRE_FALSE(cfg.event_clock_is_overridden(4));
+ REQUIRE(cfg.event_clock_offset(4) == clock_type::MONOTONIC);
+ }
+
+ SECTION("output value")
+ {
+ cfg.set_output_value_default(value::INACTIVE);
+ cfg.set_output_value_override(value::ACTIVE, 0);
+ cfg.set_output_values({ 1, 2, 8 }, { value::ACTIVE, value::ACTIVE, value::ACTIVE });
+ cfg.set_output_values({ { 17, value::ACTIVE }, { 21, value::ACTIVE } });
+
+ for (const auto& off: offsets({ 0, 1, 2, 8, 17, 21 })) {
+ REQUIRE(cfg.output_value_is_overridden(off));
+ REQUIRE(cfg.output_value_offset(off) == value::ACTIVE);
+ cfg.clear_output_value_override(off);
+ REQUIRE_FALSE(cfg.output_value_is_overridden(off));
+ REQUIRE(cfg.output_value_offset(off) == value::INACTIVE);
+ }
+ }
+}
+
+TEST_CASE("line_config can be moved", "[line-config]")
+{
+ ::gpiod::line_config cfg({
+ { lineprop::DIRECTION, direction::INPUT },
+ { lineprop::EDGE, edge::BOTH },
+ { lineprop::DEBOUNCE_PERIOD, 3000us },
+ { lineprop::EVENT_CLOCK, clock_type::REALTIME },
+ });
+
+ cfg.set_direction_override(direction::OUTPUT, 2);
+ cfg.set_edge_detection_override(edge::NONE, 2);
+
+ SECTION("move constructor works")
+ {
+ auto moved(::std::move(cfg));
+
+ REQUIRE(moved.direction_default() == direction::INPUT);
+ REQUIRE(moved.edge_detection_default() == edge::BOTH);
+ REQUIRE(moved.debounce_period_default() == 3000us);
+ REQUIRE(moved.event_clock_default() == clock_type::REALTIME);
+ REQUIRE(moved.direction_offset(2) == direction::OUTPUT);
+ REQUIRE(moved.edge_detection_offset(2) == edge::NONE);
+ }
+
+ SECTION("move constructor works")
+ {
+ ::gpiod::line_config moved;
+
+ moved = ::std::move(cfg);
+
+ REQUIRE(moved.direction_default() == direction::INPUT);
+ REQUIRE(moved.edge_detection_default() == edge::BOTH);
+ REQUIRE(moved.debounce_period_default() == 3000us);
+ REQUIRE(moved.event_clock_default() == clock_type::REALTIME);
+ REQUIRE(moved.direction_offset(2) == direction::OUTPUT);
+ REQUIRE(moved.edge_detection_offset(2) == edge::NONE);
+ }
+}
+
+TEST_CASE("line_config stream insertion operator works", "[line-config]")
+{
+ ::gpiod::line_config cfg({
+ { lineprop::DIRECTION, direction::INPUT },
+ { lineprop::EDGE, edge::BOTH },
+ { lineprop::DEBOUNCE_PERIOD, 3000us },
+ { lineprop::EVENT_CLOCK, clock_type::REALTIME },
+ });
+
+ cfg.set_direction_override(direction::OUTPUT, 2);
+ cfg.set_edge_detection_override(edge::NONE, 2);
+
+ ::std::stringstream buf;
+
+ buf << cfg;
+
+ ::std::string expected(
+ "gpiod::line_config(defaults=(direction=INPUT, edge_detection=BOTH_EDGES, bias="
+ "AS_IS, drive=PUSH_PULL, active-high, debounce_period=3000us, event_clock="
+ "REALTIME, default_output_value=INACTIVE), overrides=[(offset=2 -> direction="
+ "OUTPUT), (offset=2 -> edge_detection=NONE)])"
+ );
+
+ REQUIRE_THAT(buf.str(), Catch::Equals(expected));
+}
+
+} /* namespace */
new file mode 100644
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <gpiod.hpp>
+#include <string>
+
+#include "helpers.hpp"
+#include "gpiosim.hpp"
+
+using property = ::gpiosim::chip::property;
+using line_name = ::gpiosim::chip::line_name;
+using line_hog = ::gpiosim::chip::line_hog;
+using hog_dir = ::gpiosim::chip::hog_direction;
+using direction = ::gpiod::line::direction;
+using edge = ::gpiod::line::edge;
+using bias = ::gpiod::line::bias;
+using drive = ::gpiod::line::drive;
+using event_clock = ::gpiod::line::clock;
+
+using namespace ::std::chrono_literals;
+
+namespace {
+
+TEST_CASE("get_line_info() works", "[chip][line-info]")
+{
+ ::gpiosim::chip sim({
+ { property::NUM_LINES, 8 },
+ { property::LINE_NAME, line_name(0, "foobar") },
+ { property::HOG, line_hog(0, "hog", hog_dir::OUTPUT_HIGH ) }
+ });
+
+ ::gpiod::chip chip(sim.dev_path());
+
+ SECTION("line_info can be retrieved from chip")
+ {
+ auto info = chip.get_line_info(0);
+
+ REQUIRE(info.offset() == 0);
+ REQUIRE_THAT(info.name(), Catch::Equals("foobar"));
+ REQUIRE(info.used());
+ REQUIRE_THAT(info.consumer(), Catch::Equals("hog"));
+ REQUIRE(info.direction() == ::gpiod::line::direction::OUTPUT);
+ REQUIRE_FALSE(info.active_low());
+ REQUIRE(info.bias() == ::gpiod::line::bias::UNKNOWN);
+ REQUIRE(info.drive() == ::gpiod::line::drive::PUSH_PULL);
+ REQUIRE(info.edge_detection() == ::gpiod::line::edge::NONE);
+ REQUIRE(info.event_clock() == ::gpiod::line::clock::MONOTONIC);
+ REQUIRE_FALSE(info.debounced());
+ REQUIRE(info.debounce_period() == 0us);
+ }
+
+ SECTION("offset out of range")
+ {
+ REQUIRE_THROWS_AS(chip.get_line_info(8), ::std::invalid_argument);
+ }
+}
+
+TEST_CASE("line properties can be retrieved", "[line-info]")
+{
+ ::gpiosim::chip sim({
+ { property::NUM_LINES, 8 },
+ { property::LINE_NAME, line_name(1, "foo") },
+ { property::LINE_NAME, line_name(2, "bar") },
+ { property::LINE_NAME, line_name(4, "baz") },
+ { property::LINE_NAME, line_name(5, "xyz") },
+ { property::HOG, line_hog(3, "hog3", hog_dir::OUTPUT_HIGH) },
+ { property::HOG, line_hog(4, "hog4", hog_dir::OUTPUT_LOW) }
+ });
+
+ ::gpiod::chip chip(sim.dev_path());
+
+ SECTION("basic properties")
+ {
+ auto info4 = chip.get_line_info(4);
+ auto info6 = chip.get_line_info(6);
+
+ REQUIRE(info4.offset() == 4);
+ REQUIRE_THAT(info4.name(), Catch::Equals("baz"));
+ REQUIRE(info4.used());
+ REQUIRE_THAT(info4.consumer(), Catch::Equals("hog4"));
+ REQUIRE(info4.direction() == direction::OUTPUT);
+ REQUIRE(info4.edge_detection() == edge::NONE);
+ REQUIRE_FALSE(info4.active_low());
+ REQUIRE(info4.bias() == bias::UNKNOWN);
+ REQUIRE(info4.drive() == drive::PUSH_PULL);
+ REQUIRE(info4.event_clock() == event_clock::MONOTONIC);
+ REQUIRE_FALSE(info4.debounced());
+ REQUIRE(info4.debounce_period() == 0us);
+ }
+}
+
+TEST_CASE("line_info can be copied and moved")
+{
+ ::gpiosim::chip sim({
+ { property::NUM_LINES, 4 },
+ { property::LINE_NAME, line_name(2, "foobar") }
+ });
+
+ ::gpiod::chip chip(sim.dev_path());
+ auto info = chip.get_line_info(2);
+
+ SECTION("copy constructor works")
+ {
+ auto copy(info);
+ REQUIRE(copy.offset() == 2);
+ REQUIRE_THAT(copy.name(), Catch::Equals("foobar"));
+ /* info can still be used */
+ REQUIRE(info.offset() == 2);
+ REQUIRE_THAT(info.name(), Catch::Equals("foobar"));
+ }
+
+ SECTION("assignment operator works")
+ {
+ auto copy = chip.get_line_info(0);
+ copy = info;
+ REQUIRE(copy.offset() == 2);
+ REQUIRE_THAT(copy.name(), Catch::Equals("foobar"));
+ /* info can still be used */
+ REQUIRE(info.offset() == 2);
+ REQUIRE_THAT(info.name(), Catch::Equals("foobar"));
+ }
+
+ SECTION("move constructor works")
+ {
+ auto copy(::std::move(info));
+ REQUIRE(copy.offset() == 2);
+ REQUIRE_THAT(copy.name(), Catch::Equals("foobar"));
+ }
+
+ SECTION("move assignment operator works")
+ {
+ auto copy = chip.get_line_info(0);
+ copy = ::std::move(info);
+ REQUIRE(copy.offset() == 2);
+ REQUIRE_THAT(copy.name(), Catch::Equals("foobar"));
+ }
+}
+
+TEST_CASE("line_info stream insertion operator works")
+{
+ ::gpiosim::chip sim({
+ { property::LINE_NAME, line_name(0, "foo") },
+ { property::HOG, line_hog(0, "hogger", hog_dir::OUTPUT_HIGH) }
+ });
+
+ ::gpiod::chip chip(sim.dev_path());
+
+ auto info = chip.get_line_info(0);
+
+ REQUIRE_THAT(info, stringify_matcher<::gpiod::line_info>(
+ "gpiod::line_info(offset=0, name='foo', used=true, consumer='foo', direction=OUTPUT, "
+ "active_low=false, bias=UNKNOWN, drive=PUSH_PULL, edge_detection=NONE, event_clock=MONOTONIC, debounced=false)"));
+}
+
+} /* namespace */
new file mode 100644
@@ -0,0 +1,490 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <gpiod.hpp>
+#include <sstream>
+#include <stdexcept>
+#include <vector>
+
+#include "gpiosim.hpp"
+#include "helpers.hpp"
+
+using simprop = ::gpiosim::chip::property;
+using reqprop = ::gpiod::request_config::property;
+using lineprop = ::gpiod::line_config::property;
+using offsets = ::gpiod::line::offsets;
+using values = ::gpiod::line::values;
+using direction = ::gpiod::line::direction;
+using value = ::gpiod::line::value;
+using simval = ::gpiosim::chip::value;
+using pull = ::gpiosim::chip::pull;
+
+namespace {
+
+class value_matcher : public Catch::MatcherBase<value>
+{
+public:
+ value_matcher(pull pull, bool active_low = false)
+ : _m_pull(pull),
+ _m_active_low(active_low)
+ {
+
+ }
+
+ ::std::string describe(void) const override
+ {
+ ::std::string repr(this->_m_pull == pull::PULL_UP ? "PULL_UP" : "PULL_DOWN");
+ ::std::string active_low = this->_m_active_low ? "(active-low) " : "";
+
+ return active_low + "corresponds with " + repr;
+ }
+
+ bool match(const value& val) const override
+ {
+ if (this->_m_active_low) {
+ if ((val == value::ACTIVE && this->_m_pull == pull::PULL_DOWN) ||
+ (val == value::INACTIVE && this->_m_pull == pull::PULL_UP))
+ return true;
+ } else {
+ if ((val == value::ACTIVE && this->_m_pull == pull::PULL_UP) ||
+ (val == value::INACTIVE && this->_m_pull == pull::PULL_DOWN))
+ return true;
+ }
+
+ return false;
+ }
+
+private:
+ pull _m_pull;
+ bool _m_active_low;
+};
+
+TEST_CASE("requesting lines fails with invalid arguments", "[line-request][chip]")
+{
+ ::gpiosim::chip sim({{ simprop::NUM_LINES, 8 }});
+ ::gpiod::chip chip(sim.dev_path());
+
+ SECTION("no offsets")
+ {
+ REQUIRE_THROWS_AS(chip.request_lines(::gpiod::request_config(),
+ ::gpiod::line_config()),
+ ::std::invalid_argument);
+ }
+
+ SECTION("duplicate offsets")
+ {
+ REQUIRE_THROWS_MATCHES(chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 2, 0, 0, 4 }) }
+ }),
+ ::gpiod::line_config()),
+ ::std::system_error,
+ system_error_matcher(EBUSY)
+ );
+ }
+
+ SECTION("offset out of bounds")
+ {
+ REQUIRE_THROWS_AS(chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 2, 0, 8, 4 }) }
+ }),
+ ::gpiod::line_config()),
+ ::std::invalid_argument
+ );
+ }
+}
+
+TEST_CASE("consumer string is set correctly", "[line-request]")
+{
+ ::gpiosim::chip sim({{ simprop::NUM_LINES, 4 }});
+ ::gpiod::chip chip(sim.dev_path());
+ offsets offs({ 3, 0, 2 });
+
+ SECTION("set custom consumer")
+ {
+ auto request = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 2 }) },
+ { reqprop::CONSUMER, "foobar" }
+ }),
+ ::gpiod::line_config()
+ );
+
+ auto info = chip.get_line_info(2);
+
+ REQUIRE(info.used());
+ REQUIRE_THAT(info.consumer(), Catch::Equals("foobar"));
+ }
+
+ SECTION("empty consumer")
+ {
+ auto request = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 2 }) },
+ }),
+ ::gpiod::line_config()
+ );
+
+ auto info = chip.get_line_info(2);
+
+ REQUIRE(info.used());
+ REQUIRE_THAT(info.consumer(), Catch::Equals("?"));
+ }
+}
+
+TEST_CASE("values can be read", "[line-request]")
+{
+ ::gpiosim::chip sim({{ simprop::NUM_LINES, 8 }});
+ const offsets offs({ 7, 1, 0, 6, 2 });
+
+ const ::std::vector<pull> pulls({
+ pull::PULL_UP,
+ pull::PULL_UP,
+ pull::PULL_DOWN,
+ pull::PULL_UP,
+ pull::PULL_DOWN
+ });
+
+ for (unsigned int i = 0; i < offs.size(); i++)
+ sim.set_pull(offs[i], pulls[i]);
+
+ auto request = ::gpiod::chip(sim.dev_path()).request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offs }
+ }),
+ ::gpiod::line_config({
+ { lineprop::DIRECTION, direction::INPUT }
+ })
+ );
+
+ SECTION("get all values (returning variant)")
+ {
+ auto vals = request.get_values();
+
+ REQUIRE_THAT(vals[0], value_matcher(pull::PULL_UP));
+ REQUIRE_THAT(vals[1], value_matcher(pull::PULL_UP));
+ REQUIRE_THAT(vals[2], value_matcher(pull::PULL_DOWN));
+ REQUIRE_THAT(vals[3], value_matcher(pull::PULL_UP));
+ REQUIRE_THAT(vals[4], value_matcher(pull::PULL_DOWN));
+ }
+
+ SECTION("get all values (passed buffer variant)")
+ {
+ values vals(5);
+
+ request.get_values(vals);
+
+ REQUIRE_THAT(vals[0], value_matcher(pull::PULL_UP));
+ REQUIRE_THAT(vals[1], value_matcher(pull::PULL_UP));
+ REQUIRE_THAT(vals[2], value_matcher(pull::PULL_DOWN));
+ REQUIRE_THAT(vals[3], value_matcher(pull::PULL_UP));
+ REQUIRE_THAT(vals[4], value_matcher(pull::PULL_DOWN));
+ }
+
+ SECTION("get_values(buffer) throws for invalid buffer size")
+ {
+ values vals(4);
+ REQUIRE_THROWS_AS(request.get_values(vals), ::std::invalid_argument);
+ vals.resize(6);
+ REQUIRE_THROWS_AS(request.get_values(vals), ::std::invalid_argument);
+ }
+
+ SECTION("get a single value")
+ {
+ auto val = request.get_value(7);
+
+ REQUIRE_THAT(val, value_matcher(pull::PULL_UP));
+ }
+
+ SECTION("get a single value (active-low)")
+ {
+ request.reconfigure_lines(
+ ::gpiod::line_config({
+ { lineprop::ACTIVE_LOW, true }
+ })
+ );
+
+ auto val = request.get_value(7);
+
+ REQUIRE_THAT(val, value_matcher(pull::PULL_UP, true));
+ }
+
+ SECTION("get a subset of values (returning variant)")
+ {
+ auto vals = request.get_values(offsets({ 2, 0, 6 }));
+
+ REQUIRE_THAT(vals[0], value_matcher(pull::PULL_DOWN));
+ REQUIRE_THAT(vals[1], value_matcher(pull::PULL_DOWN));
+ REQUIRE_THAT(vals[2], value_matcher(pull::PULL_UP));
+ }
+
+ SECTION("get a subset of values (passed buffer variant)")
+ {
+ values vals(3);
+
+ request.get_values(offsets({ 2, 0, 6 }), vals);
+
+ REQUIRE_THAT(vals[0], value_matcher(pull::PULL_DOWN));
+ REQUIRE_THAT(vals[1], value_matcher(pull::PULL_DOWN));
+ REQUIRE_THAT(vals[2], value_matcher(pull::PULL_UP));
+ }
+}
+
+TEST_CASE("output values can be set at request time", "[line-request]")
+{
+ ::gpiosim::chip sim({{ simprop::NUM_LINES, 8 }});
+ ::gpiod::chip chip(sim.dev_path());
+ const offsets offs({ 0, 1, 3, 4 });
+
+ ::gpiod::request_config req_cfg({
+ { reqprop::OFFSETS, offs }
+ });
+
+ ::gpiod::line_config line_cfg({
+ { lineprop::DIRECTION, direction::OUTPUT },
+ { lineprop::OUTPUT_VALUE, value::ACTIVE }
+ });
+
+ SECTION("default output value")
+ {
+ auto request = chip.request_lines(req_cfg, line_cfg);
+
+ for (const auto& off: offs)
+ REQUIRE(sim.get_value(off) == simval::ACTIVE);
+
+ REQUIRE(sim.get_value(2) == simval::INACTIVE);
+ }
+
+ SECTION("overridden output value")
+ {
+ line_cfg.set_output_value_override(value::INACTIVE, 1);
+
+ auto request = chip.request_lines(req_cfg, line_cfg);
+
+ REQUIRE(sim.get_value(0) == simval::ACTIVE);
+ REQUIRE(sim.get_value(1) == simval::INACTIVE);
+ REQUIRE(sim.get_value(2) == simval::INACTIVE);
+ REQUIRE(sim.get_value(3) == simval::ACTIVE);
+ REQUIRE(sim.get_value(4) == simval::ACTIVE);
+ }
+}
+
+TEST_CASE("values can be set after requesting lines", "[line-request]")
+{
+ ::gpiosim::chip sim({{ simprop::NUM_LINES, 8 }});
+ const offsets offs({ 0, 1, 3, 4 });
+
+ ::gpiod::request_config req_cfg({
+ { reqprop::OFFSETS, offs }
+ });
+
+ ::gpiod::line_config line_cfg({
+ { lineprop::DIRECTION, direction::OUTPUT },
+ { lineprop::OUTPUT_VALUE, value::INACTIVE }
+ });
+
+ auto request = ::gpiod::chip(sim.dev_path()).request_lines(req_cfg, line_cfg);
+
+ SECTION("set single value")
+ {
+ request.set_value(1, value::ACTIVE);
+
+ REQUIRE(sim.get_value(0) == simval::INACTIVE);
+ REQUIRE(sim.get_value(1) == simval::ACTIVE);
+ REQUIRE(sim.get_value(3) == simval::INACTIVE);
+ REQUIRE(sim.get_value(4) == simval::INACTIVE);
+ }
+
+ SECTION("set all values")
+ {
+ request.set_values({
+ value::ACTIVE,
+ value::INACTIVE,
+ value::ACTIVE,
+ value::INACTIVE
+ });
+
+ REQUIRE(sim.get_value(0) == simval::ACTIVE);
+ REQUIRE(sim.get_value(1) == simval::INACTIVE);
+ REQUIRE(sim.get_value(3) == simval::ACTIVE);
+ REQUIRE(sim.get_value(4) == simval::INACTIVE);
+ }
+
+ SECTION("set a subset of values")
+ {
+ request.set_values({ 4, 3 }, { value::ACTIVE, value::INACTIVE });
+
+ REQUIRE(sim.get_value(0) == simval::INACTIVE);
+ REQUIRE(sim.get_value(1) == simval::INACTIVE);
+ REQUIRE(sim.get_value(3) == simval::INACTIVE);
+ REQUIRE(sim.get_value(4) == simval::ACTIVE);
+ }
+
+ SECTION("set a subset of values with mappings")
+ {
+ request.set_values({
+ { 0, value::ACTIVE },
+ { 4, value::INACTIVE },
+ { 1, value::ACTIVE}
+ });
+
+ REQUIRE(sim.get_value(0) == simval::ACTIVE);
+ REQUIRE(sim.get_value(1) == simval::ACTIVE);
+ REQUIRE(sim.get_value(3) == simval::INACTIVE);
+ REQUIRE(sim.get_value(4) == simval::INACTIVE);
+ }
+}
+
+TEST_CASE("line_request can be moved", "[line-request]")
+{
+ ::gpiosim::chip sim({{ simprop::NUM_LINES, 8 }});
+ ::gpiod::chip chip(sim.dev_path());
+ const offsets offs({ 3, 1, 0, 2 });
+
+ auto request = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offs }
+ }),
+ ::gpiod::line_config()
+ );
+
+ auto fd = request.fd();
+
+ auto another = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 6 }) }
+ }),
+ ::gpiod::line_config()
+ );
+
+ SECTION("move constructor works")
+ {
+ auto moved(::std::move(request));
+
+ REQUIRE(moved.fd() == fd);
+ REQUIRE_THAT(moved.offsets(), Catch::Equals(offs));
+ }
+
+ SECTION("move assignment operator works")
+ {
+ another = ::std::move(request);
+
+ REQUIRE(another.fd() == fd);
+ REQUIRE_THAT(another.offsets(), Catch::Equals(offs));
+ }
+}
+
+TEST_CASE("released request can no longer be used", "[line-request]")
+{
+ ::gpiosim::chip sim;
+
+ auto request = ::gpiod::chip(sim.dev_path()).request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 0 }) }
+ }),
+ ::gpiod::line_config()
+ );
+
+ request.release();
+
+ REQUIRE_THROWS_AS(request.offsets(), ::gpiod::request_released);
+}
+
+TEST_CASE("line_request survives parent chip", "[line-request][chip]")
+{
+ ::gpiosim::chip sim;
+
+ sim.set_pull(0, pull::PULL_UP);
+
+ SECTION("chip is released")
+ {
+ ::gpiod::chip chip(sim.dev_path());
+
+ auto request = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 0 }) }
+ }),
+ ::gpiod::line_config({
+ { lineprop::DIRECTION, direction::INPUT }
+ })
+ );
+
+ REQUIRE_THAT(request.get_value(0), value_matcher(pull::PULL_UP));
+
+ chip.close();
+
+ REQUIRE_THAT(request.get_value(0), value_matcher(pull::PULL_UP));
+ }
+
+ SECTION("chip goes out of scope")
+ {
+ /* Need to get the request object somehow. */
+ ::gpiod::chip dummy(sim.dev_path());
+
+ auto request = dummy.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 0 }) }
+ }),
+ ::gpiod::line_config({
+ { lineprop::DIRECTION, direction::INPUT }
+ })
+ );
+
+ request.release();
+ dummy.close();
+
+ {
+ ::gpiod::chip chip(sim.dev_path());
+
+ request = chip.request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 0 }) }
+ }),
+ ::gpiod::line_config({
+ { lineprop::DIRECTION, direction::INPUT }
+ })
+ );
+
+ REQUIRE_THAT(request.get_value(0), value_matcher(pull::PULL_UP));
+ }
+
+ REQUIRE_THAT(request.get_value(0), value_matcher(pull::PULL_UP));
+ }
+}
+
+TEST_CASE("line_request stream insertion operator works", "[line-request]")
+{
+ ::gpiosim::chip sim({{ simprop::NUM_LINES, 4 }});
+
+ auto request = ::gpiod::chip(sim.dev_path()).request_lines(
+ ::gpiod::request_config({
+ { reqprop::OFFSETS, offsets({ 3, 1, 0, 2 }) }
+ }),
+ ::gpiod::line_config()
+ );
+
+ ::std::stringstream buf, expected;
+
+ expected << "gpiod::line_request(num_lines=4, line_offsets=gpiod::offsets(3, 1, 0, 2), fd=" <<
+ request.fd() << ")";
+
+ SECTION("active request")
+ {
+ buf << request;
+
+ REQUIRE_THAT(buf.str(), Catch::Equals(expected.str()));
+ }
+
+ SECTION("request released")
+ {
+ request.release();
+
+ buf << request;
+
+ REQUIRE_THAT(buf.str(), Catch::Equals("gpiod::line_request(released)"));
+ }
+}
+
+} /* namespace */
new file mode 100644
@@ -0,0 +1,137 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <gpiod.hpp>
+
+#include "helpers.hpp"
+
+using offset = ::gpiod::line::offset;
+using value = ::gpiod::line::value;
+using direction = ::gpiod::line::direction;
+using edge = ::gpiod::line::edge;
+using bias = ::gpiod::line::bias;
+using drive = ::gpiod::line::drive;
+using clock_type = ::gpiod::line::clock;
+using offsets = ::gpiod::line::offsets;
+using values = ::gpiod::line::values;
+using value_mapping = ::gpiod::line::value_mapping;
+using value_mappings = ::gpiod::line::value_mappings;
+
+namespace {
+
+TEST_CASE("stream insertion operators for types in gpiod::line work", "[line]")
+{
+ SECTION("offset")
+ {
+ offset off = 4;
+
+ REQUIRE_THAT(off, stringify_matcher<offset>("4"));
+ }
+
+ SECTION("value")
+ {
+ auto active = value::ACTIVE;
+ auto inactive = value::INACTIVE;
+
+ REQUIRE_THAT(active, stringify_matcher<value>("ACTIVE"));
+ REQUIRE_THAT(inactive, stringify_matcher<value>("INACTIVE"));
+ }
+
+ SECTION("direction")
+ {
+ auto input = direction::INPUT;
+ auto output = direction::OUTPUT;
+ auto as_is = direction::AS_IS;
+
+ REQUIRE_THAT(input, stringify_matcher<direction>("INPUT"));
+ REQUIRE_THAT(output, stringify_matcher<direction>("OUTPUT"));
+ REQUIRE_THAT(as_is, stringify_matcher<direction>("AS_IS"));
+ }
+
+ SECTION("edge")
+ {
+ auto rising = edge::RISING;
+ auto falling = edge::FALLING;
+ auto both = edge::BOTH;
+ auto none = edge::NONE;
+
+ REQUIRE_THAT(rising, stringify_matcher<edge>("RISING_EDGE"));
+ REQUIRE_THAT(falling, stringify_matcher<edge>("FALLING_EDGE"));
+ REQUIRE_THAT(both, stringify_matcher<edge>("BOTH_EDGES"));
+ REQUIRE_THAT(none, stringify_matcher<edge>("NONE"));
+ }
+
+ SECTION("bias")
+ {
+ auto pull_up = bias::PULL_UP;
+ auto pull_down = bias::PULL_DOWN;
+ auto disabled = bias::DISABLED;
+ auto as_is = bias::AS_IS;
+ auto unknown = bias::UNKNOWN;
+
+ REQUIRE_THAT(pull_up, stringify_matcher<bias>("PULL_UP"));
+ REQUIRE_THAT(pull_down, stringify_matcher<bias>("PULL_DOWN"));
+ REQUIRE_THAT(disabled, stringify_matcher<bias>("DISABLED"));
+ REQUIRE_THAT(as_is, stringify_matcher<bias>("AS_IS"));
+ REQUIRE_THAT(unknown, stringify_matcher<bias>("UNKNOWN"));
+ }
+
+ SECTION("drive")
+ {
+ auto push_pull = drive::PUSH_PULL;
+ auto open_drain = drive::OPEN_DRAIN;
+ auto open_source = drive::OPEN_SOURCE;
+
+ REQUIRE_THAT(push_pull, stringify_matcher<drive>("PUSH_PULL"));
+ REQUIRE_THAT(open_drain, stringify_matcher<drive>("OPEN_DRAIN"));
+ REQUIRE_THAT(open_source, stringify_matcher<drive>("OPEN_SOURCE"));
+ }
+
+ SECTION("clock")
+ {
+ auto monotonic = clock_type::MONOTONIC;
+ auto realtime = clock_type::REALTIME;
+
+ REQUIRE_THAT(monotonic, stringify_matcher<clock_type>("MONOTONIC"));
+ REQUIRE_THAT(realtime, stringify_matcher<clock_type>("REALTIME"));
+ }
+
+ SECTION("offsets")
+ {
+ offsets offs = { 2, 5, 3, 9, 8, 7 };
+
+ REQUIRE_THAT(offs, stringify_matcher<offsets>("gpiod::offsets(2, 5, 3, 9, 8, 7)"));
+ }
+
+ SECTION("values")
+ {
+ values vals = {
+ value::ACTIVE,
+ value::INACTIVE,
+ value::ACTIVE,
+ value::ACTIVE,
+ value::INACTIVE
+ };
+
+ REQUIRE_THAT(vals,
+ stringify_matcher<values>("gpiod::values(ACTIVE, INACTIVE, ACTIVE, ACTIVE, INACTIVE)"));
+ }
+
+ SECTION("value_mapping")
+ {
+ value_mapping val = { 4, value::ACTIVE };
+
+ REQUIRE_THAT(val, stringify_matcher<value_mapping>("gpiod::value_mapping(4: ACTIVE)"));
+ }
+
+ SECTION("value_mappings")
+ {
+ value_mappings vals = { { 0, value::ACTIVE }, { 4, value::INACTIVE }, { 8, value::ACTIVE } };
+
+ REQUIRE_THAT(vals, stringify_matcher<value_mappings>(
+ "gpiod::value_mappings(gpiod::value_mapping(0: ACTIVE), gpiod::value_mapping(4: INACTIVE), gpiod::value_mapping(8: ACTIVE))"));
+ }
+}
+
+} /* namespace */
new file mode 100644
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <filesystem>
+#include <gpiod.hpp>
+#include <string>
+#include <regex>
+#include <unistd.h>
+
+#include "gpiosim.hpp"
+#include "helpers.hpp"
+
+using property = ::gpiosim::chip::property;
+
+namespace {
+
+class symlink_guard
+{
+public:
+ symlink_guard(const ::std::filesystem::path& target,
+ const ::std::filesystem::path& link)
+ : _m_link(link)
+ {
+ ::std::filesystem::create_symlink(target, this->_m_link);
+ }
+
+ ~symlink_guard(void)
+ {
+ ::std::filesystem::remove(this->_m_link);
+ }
+
+private:
+ ::std::filesystem::path _m_link;
+};
+
+TEST_CASE("is_gpiochip_device() works", "[misc][chip]")
+{
+ SECTION("is_gpiochip_device() returns false for /dev/null")
+ {
+ REQUIRE_FALSE(::gpiod::is_gpiochip_device("/dev/null"));
+ }
+
+ SECTION("is_gpiochip_device() returns false for nonexistent file")
+ {
+ REQUIRE_FALSE(::gpiod::is_gpiochip_device("/dev/nonexistent"));
+ }
+
+ SECTION("is_gpiochip_device() returns true for a GPIO chip")
+ {
+ ::gpiosim::chip sim;
+
+ REQUIRE(::gpiod::is_gpiochip_device(sim.dev_path()));
+ }
+
+ SECTION("is_gpiochip_device() can resolve a symlink")
+ {
+ ::gpiosim::chip sim;
+ ::std::string link("/tmp/gpiod-cxx-tmp-link.");
+
+ link += ::std::to_string(::getpid());
+
+ symlink_guard link_guard(sim.dev_path(), link);
+
+ REQUIRE(::gpiod::is_gpiochip_device(link));
+ }
+}
+
+TEST_CASE("version_string() returns a valid API version", "[misc]")
+{
+ SECTION("check version_string() format")
+ {
+ REQUIRE_THAT(::gpiod::version_string(),
+ regex_matcher("^[0-9][1-9]?\\.[0-9][1-9]?([\\.0-9]?|\\-devel)$"));
+ }
+}
+
+} /* namespace */
new file mode 100644
@@ -0,0 +1,155 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// SPDX-FileCopyrightText: 2021-2022 Bartosz Golaszewski <brgl@bgdev.pl>
+
+#include <catch2/catch.hpp>
+#include <cstddef>
+#include <gpiod.hpp>
+#include <string>
+#include <sstream>
+
+#include "helpers.hpp"
+
+using property = ::gpiod::request_config::property;
+using offsets = ::gpiod::line::offsets;
+
+namespace {
+
+TEST_CASE("request_config constructor works", "[request-config]")
+{
+ SECTION("no arguments")
+ {
+ ::gpiod::request_config cfg;
+
+ REQUIRE(cfg.consumer().empty());
+ REQUIRE(cfg.offsets().empty());
+ REQUIRE(cfg.event_buffer_size() == 0);
+ }
+
+ SECTION("constructor with default settings")
+ {
+ offsets offsets({ 0, 1, 2, 3 });
+
+ ::gpiod::request_config cfg({
+ { property::CONSUMER, "foobar" },
+ { property::OFFSETS, offsets},
+ { property::EVENT_BUFFER_SIZE, 64 }
+ });
+
+ REQUIRE_THAT(cfg.consumer(), Catch::Equals("foobar"));
+ REQUIRE_THAT(cfg.offsets(), Catch::Equals(offsets));
+ REQUIRE(cfg.event_buffer_size() == 64);
+ }
+
+ SECTION("invalid default value types passed to constructor")
+ {
+ REQUIRE_THROWS_AS(::gpiod::request_config({
+ { property::CONSUMER, 42 }
+ }), ::std::invalid_argument);
+
+ REQUIRE_THROWS_AS(::gpiod::request_config({
+ { property::OFFSETS, 42 }
+ }), ::std::invalid_argument);
+
+ REQUIRE_THROWS_AS(::gpiod::request_config({
+ { property::EVENT_BUFFER_SIZE, "foobar" }
+ }), ::std::invalid_argument);
+ }
+}
+
+TEST_CASE("request_config can be moved", "[request-config]")
+{
+ offsets offsets({ 0, 1, 2, 3 });
+
+ ::gpiod::request_config cfg({
+ { property::CONSUMER, "foobar" },
+ { property::OFFSETS, offsets },
+ { property::EVENT_BUFFER_SIZE, 64 }
+ });
+
+ SECTION("move constructor works")
+ {
+ auto moved(::std::move(cfg));
+ REQUIRE_THAT(moved.consumer(), Catch::Equals("foobar"));
+ REQUIRE_THAT(moved.offsets(), Catch::Equals(offsets));
+ REQUIRE(moved.event_buffer_size() == 64);
+ }
+
+ SECTION("move assignment operator works")
+ {
+ ::gpiod::request_config moved;
+
+ moved = ::std::move(cfg);
+
+ REQUIRE_THAT(moved.consumer(), Catch::Equals("foobar"));
+ REQUIRE_THAT(moved.offsets(), Catch::Equals(offsets));
+ REQUIRE(moved.event_buffer_size() == 64);
+ }
+}
+
+TEST_CASE("request_config mutators work", "[request-config]")
+{
+ ::gpiod::request_config cfg;
+
+ SECTION("set consumer")
+ {
+ cfg.set_consumer("foobar");
+ REQUIRE_THAT(cfg.consumer(), Catch::Equals("foobar"));
+ }
+
+ SECTION("set offsets")
+ {
+ offsets offsets({ 3, 1, 2, 7, 5 });
+ cfg.set_offsets(offsets);
+ REQUIRE_THAT(cfg.offsets(), Catch::Equals(offsets));
+ }
+
+ SECTION("set event_buffer_size")
+ {
+ cfg.set_event_buffer_size(128);
+ REQUIRE(cfg.event_buffer_size() == 128);
+ }
+}
+
+TEST_CASE("request_config generic property setting works", "[request-config]")
+{
+ ::gpiod::request_config cfg;
+
+ SECTION("set consumer")
+ {
+ cfg.set_property(property::CONSUMER, "foobar");
+ REQUIRE_THAT(cfg.consumer(), Catch::Equals("foobar"));
+ }
+
+ SECTION("set offsets")
+ {
+ offsets offsets({ 3, 1, 2, 7, 5 });
+ cfg.set_property(property::OFFSETS, offsets);
+ REQUIRE_THAT(cfg.offsets(), Catch::Equals(offsets));
+ }
+
+ SECTION("set event_buffer_size")
+ {
+ cfg.set_property(property::EVENT_BUFFER_SIZE, 128);
+ REQUIRE(cfg.event_buffer_size() == 128);
+ }
+}
+
+TEST_CASE("request_config stream insertion operator works", "[request-config]")
+{
+ ::gpiod::request_config cfg({
+ { property::CONSUMER, "foobar" },
+ { property::OFFSETS, offsets({ 0, 1, 2, 3 })},
+ { property::EVENT_BUFFER_SIZE, 32 }
+ });
+
+ ::std::stringstream buf;
+
+ buf << cfg;
+
+ ::std::string expected("gpiod::request_config(consumer='foobar', num_offsets=4, "
+ "offsets=(gpiod::offsets(0, 1, 2, 3)), event_buffer_size=32)");
+
+ REQUIRE_THAT(buf.str(), Catch::Equals(expected));
+}
+
+} /* namespace */
This adds the tests for the v2 C++ bindings. Signed-off-by: Bartosz Golaszewski <brgl@bgdev.pl> --- bindings/cxx/tests/.gitignore | 4 + bindings/cxx/tests/Makefile.am | 31 ++ bindings/cxx/tests/check-kernel.cpp | 48 ++ bindings/cxx/tests/gpiod-cxx-test-main.cpp | 5 + bindings/cxx/tests/gpiosim.cpp | 258 +++++++++++ bindings/cxx/tests/gpiosim.hpp | 69 +++ bindings/cxx/tests/helpers.cpp | 37 ++ bindings/cxx/tests/helpers.hpp | 62 +++ bindings/cxx/tests/tests-chip-info.cpp | 109 +++++ bindings/cxx/tests/tests-chip.cpp | 171 +++++++ bindings/cxx/tests/tests-edge-event.cpp | 417 +++++++++++++++++ bindings/cxx/tests/tests-info-event.cpp | 198 ++++++++ bindings/cxx/tests/tests-line-config.cpp | 270 +++++++++++ bindings/cxx/tests/tests-line-info.cpp | 156 +++++++ bindings/cxx/tests/tests-line-request.cpp | 490 ++++++++++++++++++++ bindings/cxx/tests/tests-line.cpp | 137 ++++++ bindings/cxx/tests/tests-misc.cpp | 78 ++++ bindings/cxx/tests/tests-request-config.cpp | 155 +++++++ 18 files changed, 2695 insertions(+) create mode 100644 bindings/cxx/tests/.gitignore create mode 100644 bindings/cxx/tests/Makefile.am create mode 100644 bindings/cxx/tests/check-kernel.cpp create mode 100644 bindings/cxx/tests/gpiod-cxx-test-main.cpp create mode 100644 bindings/cxx/tests/gpiosim.cpp create mode 100644 bindings/cxx/tests/gpiosim.hpp create mode 100644 bindings/cxx/tests/helpers.cpp create mode 100644 bindings/cxx/tests/helpers.hpp create mode 100644 bindings/cxx/tests/tests-chip-info.cpp create mode 100644 bindings/cxx/tests/tests-chip.cpp create mode 100644 bindings/cxx/tests/tests-edge-event.cpp create mode 100644 bindings/cxx/tests/tests-info-event.cpp create mode 100644 bindings/cxx/tests/tests-line-config.cpp create mode 100644 bindings/cxx/tests/tests-line-info.cpp create mode 100644 bindings/cxx/tests/tests-line-request.cpp create mode 100644 bindings/cxx/tests/tests-line.cpp create mode 100644 bindings/cxx/tests/tests-misc.cpp create mode 100644 bindings/cxx/tests/tests-request-config.cpp