Repository: felixdivo/ros2-easy-test (language: python)

Issue:
Is it possible to add this package as a rosdep dependency
I am wondering if it is possible to have this package be added as a <test_depend> in the package manifest. I did not find this package on the rosdep list https://github.com/ros/rosdistro/blob/master/rosdep/python.yaml.


Discussion / hints:
Oh yeah, why not?! I haven't done this yet because I didn't find the time yet but also because I was unsure about the maturity of the tool. But yeah, adding it sounds reasonable. Would you mind adding it there?
The tool is great, it has allowed us to cleanly test our nodes. One thing that would put it over the top is support for actions. 
For now I was planning to use the env node to try testing actions do you have any advise or examples?

When I get some time I will create a PR for rosdep. Cheers. 


Started PR https://github.com/ros/rosdistro/pull/39892
Excellent, thank you @vik748!

Fix patch:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 5c180a2..9a122ef 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -4,9 +4,6 @@
     "dockerfile": "../Dockerfile",
     "context": ".."
   },
-  "settings": {
-    "terminal.integrated.defaultProfile.linux": "zsh"
-  },
   "customizations": {
     "vscode": {
       "extensions": [
@@ -14,9 +11,10 @@
         "redhat.vscode-yaml",
         "bungcip.better-toml",
         "trond-snekvik.simple-rst",
-        "GitHub.vscode-github-actions"
+        "GitHub.vscode-github-actions",
+        "ms-python.black-formatter"
       ]
     }
   },
-  "postStartCommand": "pip install -e '.[dev,doc]'"
+  "postStartCommand": "rosdep install --from-paths ros2_easy_test && colcon build --symlink-install && pip install -e './ros2_easy_test[dev,doc]'"
 }
\ No newline at end of file
diff --git a/.github/workflows/python-package.yaml b/.github/workflows/python-package.yaml
index 8c2a287..e8edc09 100644
--- a/.github/workflows/python-package.yaml
+++ b/.github/workflows/python-package.yaml
@@ -13,8 +13,8 @@ jobs:
     strategy:
       fail-fast: false  # Tests may behave very different across these systems
       matrix:
-        # We do not test on ros:foxy-ros-core, since it causes many problems in the CI pipeline
-        # TODO: add ros:iron-ros-core when it is available
+        # We test on all supported LTS versions and the latest rolling release
+        # TODO: Add `ros:jazzy-ros-core`
         container: ["ros:humble-ros-core", "ros:rolling-ros-core"]
     container: ${{ matrix.container }}
 
@@ -22,32 +22,43 @@ jobs:
       run:
         shell: bash
     steps:
-      - 
+      -
         name: Checkout repository
         uses: actions/checkout@v3
       -
-        name: Install dependencies and the package
+        name: Install dependencies
         run: |
-          apt-get update -q && apt-get install -qy python3-pip python-is-python3 ros-$ROS_DISTRO-example-interfaces
-          pip install --upgrade pip setuptools
-          python -m pip install -e '.[dev]'
+          apt-get update -q
+          apt-get install -qy python3-pip python-is-python3 ros-dev-tools
+          rosdep init
+          # There seems to be no other way to install the PiPI dependencies:
+          export PIP_BREAK_SYSTEM_PACKAGES=1
+          pip install -e './ros2_easy_test[dev]'
+      - run: echo "ROS_DISTRO=$ROS_DISTRO" >> "$GITHUB_OUTPUT"
+        id: save_envs
+      -
+        uses: ros-tooling/action-ros-ci@v0.3
+        with:
+          target-ros2-distro: ${{ steps.save_envs.outputs.ROS_DISTRO }}
+          package-name: ros2_easy_test
+          skip-tests: true
       -
-        name: Format (with black)
+        name: Format (with ruff)
         run: |
-          black --check .
+          ruff format --check
       -
         name: Lint (with ruff)
         run: |
-          ruff check .
+          ruff check
       -
         name: Type check main code (with mypy)
         run: |
-          mypy ros2_easy_test
+          mypy ros2_easy_test/ros2_easy_test
       -
         name: Type check tests (with mypy)
         continue-on-error: true  # We do not want to fail the build if the type checker fails on the tests
         run: |
-          mypy tests
+          mypy ros2_easy_test/tests
       -
         name: Testing (with pytest)
         run: |
diff --git a/.github/workflows/python-release.yaml b/.github/workflows/python-release.yaml
index 7f64fd7..fc79360 100644
--- a/.github/workflows/python-release.yaml
+++ b/.github/workflows/python-release.yaml
@@ -22,7 +22,7 @@ jobs:
       - name: Create GitHub Release
         uses: softprops/action-gh-release@v1
         with:
