-
Notifications
You must be signed in to change notification settings - Fork 2
Reach every recording a fault kept, not just the newest #95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
05100fd
fix: reach every recording a fault kept, not just the newest
mfaferek93 45a0497
test(e2e): prove each recording downloads its own bytes
mfaferek93 2ed0bb1
test(e2e): skip the rosbag specs when their stack is absent
mfaferek93 2bf28d8
fix(ui): address multi-rosbag review round
mfaferek93 493119b
fix(e2e): source in statements, not in the backgrounded chain
mfaferek93 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # Stack for the rosbag-history specs: a gateway AND a fault manager, so a fault | ||
| # can actually own black-box recordings. docker-compose.yml next to this file | ||
| # runs a manifest-only gateway with no fault manager at all, which cannot | ||
| # produce a single bag; the two scenarios are kept apart rather than merged so | ||
| # neither has to carry the other's configuration. | ||
| services: | ||
| gateway: | ||
| # Overridable because the recording-id contract these specs assert on | ||
| # (ros2_medkit#620) is newer than any published tag: point this at a | ||
| # locally built image to run them before that lands. Once it is | ||
| # published, pin a digest here the way docker-compose.yml does. | ||
| image: ${E2E_ROSBAG_GATEWAY_IMAGE:-ghcr.io/selfpatch/ros2_medkit-jazzy:latest} | ||
| ports: | ||
| # Loopback only, and on its own port so this stack can run alongside | ||
| # the scripts one without either stealing the other's. | ||
| - '127.0.0.1:${E2E_ROSBAG_GATEWAY_PORT:-8081}:8080' | ||
| volumes: | ||
| - ./gateway/rosbag-params.yaml:/e2e/params.yaml:ro | ||
| - ./gateway/seed_recordings.py:/e2e/seed_recordings.py:ro | ||
|
mfaferek93 marked this conversation as resolved.
|
||
| - e2e-bags:/e2e-bags | ||
| entrypoint: ['/bin/bash', '-lc'] | ||
| command: | ||
| - > | ||
| source /opt/ros/jazzy/setup.bash && | ||
|
mfaferek93 marked this conversation as resolved.
Outdated
|
||
| source /home/medkit/ws/install/setup.bash && | ||
| ros2 run ros2_medkit_fault_manager fault_manager_node | ||
| --ros-args --params-file /e2e/params.yaml & | ||
| ros2 run ros2_medkit_gateway gateway_node | ||
| --ros-args --params-file /e2e/params.yaml | ||
| depends_on: | ||
| init-bags: | ||
| condition: service_completed_successfully | ||
| # The gateway image runs as uid 999 and a fresh named volume is root-owned, | ||
| # so the fault manager could not write a bag into it. Same one-shot chown | ||
| # the scripts stack does for its upload volume. | ||
| init-bags: | ||
| image: ${E2E_ROSBAG_GATEWAY_IMAGE:-ghcr.io/selfpatch/ros2_medkit-jazzy:latest} | ||
| user: root | ||
| volumes: | ||
| - e2e-bags:/e2e-bags | ||
| entrypoint: ['chown', '-R', '999:999', '/e2e-bags'] | ||
| volumes: | ||
| e2e-bags: | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # Gateway + fault manager for the rosbag-history scenario. | ||
| # | ||
| # Separate from params.yaml on purpose: that stack is manifest-only with script | ||
| # uploads and no fault manager at all, and this one needs the opposite - live | ||
| # ROS discovery so the seeded fault's reporting source resolves to an app, and a | ||
| # fault manager configured to keep a HISTORY of black-box recordings rather than | ||
| # overwriting on every re-confirmation. | ||
| /**: | ||
| ros__parameters: | ||
| server: | ||
| host: '0.0.0.0' | ||
| port: 8080 | ||
| cors: | ||
| # Same reasoning as params.yaml: the browser's origin is the dev | ||
| # server, not the gateway, and E2E_APP_URL is overridable. | ||
| allowed_origins: | ||
| - '*' | ||
| # Rosbag retention. 3 leaves headroom above the two occurrences the seed | ||
| # drives, so a failing spec means "a recording was lost", not "the cap | ||
| # trimmed one". | ||
| snapshots: | ||
| rosbag: | ||
| enabled: true | ||
| duration_sec: 2.0 | ||
| duration_after_sec: 0.5 | ||
| include_topics: ['/e2e/probe'] | ||
| format: 'mcap' | ||
| storage_path: '/e2e-bags' | ||
| max_bags_per_fault: 3 | ||
| # Acknowledging a fault must not delete the evidence it just | ||
| # produced - the scenario is confirm, acknowledge, confirm again. | ||
| auto_cleanup: false | ||
| lazy_start: false | ||
| confirmation_threshold: -1 | ||
| storage_type: 'sqlite' | ||
| database_path: '/e2e-bags/faults.db' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| #!/usr/bin/env python3 | ||
| # Copyright 2026 mfaferek93 | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """Leave one fault holding two black-box recordings, for the browser to click on. | ||
|
|
||
| Everything below the fault report is real: the fault manager runs its own | ||
| capture, records a topic that is genuinely being published, and writes two | ||
| separate bags to disk. Only the trigger is a service call rather than a sensor | ||
| detecting its own misconfiguration - the subject of these specs is the web UI, | ||
| and the demo nodes that detect faults on their own are not shipped in the | ||
| gateway image. | ||
|
|
||
| The fault is confirmed, acknowledged, then confirmed again: that is the sequence | ||
| that used to leave a single recording behind, because the second one overwrote | ||
| the first (ros2_medkit#620). | ||
| """ | ||
|
|
||
| import sys | ||
| import time | ||
|
|
||
| import rclpy | ||
| from rclpy.node import Node | ||
| from rclpy.qos import HistoryPolicy, QoSProfile, ReliabilityPolicy | ||
| from ros2_medkit_msgs.msg import Fault | ||
| from ros2_medkit_msgs.srv import ClearFault, ReportFault | ||
| from std_msgs.msg import Float32 | ||
|
|
||
| FAULT_CODE = 'E2E_FLAPPING_SENSOR' | ||
| SOURCE_ID = '/e2e/probe_publisher' | ||
|
mfaferek93 marked this conversation as resolved.
Outdated
|
||
| PROBE_TOPIC = '/e2e/probe' | ||
| # Must exceed the configured duration_sec so the ring buffer holds a full window | ||
| # before each confirmation; a bag flushed from an empty buffer has no content. | ||
| FILL_SECONDS = 3.0 | ||
|
|
||
|
|
||
| class Seeder(Node): | ||
| def __init__(self): | ||
| super().__init__('e2e_rosbag_seeder') | ||
| qos = QoSProfile( | ||
| reliability=ReliabilityPolicy.BEST_EFFORT, | ||
| history=HistoryPolicy.KEEP_LAST, | ||
| depth=10, | ||
| ) | ||
| self.pub = self.create_publisher(Float32, PROBE_TOPIC, qos) | ||
| self.report = self.create_client(ReportFault, '/fault_manager/report_fault') | ||
| self.clear = self.create_client(ClearFault, '/fault_manager/clear_fault') | ||
|
|
||
| def wait_for_services(self, timeout=90.0): | ||
| for client, name in ((self.report, 'report_fault'), (self.clear, 'clear_fault')): | ||
| if not client.wait_for_service(timeout_sec=timeout): | ||
| raise SystemExit(f'{name} service never appeared') | ||
|
|
||
| def publish_for(self, seconds, rate_hz=20.0): | ||
| msg = Float32() | ||
| msg.data = 1.0 | ||
| deadline = time.time() + seconds | ||
| period = 1.0 / rate_hz | ||
| while time.time() < deadline: | ||
| self.pub.publish(msg) | ||
| rclpy.spin_once(self, timeout_sec=0.0) | ||
| time.sleep(period) | ||
|
|
||
| def call(self, client, request): | ||
| future = client.call_async(request) | ||
| rclpy.spin_until_future_complete(self, future, timeout_sec=20.0) | ||
| if future.result() is None: | ||
| raise SystemExit('service call timed out') | ||
| return future.result() | ||
|
|
||
| def confirm(self): | ||
| request = ReportFault.Request() | ||
| request.fault_code = FAULT_CODE | ||
| request.event_type = ReportFault.Request.EVENT_FAILED | ||
| request.severity = Fault.SEVERITY_ERROR | ||
| request.description = 'Intermittent sensor dropout seen twice' | ||
| request.source_id = SOURCE_ID | ||
| return self.call(self.report, request) | ||
|
|
||
| def acknowledge(self): | ||
| request = ClearFault.Request() | ||
| request.fault_code = FAULT_CODE | ||
| return self.call(self.clear, request) | ||
|
|
||
|
|
||
| def main(): | ||
| rclpy.init() | ||
| node = Seeder() | ||
| node.wait_for_services() | ||
|
|
||
| # First occurrence. | ||
| node.publish_for(FILL_SECONDS) | ||
| node.confirm() | ||
| node.publish_for(FILL_SECONDS) # post-roll window, then finalize | ||
| node.acknowledge() | ||
|
|
||
| # Second occurrence. Before #620 this one replaced the first recording | ||
| # outright, so the fault ended up with exactly one bag either way. | ||
| node.publish_for(FILL_SECONDS) | ||
| node.confirm() | ||
| node.publish_for(FILL_SECONDS) | ||
|
|
||
| # Deliberately NOT acknowledged: a cleared fault drops out of the default | ||
| # CONFIRMED-only listing, so acknowledging this one too would leave the specs | ||
| # with two bags on disk and no fault on screen pointing at them. | ||
| print('SEEDED', flush=True) | ||
| node.destroy_node() | ||
| rclpy.shutdown() | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| sys.exit(main()) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.