diff --git a/.github/skills/cpp-modernize/SKILL.md b/.github/skills/cpp-modernize/SKILL.md deleted file mode 100644 index 710d338..0000000 --- a/.github/skills/cpp-modernize/SKILL.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -name: cpp-modernize -description: "Provide expert C++23 modernization advice, idiomatic refactorings, and examples that reference cppreference.com. Use when a user asks to modernize, refactor, or explain C++ code; always target C++23 and cite cppreference.com for language and library details." ---- - -# cpp-modernize - -This skill provides senior-level guidance for modernizing C++ code to idiomatic, high-performance C++23. Use it when you want concrete refactorings, API recommendations, and short, precise code examples. - -## When to use - -- Ask to modernize legacy or pre-C++11/14 code to C++23 idioms. -- Request replacements for deprecated or unsafe constructs (e.g., raw pointers, manual resource management, `auto_ptr`). -- Ask for performance-minded rewrites that prefer expressive standard library components (ranges, algorithms, `std::span`, `std::expected`, `std::format`, etc.). -- Request explanations of C++23 features with example code and references to cppreference.com. - -## Quick rules and non-negotiables - -- **Target standard:** All examples and guidance must assume **C++23**. Use features from C++20/C++23 only when they help clarity, safety, or performance. -- **Authoritative reference:** Always reference the relevant cppreference.com page(s) for the constructs or library components used (e.g., [cppreference - ranges](https://en.cppreference.com/w/cpp/ranges) for ranges). Cite them inline or in a short reference section. -- **Include examples:** Provide short, self-contained example snippets that compile under C++23 and illustrate the suggested change. -- **Prefer standard solutions:** Prefer standard library facilities before third-party libraries unless the latter are explicitly requested. -- **Explain trade-offs briefly:** For each suggestion, mention at most one performance or compatibility trade-off. -- **No code generation beyond examples:** Don’t produce large generated codebases—provide concise examples and clear instructions for application. - -## Output requirements - -- Include a short explanation (1–3 sentences). -- Provide a code snippet demonstrating the fix or modernization (annotated with one-line comments when helpful). -- Add a one-line reference link to the cppreference.com page that justifies the recommended feature or pattern. - -## References - -- See `references/guidelines.md` for detailed style notes, code style rules, and formatting conventions. -- See `references/examples.md` for sample prompts and expected outputs. diff --git a/.github/skills/cpp-modernize/references/examples.md b/.github/skills/cpp-modernize/references/examples.md deleted file mode 100644 index a2b1a06..0000000 --- a/.github/skills/cpp-modernize/references/examples.md +++ /dev/null @@ -1,237 +0,0 @@ -# Examples - -Input: - -```text -Modernize this function that concatenates strings using `sprintf` and raw buffers. - -void join(const char** parts, size_t count) { - char buf[1024]; - buf[0] = '\0'; - for (size_t i = 0; i < count; ++i) { - strcat(buf, parts[i]); - if (i + 1 < count) strcat(buf, ","); - } - puts(buf); -} -``` - -Expected output: - -```text -Summary: Replace unsafe C string handling with `std::string`/`std::string_view` and standard algorithms. - - #include - #include - #include - - void join(std::span parts) { - std::string out; - for (auto sv : parts) out += sv; - std::cout << out << '\n'; - } - -Reference: [cppreference - basic_string](https://en.cppreference.com/w/cpp/string/basic_string) -``` - -Input: - -```text -Suggest a replacement for this C-style loop that copies into a std::vector: - -void copy(const int* arr, size_t n) { - std::vector v; - for (size_t i = 0; i < n; ++i) v.push_back(arr[i]); -} -``` - -Expected output: - -```text -Summary: Use iterator- or range-based construction to avoid manual loops. - - #include - #include - - void copy(const int* arr, size_t n) { - std::vector v(std::ranges::begin(std::span(arr, n)), std::ranges::end(std::span(arr, n))); - } - -Reference: [cppreference - ranges](https://en.cppreference.com/w/cpp/ranges) -``` - -Input: - -```text -Modernize this function that takes a pointer and length into a safer interface: - -void process(const int* data, size_t n) { - for (size_t i = 0; i < n; ++i) handle(data[i]); -} -``` - -Expected output: - -```text -Summary: Use `std::span` for non-owning views and range-based iteration for clarity and safety. - - #include - - void process(std::span data) { - for (int v : data) handle(v); // range-based loop - } - -Reference: [cppreference - span](https://en.cppreference.com/w/cpp/container/span) -``` - -Input: - -```text -Suggest a modern return type for this function that uses an out-parameter and a bool success return: - -bool load_config(Config& out) { - if (!file_exists()) return false; - out = ...; // load config - return true; -} -``` - -Expected output: - -```text -Summary: Return `std::expected` to encode success or a recoverable error rather than using out-params and bool. - - #include - #include - - struct Error { int code; std::string message; }; - - std::expected load_config() { - if (!file_exists()) return std::unexpected(Error{1, "file missing"}); - return Config{/*...*/}; - } - -Reference: [cppreference - expected](https://en.cppreference.com/w/cpp/utility/expected) -``` - -Input: - -```text -Replace this snprintf usage with a modern alternative: - -char buf[128]; -snprintf(buf, sizeof buf, "user=%s id=%d", name, id); -puts(buf); -``` - -Expected output: - -```text -Summary: Use `std::format` for type-safe formatting and clearer intent; note C++23 toolchain flag may be required. - - #include - #include - - auto s = std::format("user={} id={}", name, id); - std::puts(s.c_str()); - -Reference: [cppreference - format](https://en.cppreference.com/w/cpp/utility/format) -``` - -Input: - -```text -Show a simple coroutine-based producer that yields integers lazily. - -/* Show a minimal generator-like coroutine */ -// generator counter() { for (int i = 0; i < 3; ++i) co_yield i; } -``` - -Expected output: - -```text -Summary: Use coroutines for lazy generators and asynchronous flows; show a minimal generator example (requires C++20 coroutine support). - - #include - #include - - // Minimal generator-like coroutine (illustrative) - struct generator { - struct promise_type { - std::optional value_; - generator get_return_object() { return generator{}; } - std::suspend_always initial_suspend() { return {}; } - std::suspend_always final_suspend() noexcept { return {}; } - std::suspend_always yield_value(int v) { value_ = v; return {}; } - void return_void() {} - void unhandled_exception() { std::terminate(); } - }; - }; - -Reference: [cppreference - coroutines](https://en.cppreference.com/w/cpp/language/coroutines) -``` - -Input: - -```text -Show how to use Concepts to constrain templates, e.g., an Incrementable concept. - -template concept Incrementable = requires(T a) { ++a; }; -``` - -Expected output: - -```text -Summary: Use Concepts to express template requirements and improve diagnostics; show a simple `Incrementable` example. - - template - concept Incrementable = requires(T a) { ++a; }; - - template - T inc(T x) { ++x; return x; } - -Reference: [cppreference - concepts](https://en.cppreference.com/w/cpp/language/concepts) -``` - -Input: - -```text -Rewrite this using a ranges pipeline with filter and transform: - -std::vector in = ...; std::vector out; for (int v : in) if (v%2==0) out.push_back(v*2); -``` - -Expected output: - -```text -Summary: Use `std::ranges` pipelines (`views::filter` and `views::transform`) for clear, composable transformations. - - #include - - auto evens_doubled = in | std::views::filter([](int v){ return v % 2 == 0; }) - | std::views::transform([](int v){ return v * 2; }); - - for (int v : evens_doubled) process(v); - -Reference: [cppreference - ranges](https://en.cppreference.com/w/cpp/ranges) -``` - -Input: - -```text -Show an advanced `std::format` example: padded float and hex formatting. - -float val = 3.14159; int id = 255; std::string name = "bob"; -``` - -Expected output: - -```text -Summary: Use `std::format` format specifiers for alignment, precision and integer formats (e.g., hex with zero-padding). - - #include - auto s = std::format("{:<10} id={:04X} val={:8.2f}", name, id, val); // name left-aligned, id hex padded, val width+precision - std::puts(s.c_str()); - -Reference: [cppreference - format](https://en.cppreference.com/w/cpp/utility/format) -``` diff --git a/.github/skills/cpp-modernize/references/guidelines.md b/.github/skills/cpp-modernize/references/guidelines.md deleted file mode 100644 index e0059bf..0000000 --- a/.github/skills/cpp-modernize/references/guidelines.md +++ /dev/null @@ -1,175 +0,0 @@ ---- -name: cpp-modernize-guidelines -description: "Guidelines for the `cpp-modernize` skill: how to answer, formatting rules, and C++ style rules used by examples and advice." ---- - -# Guidelines - -This file explains how `cpp-modernize` should respond and what it must include, plus a consolidated set of C++ code style rules used for examples and recommendations. These rules are intentionally **toolchain- and project-agnostic** and do not reference third-party or project-specific libraries. - -## Core rules - -- **Always target C++23.** Use modern language and library features (concepts, ranges, `std::span`, `std::format`, `std::expected`, etc.) only when they clarify intent, improve safety, or measurably improve performance. -- **Cite authoritative references.** For language or library features, include a cppreference.com link where relevant (e.g., [cppreference - ranges](https://en.cppreference.com/w/cpp/ranges)). -- **Small, compilable examples.** Examples must be minimal, self-contained, and valid C++23. Prefer 10–30 lines and use short one-line comments for clarity. -- **Explain trade-offs briefly.** Mention at most one trade-off (binary size, compile-time, toolchain requirement) when applicable. -- **Avoid environment assumptions.** If a suggestion requires flags or extra libraries (e.g., `-std=c++23`), mention it briefly. - ---- - -## Code formatting & layout - -- Filenames should be lowercase; underscores are allowed to separate words. -- C++ source files commonly use `.cxx` or `.cpp`; headers use `.h` or `.hpp`. -- Files must end with a single newline character. -- **Maximum line length:** 90 characters. -- **Indentation:** 4 spaces; do not use tabs. -- Separate top-level constructs (classes, functions, namespaces) with a single blank line. -- Group and sort `#include` directives, separated by a blank line between groups. Recommended order: - 1. Current file's header - 2. Project headers - 3. External library headers - 4. C++ standard library headers - 5. C standard library headers -- Use `#pragma once` for header guards. - -### Spacing & braces - -- The bodies of `if`, `for`, and other control blocks must always be enclosed in braces: - -```cpp -if (a == b) { - do_something(); -} -``` - -- The opening brace of a function or method is separated from the signature by a space and remains on the same line. Empty function bodies are `{}` without a line break. - -```cpp -void func_foo(Arg& a) { - // ... -} - -Foo::Foo() {} -``` - -- `case` labels align with `switch`; indent the body of each `case`. - -### Asterisk/ampersand and const placement - -- Put `*` and `&` adjacent to the type, not the variable name: - -```cpp -int* ptr; -int& ref = value; -``` - -- `const` should precede the type: - -```cpp -const int* ptr; -const int& ref; -``` - -### Literal & initialization style - -- Floating-point literals include a decimal point and an appropriate suffix for `float` (e.g., `1.0f`). -- Prefer brace-initialization for fields and complex objects (`Type var{...};`). -- Initialize simple constants and variables using `=` for clarity (`auto val = 10;`). - ---- - -## Naming conventions - -- Types (structs, classes, enum classes, template parameters): `CamelCase`. -- Functions, methods, public members, and non-member functions: `snake_case`. -- Private member variables: `snake_case_` (trailing underscore). -- Local variables and parameters: `snake_case`. -- Compile-time constants: `UPPER_CASE`. -- Local non-compile-time constants: `snake_case`. -- Namespaces: lowercase, single word. - ---- - -## Functions & complexity - -- Keep functions short and focused where practical: - - Recommended maximum function length: ~40 lines. - - Recommended maximum nesting depth: 4 levels. - - Recommended maximum parameters: 5 (use a struct for more). - - Recommended maximum local variables: 10. -- Separate logical blocks within functions with a blank line to improve readability. -- Prefer `inline` functions for small utilities where appropriate. - ---- - -## Conditionals & loops - -- Avoid overly complex conditionals; use boolean variables to express intent when helpful. -- Parenthesize sub-expressions for clarity. -- Prefer `switch` over long `if-else` chains when applicable. -- Do not put unrelated expressions in the `for (...)` header. -- For infinite loops, prefer `while (true)`. - ---- - -## Classes & members - -- Use `struct` for plain aggregates; use `class` when behavior or encapsulation is required. -- Mark single-argument constructors `explicit` unless implicit conversion is desired. -- When overriding virtual functions, mark them `override` (or `final` where appropriate). -- Order class sections consistently (recommended): types and private fields, public API, protected, private methods. Keep declaration order consistent with definitions. -- Initialize all members in constructors and prefer brace-initialization for fields: `uint32_t field_{123};`. -- Access specifiers (public/protected/private) appear without indentation and should be separated by a blank line from other sections. - ---- - -## Types & integers - -- Prefer fixed-width integer types from `` (`int32_t`, `uint64_t`) for interfaces that need precise widths. -- Standard types like `size_t` and `ptrdiff_t` remain acceptable for their intended uses. -- Prefer `nullptr` over `NULL`. - ---- - -## Comments & documentation - -- Comments should start with a capital letter. For single-sentence comments, omit the final period. -- Use single-line `//` comments with a single space after `//`. -- Use `// TODO: ` for short work-in-progress notes. - ---- - -## Macros, RTTI & other constructs - -- Prefer not to use macros; prefer `constexpr`, `inline` functions, `enum`s, and templates instead. -- If macros are required, keep them local, `#undef` them after use, avoid side effects, and favor uppercase names with a unique prefix. -- Avoid RTTI and user-defined literals unless there is a specific, well-justified need. -- Use of `goto` is discouraged. - ---- - -## Example: short style snippets - -```cpp -// Namespace (no indentation inside namespace) -namespace mylib { - -class Foo { -public: - Foo() = default; - - void do_work() { /* short function */ } - -private: - int counter_{}; -}; - -} // namespace mylib -``` - ---- - -## References - -- Use cppreference.com for language or library feature references when suggesting code changes (e.g., [cppreference - ranges](https://en.cppreference.com/w/cpp/ranges)). diff --git a/.github/skills/cpp-modernize/scripts/mock_runner.py b/.github/skills/cpp-modernize/scripts/mock_runner.py deleted file mode 100644 index 5bfe721..0000000 --- a/.github/skills/cpp-modernize/scripts/mock_runner.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env python3 -"""Minimal mock runner for `cpp-modernize` examples. - -It applies a small, deterministic transformation to input examples for smoke tests. -""" - -import sys - - -def modernize_text(s: str) -> str: - s = s.strip() - if "sprintf" in s or "strcat" in s: - return ( - "Summary: Replace unsafe C string handling with `std::string`/`std::string_view` and standard algorithms.\n\n" - " #include \n #include \n #include \n\n void join(std::span parts) {\n std::string out;\n for (auto sv : parts) out += sv;\n std::cout << out << '\\n';\n }\n\n" - "Reference: [cppreference - basic_string](https://en.cppreference.com/w/cpp/string/basic_string)" - ) - if "snprintf" in s: - return ( - "Summary: Use `std::format` for type-safe formatting and clearer intent; note C++23 toolchain flag may be required.\n\n" - ' #include \n #include \n\n auto s = std::format("user={} id={}", name, id);\n std::puts(s.c_str());\n\n' - "Reference: [cppreference - format](https://en.cppreference.com/w/cpp/utility/format)" - ) - if "const int* data, size_t n" in s or "pointer and length" in s: - return ( - "Summary: Use `std::span` for non-owning views and range-based iteration for clarity and safety.\n\n" - " #include \n\n void process(std::span data) {\n for (int v : data) handle(v); // range-based loop\n }\n\n" - "Reference: [cppreference - span](https://en.cppreference.com/w/cpp/container/span)" - ) - if "load_config(Config& out)" in s or "out-parameter" in s: - return ( - "Summary: Return `std::expected` to encode success or a recoverable error rather than using out-params and bool.\n\n" - ' #include \n #include \n\n struct Error { int code; std::string message; };\n\n std::expected load_config() {\n if (!file_exists()) return std::unexpected(Error{1, "file missing"});\n return Config{/*...*/};\n }\n\n' - "Reference: [cppreference - expected](https://en.cppreference.com/w/cpp/utility/expected)" - ) - if "coroutine" in s or "co_yield" in s: - return ( - "Summary: Use coroutines for lazy generators and asynchronous flows; show a minimal generator example (requires C++20 coroutine support).\n\n" - " #include \n #include \n\n // Minimal generator-like coroutine (illustrative)\n struct generator {\n struct promise_type {\n std::optional value_;\n generator get_return_object() { return generator{}; }\n std::suspend_always initial_suspend() { return {}; }\n std::suspend_always final_suspend() noexcept { return {}; }\n std::suspend_always yield_value(int v) { value_ = v; return {}; }\n void return_void() {}\n void unhandled_exception() { std::terminate(); }\n };\n };\n\n" - "Reference: [cppreference - coroutines](https://en.cppreference.com/w/cpp/language/coroutines)" - ) - if "concept" in s or "Incrementable" in s: - return ( - "Summary: Use Concepts to express template requirements and improve diagnostics; show a simple `Incrementable` example.\n\n" - " template\n concept Incrementable = requires(T a) { ++a; };\n\n template\n T inc(T x) { ++x; return x; }\n\n" - "Reference: [cppreference - concepts](https://en.cppreference.com/w/cpp/language/concepts)" - ) - if "views::filter" in s or "ranges pipeline" in s: - return ( - "Summary: Use `std::ranges` pipelines (`views::filter` and `views::transform`) for clear, composable transformations.\n\n" - " #include \n\n auto evens_doubled = in | std::views::filter([](int v){ return v % 2 == 0; })\n | std::views::transform([](int v){ return v * 2; });\n\n for (int v : evens_doubled) process(v);\n\n" - "Reference: [cppreference - ranges](https://en.cppreference.com/w/cpp/ranges)" - ) - if ( - "floating" in s - or "precision" in s - or "advanced std::format" in s - or "padded" in s - or "hex" in s - or "std::format" in s - ): - return ( - "Summary: Use `std::format` format specifiers for alignment, precision and integer formats (e.g., hex with zero-padding).\n\n" - ' #include \n auto s = std::format("{:<10} id={:04X} val={:8.2f}", name, id, val); // name left-aligned, id hex padded, val width+precision\n std::puts(s.c_str());\n\n' - "Reference: [cppreference - format](https://en.cppreference.com/w/cpp/utility/format)" - ) - if "const int* data, size_t n" in s or "pointer and length" in s: - return ( - "Summary: Use `std::span` for non-owning views and range-based iteration for clarity and safety.\n\n" - "```cpp\n#include \n\nvoid process(std::span data) {\n for (int v : data) handle(v); // range-based loop\n}\n```\n\n" - "Reference: [cppreference - span](https://en.cppreference.com/w/cpp/container/span)" - ) - if "load_config(Config& out)" in s or "out-parameter" in s: - return ( - "Summary: Return `std::expected` to encode success or a recoverable error rather than using out-params and bool.\n\n" - '```cpp\n#include \n#include \n\nstruct Error { int code; std::string message; };\n\nstd::expected load_config() {\n if (!file_exists()) return std::unexpected(Error{1, "file missing"});\n return Config{/*...*/};\n}\n```\n\n' - "Reference: [cppreference - expected](https://en.cppreference.com/w/cpp/utility/expected)" - ) - if "for (size_t i = 0; i < n; ++i) v.push_back(arr[i]);" in s: - return ( - "Summary: Use iterator- or range-based construction to avoid manual loops.\n\n" - " #include \n #include \n\n void copy(const int* arr, size_t n) {\n std::vector v(std::ranges::begin(std::span(arr, n)), std::ranges::end(std::span(arr, n)));\n }\n\n" - "Reference: [cppreference - ranges](https://en.cppreference.com/w/cpp/ranges)" - ) - return s - - -if __name__ == "__main__": - data = sys.stdin.read() - out = modernize_text(data) - sys.stdout.write(out) diff --git a/.github/skills/cpp-modernize/scripts/validate_examples.py b/.github/skills/cpp-modernize/scripts/validate_examples.py deleted file mode 100644 index 2663479..0000000 --- a/.github/skills/cpp-modernize/scripts/validate_examples.py +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env python3 -"""Smoke-test runner for the `cpp-modernize` skill examples. - -Usage: - python validate_examples.py --examples ../references/examples.md --runner "python mock_runner.py" - -The script expects a runner command that reads text from stdin and writes the skill's output to stdout. -Exit code: 0 if all tests pass, non-zero if any test fails. -""" - -import argparse -import re -import subprocess -import sys -from difflib import unified_diff - -RE_INPUT = re.compile(r"Input:\s*```text\n(.*?)\n```", re.S) -RE_EXPECTED = re.compile(r"Expected output:\s*```text\n(.*?)\n```", re.S) - -# type: ignore - - -def parse_examples(path): - txt = open(path, "r", encoding="utf-8").read() - inputs = RE_INPUT.findall(txt) - expecteds = RE_EXPECTED.findall(txt) - if len(inputs) != len(expecteds): - raise SystemExit( - f"Error parsing examples: found {len(inputs)} inputs but {len(expecteds)} expected outputs" - ) - examples = [] - for i, (inp, exp) in enumerate(zip(inputs, expecteds), start=1): - examples.append({"id": i, "input": inp.strip(), "expected": exp.strip()}) - return examples - - -def run_runner(cmd, input_text, timeout=10): - proc = subprocess.run( - cmd, - input=input_text.encode("utf-8"), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=True, - timeout=timeout, - ) - stdout = proc.stdout.decode("utf-8").strip() - stderr = proc.stderr.decode("utf-8").strip() - return proc.returncode, stdout, stderr - - -def normalize(text): - # Basic normalization: strip trailing spaces on lines and remove extra blank lines - lines = [line.rstrip() for line in text.strip().splitlines()] - # Remove leading/trailing blank lines - while lines and lines[0] == "": - lines.pop(0) - while lines and lines[-1] == "": - lines.pop() - return "\n".join(lines) - - -def main(): - p = argparse.ArgumentParser() - p.add_argument("--examples", required=True, help="Path to examples.md") - p.add_argument( - "--runner", - required=True, - help='Runner command (reads stdin, writes stdout). e.g. "python mock_runner.py"', - ) - p.add_argument("--timeout", type=int, default=10) - p.add_argument( - "--fuzzy", - action="store_true", - help="Allow fuzzy comparison (ignore repeated whitespace)", - ) - args = p.parse_args() - - examples = parse_examples(args.examples) - - failures = 0 - for ex in examples: - print(f"--- Test #{ex['id']} ---") - rc, out, err = run_runner(args.runner, ex["input"], timeout=args.timeout) - if err: - print(f"Runner stderr:\n{err}\n") - if rc != 0: - print(f"Runner exited with code {rc}") - expected = ex["expected"] - actual = out - if args.fuzzy: - # collapse whitespace - expected_c = " ".join(expected.split()) - actual_c = " ".join(actual.split()) - else: - expected_c = normalize(expected) - actual_c = normalize(actual) - - if expected_c == actual_c: - print("PASS\n") - else: - print("FAIL") - print("--- Expected ---") - print(expected) - print("--- Actual ---") - print(actual) - print("--- Diff ---") - for line in unified_diff( - expected.splitlines(), actual.splitlines(), lineterm="" - ): - print(line) - print("") - failures += 1 - - if failures: - print(f"{failures} test(s) failed") - sys.exit(2) - else: - print("All tests passed") - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/.gitignore b/.gitignore index 85f993b..643acc1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ build*/ cmake-build-*/ .cmake/ +.cache/ # CMake generated files CMakeCache.txt @@ -126,3 +127,5 @@ __pycache__/ *.pyc *_pb2_grpc.py *_pb2.py +.worktrees/ +docs/superpowers diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..25b3936 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,176 @@ +# AGENTS.md + +Practical guide for agentic coding assistants in this repository. + +## 1) Project Overview + +- Name: `llmx-rtaco` (`llmx::rtaco` alias) +- Language: C++23 +- Platform: Linux only (RTNETLINK / `NETLINK_ROUTE`) +- Build system: CMake (>= 3.22), usually with Ninja +- Main code: `include/rtaco/` and `src/` +- Tests: GoogleTest + CTest (`tests/`) + +## 2) Important Paths + +- Public API headers: `include/rtaco/` +- Core implementation: `src/core/` +- Task/event/socket code: `src/tasks/`, `src/events/`, `src/socket/` +- Test setup: `tests/CMakeLists.txt` +- CMake options/tooling: `cmake/` +- CI workflow: `.github/workflows/ci.yml` +- Copilot rules: `.github/copilot-instructions.md` +- Formatting rules: `.clang-format` + +## 3) Build Commands + +Run from repository root. + +### Configure (release) + +```bash +cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release +``` + +### Configure with tests + +```bash +cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DRTACO_BUILD_TESTS=ON +``` + +### Configure with examples + +```bash +cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DRTACO_BUILD_EXAMPLES=ON +``` + +### Build and install + +```bash +cmake --build build -j +cmake --install build +``` + +### Build docs + +```bash +cmake -S . -B build -G Ninja -DRTACO_BUILD_DOCS=ON +cmake --build build --target docs +``` + +## 4) Test Commands + +Tests are available only if configured with `-DRTACO_BUILD_TESTS=ON`. + +### Run all tests + +```bash +ctest --test-dir build --output-on-failure +``` + +### Run one test case (preferred) + +```bash +ctest --test-dir build -R '^SignalTest\.SyncAndAsyncSlots$' --output-on-failure +``` + +### List all discovered tests + +```bash +ctest --test-dir build -N +``` + +### Run one gtest directly (fallback) + +```bash +./build/tests/test_rtaco --gtest_filter=SignalTest.SyncAndAsyncSlots +``` + +## 5) Lint / Formatting / Checks + +There is no dedicated lint target in CMake. + +### Format changed files + +```bash +clang-format -i +``` + +`.clang-format` is Google-derived with 4-space indent and column limit 90. + +### Baseline verification before finishing + +```bash +cmake --build build -j +ctest --test-dir build --output-on-failure +``` + +## 6) Code Style Guidelines + +Follow existing conventions in nearby files before introducing new patterns. + +### Naming + +- Classes/types: `PascalCase` (`Control`, `RouteDumpTask`) +- Methods/functions/variables: `snake_case` (`dump_routes`, `acquire_socket_token`) +- Async APIs: prefix with `async_` +- Private fields: trailing underscore (`socket_guard_`, `sequence_`) + +### Includes + +- Keep the established include grouping: + 1) matching project header + 2) C++ standard headers + 3) third-party headers + 4) project headers +- Do not auto-reorder includes (`SortIncludes: Never` in `.clang-format`). + +### Formatting + +- Use 4 spaces, no tabs. +- Keep lines around 90 columns when practical. +- Apply `clang-format` to touched files. + +### Types and APIs + +- Keep async-first API symmetry: `async_xxx()` + sync `xxx()` wrapper. +- Use fixed-width integer types for protocol/kernel boundaries. +- Prefer `std::span` for non-owning byte/data views. +- Keep copy/move/noexcept patterns consistent with neighboring code. + +### Error handling + +- Use `std::expected` as the default error model. +- Return failures with `std::unexpected{err}`. +- Avoid exception-based control flow for netlink/control-plane operations. +- Convert errno/kernel failures into `std::error_code` consistently. + +### Netlink/task behavior + +- Follow task shape (`prepare_request`, `process_message`, `handle_*`). +- Keep sequence checks (`header.nlmsg_seq`) in message processing paths. +- Keep filtering explicit and conservative for route/address/link/neighbor events. + +## 7) Copilot Rules To Preserve + +From `.github/copilot-instructions.md`: + +- Do not replace `expected<>`-based flows with exceptions. +- Do not change public API shape without a clear migration plan. +- NEVER use `0U`; write `0`. +- ALWAYS parenthesize bitshifts (for example `(1 << n)`). +- Keep async/sync naming symmetry (`async_get_neighbor` / `get_neighbor`). +- Assume Linux runtime and privilege constraints (`CAP_NET_ADMIN` for some ops). + +## 8) Cursor Rules Status + +- No `.cursorrules` file found. +- No `.cursor/rules/` directory found. +- Re-check and merge those rules here if they are added later. + +## 9) Agent Workflow + +- Read both header and source before changing API behavior. +- Keep patches focused; avoid unrelated refactors. +- Run targeted tests first, then full test suite when scope is broad. +- If behavior is unclear, add/adjust tests before rewriting internals. diff --git a/include/rtaco/core/nl_common.hxx b/include/rtaco/core/nl_common.hxx index aa0f8ca..5b6f17e 100644 --- a/include/rtaco/core/nl_common.hxx +++ b/include/rtaco/core/nl_common.hxx @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,20 @@ inline constexpr auto trim_string(const std::array& arr) -> std::string return trim_string(std::string_view{arr.data(), arr.size()}); } +template +inline auto checked_attr_begin(const nlmsghdr& header, const MsgT* info, int& attr_length) + -> const rtattr*; + +inline auto checked_attr_ok(const rtattr* attr, int attr_length) -> bool; + +inline auto checked_attr_next(const rtattr* attr, int& attr_length) -> const rtattr*; + +template +inline auto checked_payload(const rtattr& attr) -> const T*; + +template +inline auto checked_payload_copy(const rtattr& attr) -> std::optional; + /** @brief Extract the interface name from a netlink header. * * Parses netlink attributes for an ifinfomsg within the provided header and @@ -58,21 +73,16 @@ inline auto extract_ifname(const nlmsghdr& header) -> std::string { } const auto* info = reinterpret_cast(NLMSG_DATA(&header)); - int attr_length = static_cast(header.nlmsg_len) - - static_cast(NLMSG_LENGTH(sizeof(ifinfomsg))); - - if (attr_length <= 0) { - return {}; - } - - for (auto* attr = IFLA_RTA(info); RTA_OK(attr, attr_length); - attr = RTA_NEXT(attr, attr_length)) { + int attr_length = 0; + for (auto* attr = checked_attr_begin(header, info, attr_length); + checked_attr_ok(attr, attr_length); + attr = checked_attr_next(attr, attr_length)) { if (attr->rta_type != IFLA_IFNAME) { continue; } const auto payload = static_cast(RTA_PAYLOAD(attr)); - if (payload == 0U) { + if (payload == 0) { continue; } @@ -94,7 +104,7 @@ inline auto extract_ifname(const nlmsghdr& header) -> std::string { */ inline auto attribute_string(const rtattr& attr) -> std::string { const auto payload = static_cast(RTA_PAYLOAD(&attr)); - if (payload == 0U) { + if (payload == 0) { return {}; } @@ -141,13 +151,12 @@ inline auto attribute_address(const rtattr& attr, uint8_t family) -> std::string * Returns 0 on insufficient payload length. */ inline auto attribute_uint32(const rtattr& attr) -> uint32_t { - if (RTA_PAYLOAD(&attr) < sizeof(uint32_t)) { - return 0U; + const auto value = checked_payload_copy(attr); + if (!value.has_value()) { + return 0; } - uint32_t value{}; - std::memcpy(&value, RTA_DATA(&attr), sizeof(value)); - return value; + return *value; } /** @brief Format a hardware address (MAC) from an rtattr payload as a string. @@ -156,7 +165,7 @@ inline auto attribute_uint32(const rtattr& attr) -> uint32_t { */ inline auto attribute_hwaddr(const rtattr& attr) -> std::string { const auto payload = static_cast(RTA_PAYLOAD(&attr)); - if (payload == 0U) { + if (payload == 0) { return {}; } @@ -170,7 +179,7 @@ inline auto attribute_hwaddr(const rtattr& attr) -> std::string { constexpr char kHex[] = "0123456789abcdef"; for (size_t i = 0; i < payload; ++i) { - if (i != 0U) { + if (i != 0) { value.push_back(':'); } const auto byte = data[i]; @@ -195,25 +204,87 @@ inline const MsgT* get_msg_payload(const nlmsghdr& header) { return reinterpret_cast(NLMSG_DATA(&header)); } -/** @brief Iterate over rtattr attributes for a given message payload. - * - * Calls the provided function for each attribute discovered. - */ -template -inline void for_each_attr(const nlmsghdr& header, const MsgT* info, Fn&& fn) { - if (info == nullptr) { - return; +/** @brief Extract a checked nlmsgerr payload from an NLMSG_ERROR message. */ +inline auto checked_nlmsgerr(const nlmsghdr& header) -> const nlmsgerr* { + if (header.nlmsg_type != NLMSG_ERROR) { + return nullptr; } + return get_msg_payload(header); +} - int attr_length = static_cast(header.nlmsg_len) - +/** @brief Return the aligned first attribute pointer and payload length. */ +template +inline auto checked_attr_begin(const nlmsghdr& header, const MsgT* info, int& attr_length) + -> const rtattr* { + attr_length = 0; + if (info == nullptr || header.nlmsg_len < NLMSG_LENGTH(sizeof(MsgT))) { + return nullptr; + } + + attr_length = static_cast(header.nlmsg_len) - static_cast(NLMSG_LENGTH(sizeof(MsgT))); if (attr_length <= 0) { - return; + attr_length = 0; + return nullptr; + } + + return reinterpret_cast( + reinterpret_cast(info) + NLMSG_ALIGN(sizeof(MsgT))); +} + +/** @brief Validate the current attribute against remaining message length. */ +inline auto checked_attr_ok(const rtattr* attr, int attr_length) -> bool { + if (attr == nullptr) { + return false; + } + return RTA_OK(attr, attr_length); +} + +/** @brief Advance to the next attribute and update remaining length. */ +inline auto checked_attr_next(const rtattr* attr, int& attr_length) -> const rtattr* { + if (attr == nullptr) { + return nullptr; + } + return RTA_NEXT(attr, attr_length); +} + +/** @brief Read a typed rtattr payload only when the payload is large enough. */ +template +inline auto checked_payload(const rtattr& attr) -> const T* { + if (RTA_PAYLOAD(&attr) < sizeof(T)) { + return nullptr; + } + + return reinterpret_cast(RTA_DATA(&attr)); +} + +/** @brief Read a typed rtattr payload by value when the payload is large enough. + * + * Copies the payload by value to avoid alignment-related undefined behavior from + * typed pointer dereference. + */ +template +inline auto checked_payload_copy(const rtattr& attr) -> std::optional { + const auto* payload = checked_payload(attr); + if (payload == nullptr) { + return std::nullopt; } - const rtattr* attr = reinterpret_cast( - reinterpret_cast(info) + NLMSG_ALIGN(sizeof(MsgT))); - for (; RTA_OK(attr, attr_length); attr = RTA_NEXT(attr, attr_length)) { + T value{}; + std::memcpy(&value, payload, sizeof(value)); + return value; +} + +/** @brief Iterate over rtattr attributes for a given message payload. + * + * Calls the provided function for each attribute discovered. + */ +template +inline void for_each_attr(const nlmsghdr& header, const MsgT* info, Fn&& fn) { + int attr_length = 0; + for (const rtattr* attr = checked_attr_begin(header, info, attr_length); + checked_attr_ok(attr, attr_length); + attr = checked_attr_next(attr, attr_length)) { fn(attr); } } diff --git a/include/rtaco/core/nl_control.hxx b/include/rtaco/core/nl_control.hxx index 39e3165..57a46b5 100644 --- a/include/rtaco/core/nl_control.hxx +++ b/include/rtaco/core/nl_control.hxx @@ -114,7 +114,7 @@ public: void stop(); private: - auto acquire_socket_token() -> boost::asio::awaitable; + auto acquire_socket_token() -> boost::asio::awaitable; auto async_dump_routes_impl() -> boost::asio::awaitable; auto async_dump_addresses_impl() -> boost::asio::awaitable; diff --git a/include/rtaco/events/nl_neighbor_event.hxx b/include/rtaco/events/nl_neighbor_event.hxx index 2af71bc..ee9c27e 100644 --- a/include/rtaco/events/nl_neighbor_event.hxx +++ b/include/rtaco/events/nl_neighbor_event.hxx @@ -36,10 +36,10 @@ struct NeighborEvent { Type type{Type::UNKNOWN}; int index{0}; - uint8_t family{0U}; + uint8_t family{0}; State state{State::NONE}; - uint8_t flags{0U}; - uint8_t neighbor_type{0U}; + uint8_t flags{0}; + uint8_t neighbor_type{0}; std::string address{}; std::string lladdr{}; @@ -70,7 +70,7 @@ struct NeighborEvent { std::string result{}; for (const auto& entry : state_names) { - if ((std::to_underlying(state) & entry.mask) != 0U) { + if ((std::to_underlying(state) & entry.mask) != 0) { if (!result.empty()) { result += '|'; } diff --git a/src/core/nl_control.cxx b/src/core/nl_control.cxx index f09c64a..a5e0d25 100644 --- a/src/core/nl_control.cxx +++ b/src/core/nl_control.cxx @@ -7,7 +7,6 @@ #include #include #include -#include #include #include @@ -33,6 +32,10 @@ #include "rtaco/tasks/nl_route_dump_task.hxx" #include "rtaco/tasks/nl_link_dump_task.hxx" +#if defined(RTACO_ENABLE_TEST_HOOKS) +#include "tests/support/nl_test_hooks.hxx" +#endif + namespace llmx { namespace rtaco { @@ -139,7 +142,10 @@ void Control::stop() { } auto Control::async_dump_routes_impl() -> asio::awaitable { - co_await acquire_socket_token(); + if (auto gate_error = co_await acquire_socket_token(); gate_error) { + co_return std::unexpected{gate_error}; + } + SocketGuard guard{io_, "nl-control-route"}; if (auto result = guard.ensure_open(); !result) { @@ -153,7 +159,9 @@ auto Control::async_dump_routes_impl() -> asio::awaitable { } auto Control::async_dump_addresses_impl() -> asio::awaitable { - co_await acquire_socket_token(); + if (auto gate_error = co_await acquire_socket_token(); gate_error) { + co_return std::unexpected{gate_error}; + } SocketGuard guard{io_, "nl-control-address"}; @@ -168,7 +176,9 @@ auto Control::async_dump_addresses_impl() -> asio::awaitable asio::awaitable { - co_await acquire_socket_token(); + if (auto gate_error = co_await acquire_socket_token(); gate_error) { + co_return std::unexpected{gate_error}; + } SocketGuard guard{io_, "nl-control-neighbor"}; @@ -183,7 +193,9 @@ auto Control::async_dump_neighbors_impl() -> asio::awaitable asio::awaitable { - co_await acquire_socket_token(); + if (auto gate_error = co_await acquire_socket_token(); gate_error) { + co_return std::unexpected{gate_error}; + } SocketGuard guard{io_, "nl-control-link"}; @@ -199,7 +211,9 @@ auto Control::async_dump_links_impl() -> asio::awaitable { auto Control::async_probe_neighbor_impl(uint16_t ifindex, std::span address) -> asio::awaitable { - co_await acquire_socket_token(); + if (auto gate_error = co_await acquire_socket_token(); gate_error) { + co_return std::unexpected{gate_error}; + } if (auto result = socket_guard_.ensure_open(); !result) { co_return std::unexpected(result.error()); @@ -213,7 +227,9 @@ auto Control::async_probe_neighbor_impl(uint16_t ifindex, std::span auto Control::async_flush_neighbor_impl(uint16_t ifindex, std::span address) -> asio::awaitable { - co_await acquire_socket_token(); + if (auto gate_error = co_await acquire_socket_token(); gate_error) { + co_return std::unexpected{gate_error}; + } if (auto result = socket_guard_.ensure_open(); !result) { co_return std::unexpected(result.error()); @@ -227,7 +243,9 @@ auto Control::async_flush_neighbor_impl(uint16_t ifindex, std::span auto Control::async_get_neighbor_impl(uint16_t ifindex, std::span address) -> asio::awaitable { - co_await acquire_socket_token(); + if (auto gate_error = co_await acquire_socket_token(); gate_error) { + co_return std::unexpected{gate_error}; + } if (auto result = socket_guard_.ensure_open(); !result) { co_return std::unexpected(result.error()); @@ -239,7 +257,7 @@ auto Control::async_get_neighbor_impl(uint16_t ifindex, std::span a co_return co_await task.async_run(); } -auto Control::acquire_socket_token() -> asio::awaitable { +auto Control::acquire_socket_token() -> asio::awaitable { auto next = gate_.expiry() + std::chrono::nanoseconds{1}; auto now = asio::steady_timer::clock_type::now(); @@ -250,11 +268,22 @@ auto Control::acquire_socket_token() -> asio::awaitable { gate_.expires_at(next); boost::system::error_code ec; + +#if defined(RTACO_ENABLE_TEST_HOOKS) + if (auto injected = test_hooks::consume_gate_wait_error(); injected) { + ec = injected; + } else { + co_await gate_.async_wait(asio::redirect_error(asio::use_awaitable, ec)); + } +#else co_await gate_.async_wait(asio::redirect_error(asio::use_awaitable, ec)); +#endif if (ec && ec != asio::error::operation_aborted) { - throw std::runtime_error("gate wait failed: " + ec.message()); + co_return ec; } + + co_return std::error_code{}; } } // namespace rtaco diff --git a/src/core/nl_listener.cxx b/src/core/nl_listener.cxx index 23a01ba..0e94bfa 100644 --- a/src/core/nl_listener.cxx +++ b/src/core/nl_listener.cxx @@ -19,6 +19,7 @@ #include #include +#include "rtaco/core/nl_common.hxx" #include "rtaco/events/nl_address_event.hxx" #include "rtaco/events/nl_link_event.hxx" #include "rtaco/events/nl_route_event.hxx" @@ -161,8 +162,7 @@ void Listener::handle_message(const nlmsghdr& header) { } void Listener::handle_error_message(const nlmsghdr& header) { - if (const auto* err = reinterpret_cast(NLMSG_DATA(&header)); - err != nullptr) { + if (const auto* err = checked_nlmsgerr(header); err != nullptr) { on_nlmsgerr_event_(*err, header); } } diff --git a/src/events/nl_route_event.cxx b/src/events/nl_route_event.cxx index cbd6a0c..b6386bf 100644 --- a/src/events/nl_route_event.cxx +++ b/src/events/nl_route_event.cxx @@ -50,7 +50,7 @@ auto RouteEvent::from_nlmsghdr(const nlmsghdr& header) -> RouteEvent { } }); - if (event.oif_index != 0U) { + if (event.oif_index != 0) { event.oif = std::to_string(event.oif_index); } diff --git a/src/socket/nl_socket.cxx b/src/socket/nl_socket.cxx index a2a5cc4..a04ca55 100644 --- a/src/socket/nl_socket.cxx +++ b/src/socket/nl_socket.cxx @@ -2,10 +2,7 @@ #include #include -#include #include -#include -#include #include #include @@ -15,6 +12,10 @@ #include #include +#if defined(RTACO_ENABLE_TEST_HOOKS) +#include "tests/support/nl_test_hooks.hxx" +#endif + namespace llmx { namespace rtaco { @@ -55,9 +56,14 @@ auto Socket::cancel() -> std::expected { auto Socket::open(int proto, uint32_t groups) -> std::expected { boost::system::error_code ec; +#if defined(RTACO_ENABLE_TEST_HOOKS) + if (auto injected = test_hooks::consume_socket_open_error(); injected) { + return std::unexpected{injected}; + } +#endif + if (socket_.open(Protocol{proto}, ec); ec) { - throw std::runtime_error("failed to open netlink " + std::string{label_} + - " socket: " + ec.message()); + return std::unexpected{ec}; } const auto enable_option = @@ -72,7 +78,7 @@ auto Socket::open(int proto, uint32_t groups) -> std::expected std::expected(&sa), - &salen) == 0) { - std::cout << label_ << ": bound nl_pid=" << sa.nl_pid << " nl_groups=0x" - << std::hex << sa.nl_groups << std::dec << "\n"; - } else { - std::cout << label_ << ": getsockname failed: " << strerror(errno) << "\n"; - } + if (ec) { + (void)close(); + return std::unexpected{ec}; } return {}; diff --git a/src/tasks/nl_address_dump_task.cxx b/src/tasks/nl_address_dump_task.cxx index d835830..1a66936 100644 --- a/src/tasks/nl_address_dump_task.cxx +++ b/src/tasks/nl_address_dump_task.cxx @@ -14,6 +14,7 @@ #include #include +#include "rtaco/core/nl_common.hxx" #include "rtaco/events/nl_address_event.hxx" #include "rtaco/tasks/nl_request_task.hxx" @@ -49,7 +50,7 @@ auto AddressDumpTask::handle_done() -> std::expected std::expected { - const auto* err = reinterpret_cast(NLMSG_DATA(&header)); + const auto* err = checked_nlmsgerr(header); const auto code = err != nullptr ? -err->error : EPROTO; const auto error_code = std::make_error_code(static_cast(code)); diff --git a/src/tasks/nl_link_dump_task.cxx b/src/tasks/nl_link_dump_task.cxx index 3199263..37cd182 100644 --- a/src/tasks/nl_link_dump_task.cxx +++ b/src/tasks/nl_link_dump_task.cxx @@ -13,6 +13,7 @@ #include #include +#include "rtaco/core/nl_common.hxx" #include "rtaco/events/nl_link_event.hxx" #include "rtaco/tasks/nl_link_task.hxx" @@ -50,7 +51,7 @@ auto LinkDumpTask::handle_done() -> std::expected std::expected { - const auto* err = reinterpret_cast(NLMSG_DATA(&header)); + const auto* err = checked_nlmsgerr(header); const auto code = err != nullptr ? -err->error : EPROTO; const auto error_code = std::make_error_code(static_cast(code)); diff --git a/src/tasks/nl_route_dump_task.cxx b/src/tasks/nl_route_dump_task.cxx index c3416b4..bd30e25 100644 --- a/src/tasks/nl_route_dump_task.cxx +++ b/src/tasks/nl_route_dump_task.cxx @@ -11,6 +11,7 @@ #include #include +#include "rtaco/core/nl_common.hxx" #include "rtaco/events/nl_route_event.hxx" #include "rtaco/tasks/nl_route_task.hxx" @@ -47,7 +48,7 @@ auto RouteDumpTask::handle_done() -> std::expected std::expected { - const auto* err = reinterpret_cast(NLMSG_DATA(&header)); + const auto* err = checked_nlmsgerr(header); const auto code = err != nullptr ? -err->error : EPROTO; const auto error_code = std::make_error_code(static_cast(code)); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1cab645..7e3d761 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,32 +1,32 @@ cmake_minimum_required(VERSION 3.22) function(add_llmx_test target) - cmake_parse_arguments( - LLMX_TEST - "" - "TEST_NAME" - "" - ${ARGN} - ) - - if(LLMX_TEST_UNPARSED_ARGUMENTS) - message(FATAL_ERROR "add_llmx_test does not accept source file arguments") - endif() - - add_executable(${target} ${target}.cxx) - target_link_libraries(${target} - PRIVATE - GTest::gtest - ${PROJECT_NAME} - ) - - if(LLMX_TEST_TEST_NAME) - set(test_name ${LLMX_TEST_TEST_NAME}) - else() - set(test_name ${target}) - endif() - - add_test(NAME ${test_name} COMMAND ${target}) + cmake_parse_arguments( + LLMX_TEST + "" + "TEST_NAME" + "" + ${ARGN} + ) + + if(LLMX_TEST_UNPARSED_ARGUMENTS) + message(FATAL_ERROR "add_llmx_test does not accept source file arguments") + endif() + + add_executable(${target} ${target}.cxx) + target_link_libraries(${target} + PRIVATE + GTest::gtest + ${PROJECT_NAME} + ) + + if(LLMX_TEST_TEST_NAME) + set(test_name ${LLMX_TEST_TEST_NAME}) + else() + set(test_name ${target}) + endif() + + add_test(NAME ${test_name} COMMAND ${target}) endfunction() @@ -42,10 +42,36 @@ add_executable(test_rtaco test_signal.cpp test_requesttask_compile.cpp test_socket.cpp + test_control.cpp test_nl_common.cpp ) -target_link_libraries(test_rtaco PRIVATE llmx_rtaco GTest::gtest_main) +set(rtaco_test_sources) +foreach(source IN LISTS RTACO_SOURCES) + list(APPEND rtaco_test_sources "${PROJECT_SOURCE_DIR}/${source}") +endforeach() + +add_library(llmx_rtaco_test_instrumented STATIC ${rtaco_test_sources}) +target_compile_features(llmx_rtaco_test_instrumented PUBLIC cxx_std_23) +target_link_libraries(llmx_rtaco_test_instrumented PUBLIC Threads::Threads Boost::system) +target_include_directories(llmx_rtaco_test_instrumented + PUBLIC + ${PROJECT_SOURCE_DIR}/include + PRIVATE + ${PROJECT_SOURCE_DIR} +) +target_compile_definitions(llmx_rtaco_test_instrumented PRIVATE RTACO_ENABLE_TEST_HOOKS) + +get_target_property(rtaco_compile_defs llmx_rtaco COMPILE_DEFINITIONS) +if(rtaco_compile_defs) + list(FIND rtaco_compile_defs RTACO_ENABLE_TEST_HOOKS rtaco_hooks_idx) + if(NOT rtaco_hooks_idx EQUAL -1) + message(FATAL_ERROR "RTACO_ENABLE_TEST_HOOKS leaked into llmx_rtaco") + endif() +endif() + +target_link_libraries(test_rtaco PRIVATE llmx_rtaco_test_instrumented GTest::gtest_main) +target_include_directories(test_rtaco PRIVATE ${PROJECT_SOURCE_DIR}) enable_testing() diff --git a/tests/support/nl_test_hooks.hxx b/tests/support/nl_test_hooks.hxx new file mode 100644 index 0000000..a078c28 --- /dev/null +++ b/tests/support/nl_test_hooks.hxx @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include + +namespace llmx { +namespace rtaco::test_hooks { + +inline std::atomic_int socket_open_errno{0}; +inline std::atomic_int socket_bind_errno{0}; +inline std::atomic_int gate_wait_errno{0}; + +inline void reset() { + socket_open_errno.store(0, std::memory_order_relaxed); + socket_bind_errno.store(0, std::memory_order_relaxed); + gate_wait_errno.store(0, std::memory_order_relaxed); +} + +inline void fail_socket_open_once(std::errc code) { + socket_open_errno.store(static_cast(code), std::memory_order_relaxed); +} + +inline void fail_socket_bind_once(std::errc code) { + socket_bind_errno.store(static_cast(code), std::memory_order_relaxed); +} + +inline void fail_gate_wait_once(std::errc code) { + gate_wait_errno.store(static_cast(code), std::memory_order_relaxed); +} + +inline auto consume_socket_open_error() -> std::error_code { + auto value = socket_open_errno.exchange(0, std::memory_order_relaxed); + if (value == 0) { + return {}; + } + + return {value, std::generic_category()}; +} + +inline auto consume_socket_bind_error() -> std::error_code { + auto value = socket_bind_errno.exchange(0, std::memory_order_relaxed); + if (value == 0) { + return {}; + } + + return {value, std::generic_category()}; +} + +inline auto consume_gate_wait_error() -> std::error_code { + auto value = gate_wait_errno.exchange(0, std::memory_order_relaxed); + if (value == 0) { + return {}; + } + + return {value, std::generic_category()}; +} + +} // namespace rtaco::test_hooks +} // namespace llmx diff --git a/tests/test_control.cpp b/tests/test_control.cpp new file mode 100644 index 0000000..d023150 --- /dev/null +++ b/tests/test_control.cpp @@ -0,0 +1,33 @@ +#include + +#include +#include + +#include +#include + +#include "rtaco/core/nl_control.hxx" +#include "tests/support/nl_test_hooks.hxx" + +using namespace llmx::rtaco; + +TEST(ControlTest, DumpRoutesReturnsExpectedOnGateWaitFailure) { + boost::asio::io_context io; + auto work = boost::asio::make_work_guard(io); + std::thread runner([&io] { io.run(); }); + + Control control(io); + + llmx::rtaco::test_hooks::reset(); + llmx::rtaco::test_hooks::fail_gate_wait_once(std::errc::io_error); + + auto result = control.dump_routes(); + + ASSERT_FALSE(static_cast(result)); + EXPECT_EQ(result.error(), std::make_error_code(std::errc::io_error)); + + control.stop(); + work.reset(); + io.stop(); + runner.join(); +} diff --git a/tests/test_nl_common.cpp b/tests/test_nl_common.cpp index 21e495f..3904fd4 100644 --- a/tests/test_nl_common.cpp +++ b/tests/test_nl_common.cpp @@ -1,5 +1,7 @@ #include +#include #include +#include #include "rtaco/core/nl_common.hxx" @@ -34,3 +36,82 @@ TEST(NLCommonTest, GetMsgPayloadShort) { auto ptr = get_msg_payload(short_hdr); EXPECT_EQ(ptr, nullptr); } + +TEST(NLCommonTest, CheckedNlmsgerrFromHeader) { + std::vector buf(NLMSG_SPACE(sizeof(nlmsgerr)), 0); + + auto* header = reinterpret_cast(buf.data()); + header->nlmsg_len = NLMSG_LENGTH(sizeof(nlmsgerr)); + header->nlmsg_type = NLMSG_ERROR; + + auto* error = reinterpret_cast(NLMSG_DATA(header)); + error->error = -EEXIST; + + const auto* extracted = checked_nlmsgerr(*header); + ASSERT_NE(extracted, nullptr); + EXPECT_EQ(extracted->error, -EEXIST); +} + +TEST(NLCommonTest, CheckedAttrBeginAndTraversal) { + constexpr uint32_t kPayload = 42; + constexpr size_t kAttrLen = RTA_LENGTH(sizeof(kPayload)); + constexpr size_t kMsgLen = NLMSG_LENGTH(sizeof(ifinfomsg)) + RTA_ALIGN(kAttrLen); + struct alignas(nlmsghdr) MsgBuffer { + std::array bytes{}; + } buffer; + + auto* header = reinterpret_cast(buffer.bytes.data()); + header->nlmsg_len = static_cast(kMsgLen); + auto* info = reinterpret_cast(NLMSG_DATA(header)); + + int attr_length = 0; + const rtattr* attr = checked_attr_begin(*header, info, attr_length); + ASSERT_NE(attr, nullptr); + EXPECT_GT(attr_length, 0); + + auto* mutable_attr = const_cast(attr); + mutable_attr->rta_len = static_cast(kAttrLen); + mutable_attr->rta_type = IFLA_MTU; + std::memcpy(RTA_DATA(mutable_attr), &kPayload, sizeof(kPayload)); + + EXPECT_TRUE(checked_attr_ok(attr, attr_length)); + EXPECT_EQ(attr->rta_type, IFLA_MTU); +} + +TEST(NLCommonTest, CheckedPayloadAccess) { + constexpr uint32_t kValue = 1337; + const size_t attr_len = RTA_LENGTH(sizeof(kValue)); + std::vector buf(attr_len, 0); + + auto* attr = reinterpret_cast(buf.data()); + attr->rta_len = static_cast(attr_len); + std::memcpy(RTA_DATA(attr), &kValue, sizeof(kValue)); + + const auto* value = checked_payload(*attr); + ASSERT_NE(value, nullptr); + EXPECT_EQ(*value, kValue); + + attr->rta_len = RTA_LENGTH(sizeof(uint16_t)); + EXPECT_EQ(checked_payload(*attr), nullptr); +} + +TEST(NLCommonTest, CheckedPayloadSupportsUnalignedData) { + constexpr uint64_t kValue = 0x0102030405060708ULL; + const size_t attr_len = RTA_LENGTH(sizeof(kValue)); + struct alignas(uint64_t) AlignedStorage { + std::array bytes{}; + } storage; + + auto* attr = reinterpret_cast(storage.bytes.data()); + attr->rta_len = static_cast(attr_len); + attr->rta_type = IFLA_MTU; + + std::memcpy(RTA_DATA(attr), &kValue, sizeof(kValue)); + + const auto payload_addr = reinterpret_cast(RTA_DATA(attr)); + ASSERT_NE((payload_addr % alignof(uint64_t)), 0); + + const auto value = checked_payload_copy(*attr); + ASSERT_TRUE(value.has_value()); + EXPECT_EQ(*value, kValue); +} diff --git a/tests/test_socket.cpp b/tests/test_socket.cpp index 59c0e26..b74ab21 100644 --- a/tests/test_socket.cpp +++ b/tests/test_socket.cpp @@ -1,8 +1,11 @@ #include #include +#include + #include "rtaco/socket/nl_socket.hxx" #include "rtaco/socket/nl_socket_guard.hxx" +#include "tests/support/nl_test_hooks.hxx" using namespace llmx::rtaco; @@ -27,3 +30,25 @@ TEST(SocketGuardTest, StopNoThrow) { // stop should be safe even if socket not open EXPECT_NO_THROW(g.stop()); } + +TEST(SocketTest, OpenFailureReturnsExpected) { + boost::asio::io_context io; + Socket s(io, "test-socket"); + + auto rc = s.open(-1, 0); + ASSERT_FALSE(static_cast(rc)); + EXPECT_FALSE(s.is_open()); +} + +TEST(SocketTest, BindFailureReturnsExpected) { + boost::asio::io_context io; + Socket s(io, "test-socket"); + + llmx::rtaco::test_hooks::reset(); + llmx::rtaco::test_hooks::fail_socket_bind_once(std::errc::address_in_use); + + auto rc = s.open(NETLINK_ROUTE, 0); + ASSERT_FALSE(static_cast(rc)); + EXPECT_EQ(rc.error(), std::make_error_code(std::errc::address_in_use)); + EXPECT_FALSE(s.is_open()); +}