-          release_name: Release ${{ github.ref }}
+          name: Release ${{ github.ref }}
           draft: false
           prerelease: false
 
@@ -32,9 +32,9 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
     - name: Set up Python
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: '3.x'  # The latest stable version of CPython 3
     - name: Install dependencies
@@ -42,9 +42,9 @@ jobs:
         python -m pip install --upgrade pip
         pip install build
     - name: Build package
-      run: python -m build
+      run: python -m build ros2_easy_test
     - name: Publish package
-      uses: pypa/gh-action-pypi-publish@0bf742be3ebe032c25dd15117957dc15d0cfc38d  # Version v1.8.5
+      uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450  # Version v1.8.14
       with:
         user: __token__
         password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 0909b62..60c3431 100644
--- a/.gitignore
+++ b/.gitignore
@@ -129,3 +129,63 @@ dmypy.json
 .pyre/
 
 .ruff_cache
+
+devel/
+logs/
+build/
+bin/
+lib/
+install/
+log/
+msg_gen/
+srv_gen/
+msg/*Action.msg
+msg/*ActionFeedback.msg
+msg/*ActionGoal.msg
+msg/*ActionResult.msg
+msg/*Feedback.msg
+msg/*Goal.msg
+msg/*Result.msg
+msg/_*.py
+build_isolated/
+devel_isolated/
+
+# Generated by dynamic reconfigure
+*.cfgc
+/cfg/cpp/
+/cfg/*.py
+
+# Ignore generated docs
+*.dox
+*.wikidoc
+
+# eclipse stuff
+.project
+.cproject
+
+# qcreator stuff
+CMakeLists.txt.user
+
+srv/_*.py
+*.pcd
+*.pyc
+qtcreator-*
+*.user
+
+/planning/cfg
+/planning/docs
+/planning/src
+
+*~
+
+# Emacs
+.#*
+
+# Catkin custom files
+CATKIN_IGNORE
+
+############ CUSTOM ############
+
+# Gets created in the CI pipeline, but we don't want ruff to look at it
+ros_ws/build
+ros_ws/install
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 900a04d..d754079 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -10,7 +10,7 @@ build:
 python:
   install:
     - method: pip
-      path: .
+      path: ros2_easy_test
       extra_requirements:
         - doc
 
diff --git a/Dockerfile b/Dockerfile
index 2c63a36..a3ca1de 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,7 +8,8 @@
 FROM ros:humble-ros-core
 
 # Install runtime dependencies
-RUN apt-get update -q && apt-get install -qy python3-pip python-is-python3 git zsh ros-$ROS_DISTRO-example-interfaces
+RUN apt-get update -q && apt-get install -qy python3-pip python-is-python3 git ros-dev-tools
+RUN rosdep init && rosdep update
 
 # Need to have setuptools version 64+ for editable installs
 RUN pip install --upgrade pip setuptools
diff --git a/README.md b/README.md
index b726026..982bd7c 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,7 @@
 
 [![CI status](https://github.com/felixdivo/ros2-easy-test/actions/workflows/python-package.yaml/badge.svg)](https://github.com/felixdivo/ros2-easy-test/actions/workflows/python-package.yaml)
 [![documentation status](https://readthedocs.org/projects/ros2-easy-test/badge/)](https://ros2-easy-test.readthedocs.io/en/latest/)
-[![code style](https://img.shields.io/badge/code%20style-black-black)](https://github.com/psf/black)
-[![linter](https://img.shields.io/badge/linter-ruff-black)](https://github.com/charliermarsh/ruff)
+[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
 [![static type checker](https://img.shields.io/badge/static%20typing-mypy-black)](https://mypy-lang.org/)
 
 A Python test framework for [ROS2](https://ros.org/) allowing for:
@@ -115,7 +114,10 @@ See [the documentation on that](https://ros2-easy-test.readthedocs.io/en/latest/
 
 ## Contributing
 
-You can install the development dependencies with `pip install -e ".[dev]"`. After this, you will have access to the configured formatters `black .` and `ruff check .`.
+Basic installation is easiest with the porvided `Dockerfile`.
+For the last piece of setup, either open it in the provided [Devcontainer](https://code.visualstudio.com/docs/remote/containers) or maunally run `rosdep install --from-paths ros2_easy_test && colcon build --symlink-install && pip install -e './ros2_easy_test[dev]` afterward.
+
+After this, you will have access to the configured formatter (`ruff format`) and linter (`ruff check`).
 
 You can run the test with simply `pytest`. Coverage reports and timings will be printed on the command line, and a fresh line-by-line coverage report is in `htmlcov/index.html`.
 
@@ -143,4 +145,4 @@ See [Releases](https://github.com/felixdivo/ros2-easy-test/releases).
 See [LICENSE](LICENSE).
 
 Initially developed by [Felix Divo](https://github.com/felixdivo) at [*Sailing Team Darmstadt e. V.*](https://www.st-darmstadt.de/), a student group devoted to robotic sailing based in Darmstadt, Germany.
-Thanks to [Simon Kohaut](https://github.com/simon-kohaut) for his kind and nuanced feedback.
+Thanks to [Simon Kohaut](https://github.com/simon-kohaut) for his kind and nuanced feedback, and all [the other awesome contributors](https://github.com/felixdivo/ros2-easy-test/graphs/contributors) for their help and support!
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 3963a9a..ea5abca 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -14,7 +14,7 @@
 project = "ROS2 easy-test"
 
 # From: https://stackoverflow.com/a/5872024/3753684
-with open(Path(__file__).parents[2] / "ros2_easy_test" / "__init__.py") as init:
+with open(Path(__file__).parents[2] / "ros2_easy_test" / "ros2_easy_test" / "__init__.py") as init:
     metadata = dict(re.findall('''__([a-z]+)__ = "([^"]+)"''', init.read()))
 
 copyright = f"2021, {metadata['author']}"
diff --git a/pyproject.toml b/pyproject.toml
index f2bedc7..09930c5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,77 +1,25 @@
-[build-system]
-requires = ["setuptools", "setuptools-scm"]
-build-backend = "setuptools.build_meta"
-
-[project]
-dynamic = ["version"]
-name = "ros2_easy_test"
-authors = [
-  { name = "Felix Divo", email = "felix.divo@sailingteam.tu-darmstadt.de" },
-]
-description = "A Python test framework for ROS2 allowing simple and expressive assertions based on message interactions."
-readme = "README.md"
-requires-python = ">=3.8"
-classifiers = [
-  "Programming Language :: Python :: 3",
-  "Operating System :: OS Independent",
-  "Natural Language :: English",
-  "License :: OSI Approved :: MIT License",
-  "Intended Audience :: Developers",
-  "Framework :: Robot Framework :: Library",
-  "Topic :: Software Development :: Testing",
-  "Topic :: Software Development :: Testing :: BDD",
-  "Typing :: Typed",
-]
-urls = { Homepage = "https://github.com/felixdivo/ros2-easy-test" }
-dependencies = [
-  "makefun>=1.15.2"
-]
-# The package is discovered automatically (including the py.typed file):
-# https://setuptools.pypa.io/en/latest/userguide/package_discovery.html#flat-layout
-
-[project.optional-dependencies]
-dev = [
-  "black~=24.2",
-  "ruff",
-  "mypy",
-  "pytest",
-  "pytest-cov",
-  "pytest-sugar",
-  "hypothesis",  # To demonstrate how it works with this
-]
-doc = [
-  "sphinx",
-  "sphinx_rtd_theme",
-]
-
-[tool.setuptools.dynamic]
-version = { attr = "ros2_easy_test.__version__" }
-
 [tool.pytest.ini_options]
 # We want errors to fail loudly to not miss any exceptions in the background
 # The latter part shows the duration of the slowest tests
 # -v prints the test case names to see deadlocks
 addopts = "-Werror --color=yes --cov=ros2_easy_test --doctest-modules --cov-report html --durations=0 --durations-min=3.0 -v"
 testpaths = [
-    "ros2_easy_test",  # for the doctests
-    "tests",  # for the actual tests
+    "ros2_easy_test/ros2_easy_test",  # for the doctests
+    "ros2_easy_test/tests",  # for the actual tests
 ]
 
-[tool.black]
-line-length = 110
-
-[tool.isort]
-profile = "black"
-
 [tool.ruff]
-# Same as Black
 line-length = 110
 # The lowest supported version
 target-version = "py38"
+exclude = ["ros_ws/**"]  # Gets created by the CI pipeline
 
 [tool.ruff.lint]
 select = ["E", "F", "W", "I", "N", "UP"]
 
+[tool.ruff.format]
+docstring-code-format = true
+
 [tool.mypy]
 warn_return_any = true
 warn_unused_configs = true


Test patch:
diff --git a/ros2_easy_test/package.xml b/ros2_easy_test/package.xml
new file mode 100644
index 0000000..c84d1b7
--- /dev/null
+++ b/ros2_easy_test/package.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
+<package format="3">
+  <name>ros2_easy_test</name>
+  <version>0.3.0</version>
+  <description>A Python test framework for ROS2 allowing simple and expressive assertions based on message interactions.</description>
+  <maintainer email="felix.divo@sailingteam.tu-darmstadt.de">Felix Divo</maintainer>
+  <license>MIT</license>
+
+  <test_depend>python3-pytest</test_depend>
+  <test_depend>example_interfaces</test_depend>
+
+  <export>
+    <build_type>ament_python</build_type>
+  </export>
+</package>
diff --git a/ros2_easy_test/py.typed b/ros2_easy_test/resource/ros2_easy_test
similarity index 100%
rename from ros2_easy_test/py.typed
rename to ros2_easy_test/resource/ros2_easy_test
diff --git a/ros2_easy_test/__init__.py b/ros2_easy_test/ros2_easy_test/__init__.py
similarity index 93%
rename from ros2_easy_test/__init__.py
rename to ros2_easy_test/ros2_easy_test/__init__.py
index 90d34c6..312bff6 100644
--- a/ros2_easy_test/__init__.py
+++ b/ros2_easy_test/ros2_easy_test/__init__.py
@@ -4,7 +4,7 @@
 """
 
 __author__ = "Felix Divo <felix.divo@sailingteam.tu-darmstadt.de>"
-__version__ = "0.2.0.post1"
+__version__ = "0.3.0"
 
 from .decorators import with_launch_file, with_single_node
 from .env import ROS2TestEnvironment
diff --git a/ros2_easy_test/decorators.py b/ros2_easy_test/ros2_easy_test/decorators.py
similarity index 94%
rename from ros2_easy_test/decorators.py
rename to ros2_easy_test/ros2_easy_test/decorators.py
index b413bcc..38d459a 100644
--- a/ros2_easy_test/decorators.py
+++ b/ros2_easy_test/ros2_easy_test/decorators.py
@@ -130,7 +130,8 @@ def wrapper(*args_inner, **kwargs_inner) -> None:
                     )
                     # The type ignore is needed due to a bug in ROS2 Humble+
                     executor.spin_until_future_complete(
-                        test_function_task, timeout_sec=time_limit  # type: ignore[arg-type]
+                        test_function_task,
+                        timeout_sec=time_limit,  # type: ignore[arg-type]
                     )
 
                     test_function_exception = test_function_task.exception()
@@ -294,7 +295,8 @@ def wrapper(*args_inner, **kwargs_inner) -> None:
                         )
                         # The type ignore is needed due to a bug in ROS2 Humble+
                         executor.spin_until_future_complete(
-                            test_function_task, timeout_sec=time_limit  # type: ignore[arg-type]
+                            test_function_task,
+                            timeout_sec=time_limit,  # type: ignore[arg-type]
                         )
 
                         test_function_exception = test_function_task.exception()
@@ -341,16 +343,18 @@ def wrapper(*args_inner, **kwargs_inner) -> None:
                     except NameError:
                         pass  # Maybe, the launch process was not even started
                     else:
-                        # Signal the child launch process to finish
-                        # This is much like pressing Ctrl+C on the console
-                        ros2_process.send_signal(SIGINT)
-                        try:
-                            # Might raise a TimeoutExpired if it takes too long
-                            return_code = ros2_process.wait(timeout=shutdown_timeout / 2)
-                        except TimeoutExpired:  # pragma: no cover
-                            ros2_process.terminate()
-                            # return_code will be larger than 130
-                            return_code = ros2_process.wait(timeout=shutdown_timeout / 2)
+                        return_code = ros2_process.poll()
+                        if return_code is None:
+                            # Signal the child launch process to finish
+                            # This is much like pressing Ctrl+C on the console
+                            ros2_process.send_signal(SIGINT)
+                            try:
+                                # Might raise a TimeoutExpired if it takes too long
+                                return_code = ros2_process.wait(timeout=shutdown_timeout / 2)
+                            except TimeoutExpired:  # pragma: no cover
+                                ros2_process.terminate()
+                                # return_code will be larger than 130
+                                return_code = ros2_process.wait(timeout=shutdown_timeout / 2)
 
                 # Both SUCCESS (0) or the result code of SIGINT (130) are acceptable
                 return_code_problematic = return_code not in {0, 130}
diff --git a/ros2_easy_test/env.py b/ros2_easy_test/ros2_easy_test/env.py
similarity index 100%
rename from ros2_easy_test/env.py
rename to ros2_easy_test/ros2_easy_test/env.py
diff --git a/ros2_easy_test/launch_file.py b/ros2_easy_test/ros2_easy_test/launch_file.py
similarity index 100%
rename from ros2_easy_test/launch_file.py
rename to ros2_easy_test/ros2_easy_test/launch_file.py
diff --git a/ros2_easy_test/ros2_easy_test/py.typed b/ros2_easy_test/ros2_easy_test/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/ros2_easy_test/setup.cfg b/ros2_easy_test/setup.cfg
new file mode 100644
index 0000000..dc598bc
--- /dev/null
+++ b/ros2_easy_test/setup.cfg
@@ -0,0 +1,4 @@
+[develop]
+script_dir=$base/lib/ros2_easy_test
+[install]
+install_scripts=$base/lib/ros2_easy_test
diff --git a/ros2_easy_test/setup.py b/ros2_easy_test/setup.py
new file mode 100644
index 0000000..e69d3fd
--- /dev/null
+++ b/ros2_easy_test/setup.py
@@ -0,0 +1,54 @@
+from xml.etree import ElementTree
+
+from setuptools import find_packages, setup
+
+# Try to get package info
+# Since checking everything for existence would be cumbersome, we disable strict typing here
+# mypy: no-strict-optional
+package = ElementTree.parse("package.xml").getroot()
+PACKAGE_NAME = package.find("name").text
+
+
+setup(
+    name=PACKAGE_NAME,
+    version=package.find("version").text,
+    packages=find_packages(exclude=["test"]),
+    data_files=[
+        ("share/ament_index/resource_index/packages", [f"resource/{PACKAGE_NAME}"]),
+        (f"share/{PACKAGE_NAME}", ["package.xml"]),
+    ],
+    zip_safe=True,
+    maintainer=package.find("maintainer").text,
+    maintainer_email=package.find("maintainer").attrib["email"],
+    description=package.find("description").text,
+    license=package.find("license").text,
+    url="https://github.com/felixdivo/ros2-easy-test",
+    entry_points={"console_scripts": []},
+    classifiers=[
+        "Programming Language :: Python :: 3",
+        "Operating System :: OS Independent",
+        "Natural Language :: English",
+        "License :: OSI Approved :: MIT License",
+        "Intended Audience :: Developers",
+        "Framework :: Robot Framework :: Library",
+        "Topic :: Software Development :: Testing",
+        "Topic :: Software Development :: Testing :: BDD",
+        "Typing :: Typed",
+    ],
+    requires_python=">=3.8",
+    install_requires=["makefun>=1.15.2"],
+    extras_require={
+        "dev": [
+            "ruff",
+            "mypy",
+            "pytest",
+            "pytest-cov",
+            "pytest-sugar",
+            "hypothesis",  # To demonstrate how it works with this
+        ],
+        "doc": [
+            "sphinx",
+            "sphinx_rtd_theme",
+        ],
+    },
+)
diff --git a/ros2_easy_test/tests/__init__.py b/ros2_easy_test/tests/__init__.py
new file mode 100644
index 0000000..9dfec03
--- /dev/null
+++ b/ros2_easy_test/tests/__init__.py
@@ -0,0 +1,3 @@
+from pathlib import Path
+
+LAUNCH_FILES = Path(__file__).parent / "example_launch_files"
diff --git a/ros2_easy_test/tests/demo/__init__.py b/ros2_easy_test/tests/demo/__init__.py
new file mode 100644
index 0000000..9f7ddbd
--- /dev/null
+++ b/ros2_easy_test/tests/demo/__init__.py
@@ -0,0 +1,5 @@
+"""These tests serve a dual purpose:
+1) They showcase how to reuse the library.
+2) They test the library itself.
+
+"""
diff --git a/tests/demo/coroutine_test.py b/ros2_easy_test/tests/demo/coroutine_test.py
similarity index 100%
rename from tests/demo/coroutine_test.py
rename to ros2_easy_test/tests/demo/coroutine_test.py
diff --git a/tests/demo/fixture_test.py b/ros2_easy_test/tests/demo/fixture_test.py
similarity index 100%
rename from tests/demo/fixture_test.py
rename to ros2_easy_test/tests/demo/fixture_test.py
diff --git a/tests/demo/hypothesis_test.py b/ros2_easy_test/tests/demo/hypothesis_test.py
similarity index 100%
rename from tests/demo/hypothesis_test.py
rename to ros2_easy_test/tests/demo/hypothesis_test.py
diff --git a/tests/demo/latching_topic_test.py b/ros2_easy_test/tests/demo/latching_topic_test.py
similarity index 100%
rename from tests/demo/latching_topic_test.py
rename to ros2_easy_test/tests/demo/latching_topic_test.py
diff --git a/tests/demo/reuse_decorator_test.py b/ros2_easy_test/tests/demo/reuse_decorator_test.py
similarity index 100%
rename from tests/demo/reuse_decorator_test.py
rename to ros2_easy_test/tests/demo/reuse_decorator_test.py
diff --git a/ros2_easy_test/tests/example_launch_files/adder.yaml b/ros2_easy_test/tests/example_launch_files/adder.yaml
new file mode 100644
index 0000000..3f29acf
--- /dev/null
+++ b/ros2_easy_test/tests/example_launch_files/adder.yaml
@@ -0,0 +1,5 @@
+launch:
+- executable:
+    name: AddTwoIntsServer
+    cmd: python ros2_easy_test/tests/example_nodes/run_node.py ros2_easy_test/tests/example_nodes/well_behaved.py AddTwoIntsServer
+    output: screen
diff --git a/ros2_easy_test/tests/example_launch_files/echo.yaml b/ros2_easy_test/tests/example_launch_files/echo.yaml
new file mode 100644
index 0000000..6531f3f
--- /dev/null
+++ b/ros2_easy_test/tests/example_launch_files/echo.yaml
@@ -0,0 +1,5 @@
+launch:
+- executable:
+    name: EchoNode
+    cmd: python ros2_easy_test/tests/example_nodes/run_node.py ros2_easy_test/tests/example_nodes/well_behaved.py EchoNode
+    output: screen
diff --git a/ros2_easy_test/tests/example_launch_files/raise_in_init.yaml b/ros2_easy_test/tests/example_launch_files/raise_in_init.yaml
new file mode 100644
index 0000000..27bac2f
--- /dev/null
+++ b/ros2_easy_test/tests/example_launch_files/raise_in_init.yaml
@@ -0,0 +1,5 @@
+launch:
+- executable:
+    name: NodeRaiseInInit
+    cmd: python ros2_easy_test/tests/example_nodes/run_node.py ros2_easy_test/tests/example_nodes/failing.py NodeRaiseInInit
+    output: screen
diff --git a/ros2_easy_test/tests/example_launch_files/raise_in_timer.yaml b/ros2_easy_test/tests/example_launch_files/raise_in_timer.yaml
new file mode 100644
index 0000000..d9bb9cd
--- /dev/null
+++ b/ros2_easy_test/tests/example_launch_files/raise_in_timer.yaml
@@ -0,0 +1,5 @@
+launch:
+- executable:
+    name: NodeRaiseInTimer
+    cmd: python ros2_easy_test/tests/example_nodes/run_node.py ros2_easy_test/tests/example_nodes/failing.py NodeRaiseInTimer
+    output: screen
diff --git a/ros2_easy_test/tests/example_launch_files/raise_on_request.yaml b/ros2_easy_test/tests/example_launch_files/raise_on_request.yaml
new file mode 100644
index 0000000..84836c9
--- /dev/null
+++ b/ros2_easy_test/tests/example_launch_files/raise_on_request.yaml
@@ -0,0 +1,5 @@
+launch:
+- executable:
+    name: NodeRaiseOnRequest
+    cmd: python ros2_easy_test/tests/example_nodes/run_node.py ros2_easy_test/tests/example_nodes/failing.py NodeRaiseOnRequest
+    output: screen
diff --git a/tests/example_launch_files/talker.py b/ros2_easy_test/tests/example_launch_files/talker.py
similarity index 74%
rename from tests/example_launch_files/talker.py
rename to ros2_easy_test/tests/example_launch_files/talker.py
index d6b42b8..8ad1ca9 100644
--- a/tests/example_launch_files/talker.py
+++ b/ros2_easy_test/tests/example_launch_files/talker.py
@@ -12,8 +12,8 @@ def generate_launch_description() -> LaunchDescription:
             ExecuteProcess(
                 cmd=[
                     sys.executable,
-                    "tests/example_nodes/run_node.py",
-                    "tests/example_nodes/well_behaved.py",
+                    "ros2_easy_test/tests/example_nodes/run_node.py",
+                    "ros2_easy_test/tests/example_nodes/well_behaved.py",
                     "Talker",
                 ],
                 output="screen",
diff --git a/ros2_easy_test/tests/example_launch_files/talker.yaml b/ros2_easy_test/tests/example_launch_files/talker.yaml
new file mode 100644
index 0000000..cff9833
--- /dev/null
+++ b/ros2_easy_test/tests/example_launch_files/talker.yaml
@@ -0,0 +1,5 @@
+launch:
+- executable:
+    name: Talker
+    cmd: python ros2_easy_test/tests/example_nodes/run_node.py ros2_easy_test/tests/example_nodes/well_behaved.py Talker
+    output: screen
diff --git a/tests/example_launch_files/talker_with_params.py b/ros2_easy_test/tests/example_launch_files/talker_with_params.py
similarity index 88%
rename from tests/example_launch_files/talker_with_params.py
rename to ros2_easy_test/tests/example_launch_files/talker_with_params.py
index a5c02b7..a577341 100644
--- a/tests/example_launch_files/talker_with_params.py
+++ b/ros2_easy_test/tests/example_launch_files/talker_with_params.py
@@ -13,8 +13,8 @@ def _launch_setup(context: LaunchContext, start_value: LaunchConfiguration):
         Node(
             executable=sys.executable,
             arguments=[
-                "tests/example_nodes/run_node.py",
-                "tests/example_nodes/well_behaved.py",
+                "ros2_easy_test/tests/example_nodes/run_node.py",
+                "ros2_easy_test/tests/example_nodes/well_behaved.py",
                 "Talker",
             ],
             parameters=[{"/start_value": int(context.perform_substitution(start_value))}],
@@ -24,7 +24,6 @@ def _launch_setup(context: LaunchContext, start_value: LaunchConfiguration):
 
 
 def generate_launch_description() -> LaunchDescription:
-
     declared_arguments = []
 
     declared_arguments.append(
diff --git a/tests/example_nodes/__init__.py b/ros2_easy_test/tests/example_nodes/__init__.py
similarity index 100%
rename from tests/example_nodes/__init__.py
rename to ros2_easy_test/tests/example_nodes/__init__.py
diff --git a/tests/example_nodes/failing.py b/ros2_easy_test/tests/example_nodes/failing.py
similarity index 100%
rename from tests/example_nodes/failing.py
rename to ros2_easy_test/tests/example_nodes/failing.py
diff --git a/tests/example_nodes/run_node.py b/ros2_easy_test/tests/example_nodes/run_node.py
similarity index 100%
rename from tests/example_nodes/run_node.py
rename to ros2_easy_test/tests/example_nodes/run_node.py
diff --git a/tests/example_nodes/well_behaved.py b/ros2_easy_test/tests/example_nodes/well_behaved.py
similarity index 100%
rename from tests/example_nodes/well_behaved.py
rename to ros2_easy_test/tests/example_nodes/well_behaved.py
diff --git a/tests/test_env_coverage.py b/ros2_easy_test/tests/test_env_coverage.py
similarity index 97%
rename from tests/test_env_coverage.py
rename to ros2_easy_test/tests/test_env_coverage.py
index f2be685..a55a0d8 100644
--- a/tests/test_env_coverage.py
+++ b/ros2_easy_test/tests/test_env_coverage.py
@@ -11,7 +11,7 @@
 from ros2_easy_test import ROS2TestEnvironment, with_launch_file, with_single_node
 
 # Helpers
-from . import LAUNCH_FILES, is_ros_version
+from . import LAUNCH_FILES
 
 # Module under test and interfaces
 from .example_nodes.well_behaved import EchoNode, Talker
@@ -91,7 +91,7 @@ def test_mailbox_clearing_no_topics(self, env: ROS2TestEnvironment) -> None:
     @mark.xfail(
         raises=Exception,
         reason="specifiying a wrong message type is a common mistake and shall fail loudly",
-        strict=not is_ros_version("foxy"),  # It does fail on foxy
+        strict=True,
     )
     @with_single_node(EchoNode, watch_topics={"/mouth": Empty})
     def test_wrong_topic_type(self, env: ROS2TestEnvironment) -> None:
diff --git a/tests/test_failing_nodes.py b/ros2_easy_test/tests/test_failing_nodes.py
similarity index 98%
rename from tests/test_failing_nodes.py
rename to ros2_easy_test/tests/test_failing_nodes.py
index 1d8d058..f2c616a 100644
--- a/tests/test_failing_nodes.py
+++ b/ros2_easy_test/tests/test_failing_nodes.py
@@ -15,9 +15,6 @@
 # What we are testing
 from ros2_easy_test import ROS2TestEnvironment, with_launch_file, with_single_node
 
-# Helpers
-from . import is_ros_version
-
 # Module under test and interfaces
 from .example_nodes.failing import (
     NodeRaiseInInit,
@@ -121,7 +118,6 @@ def test_service_callback_failure_sync(self, env: ROS2TestEnvironment) -> None:
         reason="Failures in a service callback should be propagated to the test.",
         strict=False,  # TODO: It would be nice to fix this some time -> make it strict=True
         # At least this one doesn't deadlock, but it still doesn't work
-        run=not is_ros_version("foxy"),  # TODO: Somehow deadlocks
     )
     @with_single_node(NodeRaiseOnRequest)
     def test_service_callback_failure_async(self, env: ROS2TestEnvironment) -> None:
@@ -185,7 +181,6 @@ def test_service_callback_failure_sync(self, env: ROS2TestEnvironment) -> None:
         reason="Failures in a service callback should be propagated to the test.",
         strict=False,  # TODO: It would be nice to fix this some time -> make it strict=True
         # At least this one doesn't deadlock, but it still doesn't work
-        run=not is_ros_version("foxy"),  # TODO: Somehow deadlocks
     )
     @with_launch_file(BASE / "raise_on_request.yaml", warmup_time=2)
     def test_service_callback_failure_async(self, env: ROS2TestEnvironment) -> None:
diff --git a/tests/test_interactions.py b/ros2_easy_test/tests/test_interactions.py
similarity index 97%
rename from tests/test_interactions.py
rename to ros2_easy_test/tests/test_interactions.py
index e1bafba..ec2bf35 100644
--- a/tests/test_interactions.py
+++ b/ros2_easy_test/tests/test_interactions.py
@@ -116,7 +116,6 @@ def test_subscriber_and_publisher(self, env: ROS2TestEnvironment) -> None:
     def test_service(self, env: ROS2TestEnvironment) -> None:
         super().test_service(env)
 
-    @mark.xfail(raises=ValueError, reason="See https://github.com/ros2/rclpy/pull/1129")
     @with_single_node(AddTwoIntsServer)
     def test_service_manual(self, env: ROS2TestEnvironment) -> None:
         super().test_service_manual(env)
@@ -175,7 +174,6 @@ def test_subscriber_and_publisher(self, env: ROS2TestEnvironment) -> None:
     def test_service(self, env: ROS2TestEnvironment) -> None:
         super().test_service(env)
 
-    @mark.xfail(raises=ValueError, reason="See https://github.com/ros2/rclpy/pull/1129")
     @with_launch_file(LAUNCH_FILES / "adder.yaml", warmup_time=2)
     def test_service_manual(self, env: ROS2TestEnvironment) -> None:
         super().test_service_manual(env)
diff --git a/tests/test_timeouts.py b/ros2_easy_test/tests/test_timeouts.py
similarity index 100%
rename from tests/test_timeouts.py
rename to ros2_easy_test/tests/test_timeouts.py
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644
index efb6cf2..0000000
--- a/tests/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from os import environ
-from pathlib import Path
-
-LAUNCH_FILES = Path(__file__).parent / "example_launch_files"
-
-
-def is_ros_version(what: str) -> bool:
-    try:
-        return environ["ROS_DISTRO"].lower() == what.lower()
-    except KeyError:
-        return False
diff --git a/tests/demo/__init__.py b/tests/demo/__init__.py
deleted file mode 100644
index f8da1d3..0000000
--- a/tests/demo/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""These tests serve a dual purpose:
-  1) They showcase how to reuse the library.
-  2) They test the library itself.
-
-"""
diff --git a/tests/example_launch_files/adder.yaml b/tests/example_launch_files/adder.yaml
deleted file mode 100644
index da6b0a5..0000000
--- a/tests/example_launch_files/adder.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-launch:
-- executable:
-    name: AddTwoIntsServer
-    cmd: python tests/example_nodes/run_node.py tests/example_nodes/well_behaved.py AddTwoIntsServer
-    output: screen
diff --git a/tests/example_launch_files/echo.yaml b/tests/example_launch_files/echo.yaml
deleted file mode 100644
index 62ba91f..0000000
--- a/tests/example_launch_files/echo.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-launch:
-- executable:
-    name: EchoNode
-    cmd: python tests/example_nodes/run_node.py tests/example_nodes/well_behaved.py EchoNode
-    output: screen
diff --git a/tests/example_launch_files/raise_in_init.yaml b/tests/example_launch_files/raise_in_init.yaml
deleted file mode 100644
index 0168923..0000000
--- a/tests/example_launch_files/raise_in_init.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-launch:
-- executable:
-    name: NodeRaiseInInit
-    cmd: python tests/example_nodes/run_node.py tests/example_nodes/failing.py NodeRaiseInInit
-    output: screen
diff --git a/tests/example_launch_files/raise_in_timer.yaml b/tests/example_launch_files/raise_in_timer.yaml
deleted file mode 100644
index b6a0c9e..0000000
--- a/tests/example_launch_files/raise_in_timer.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-launch:
-- executable:
-    name: NodeRaiseInTimer
-    cmd: python tests/example_nodes/run_node.py tests/example_nodes/failing.py NodeRaiseInTimer
-    output: screen
diff --git a/tests/example_launch_files/raise_on_request.yaml b/tests/example_launch_files/raise_on_request.yaml
deleted file mode 100644
index 87d5a36..0000000
--- a/tests/example_launch_files/raise_on_request.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-launch:
-- executable:
-    name: NodeRaiseOnRequest
-    cmd: python tests/example_nodes/run_node.py tests/example_nodes/failing.py NodeRaiseOnRequest
-    output: screen
diff --git a/tests/example_launch_files/talker.yaml b/tests/example_launch_files/talker.yaml
deleted file mode 100644
index 8698b7a..0000000
--- a/tests/example_launch_files/talker.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-launch:
-- executable:
-    name: Talker
-    cmd: python tests/example_nodes/run_node.py tests/example_nodes/well_behaved.py Talker
-    output: screen