-
Notifications
You must be signed in to change notification settings - Fork 493
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
Tests for TrajectoryMonitor using dependency injection #570
Conversation
c259fe6
to
8df1ba0
Compare
Codecov Report
@@ Coverage Diff @@
## main #570 +/- ##
==========================================
+ Coverage 55.39% 55.57% +0.19%
==========================================
Files 196 198 +2
Lines 21402 21420 +18
==========================================
+ Hits 11854 11903 +49
+ Misses 9548 9517 -31
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to have tests for this component :)
* | ||
* @param[in] node The ros node | ||
*/ | ||
TrajectoryMonitorMiddlewareHandle(const double sampling_frequency); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type can be just double
. Constness isn't required.
namespace planning_scene_monitor | ||
{ | ||
planning_scene_monitor::TrajectoryMonitorMiddlewareHandle::TrajectoryMonitorMiddlewareHandle( | ||
const double sampling_frequency) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const
can be removed.
EXPECT_CALL(*mock_trajectory_monitor_middleware_handle, sleep).Times(::testing::AtLeast(1)); | ||
|
||
// GIVEN a TrajectoryMonitor is started | ||
planning_scene_monitor::CurrentStateMonitorPtr current_state_monitor; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should it be GIVEN, WHEN and THEN after those two?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jliukkonen the pattern order is this way because of how gmock works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mock middleware handles are moved when creating the current_state_monitor
and trajectory_monitor
too
|
||
// WHEN we call the startTrajectoryMonitor function | ||
trajectory_monitor.startTrajectoryMonitor(); | ||
waitFor(10s, [&]() { return callback_called == true; }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this test run for 10 seconds? Probably should be in millisecond range? Or?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will wait for up to 10s for callback_called to equal true. The actual time this takes is unknown but should be <1ms.
@@ -46,6 +47,12 @@ if(BUILD_TESTING) | |||
test/current_state_monitor_tests.cpp | |||
) | |||
target_link_libraries(current_state_monitor_tests | |||
${MOVEIT_LIB_NAME} | |||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: the formatting looks a bit weird here.
struct MockCurrentStateMonitorMiddlewareHandle : public planning_scene_monitor::CurrentStateMonitor::MiddlewareHandle | ||
{ | ||
MOCK_METHOD(rclcpp::Time, now, (), (const, override)); | ||
MOCK_METHOD(void, createJointStateSubscription, | ||
(const std::string& topic, planning_scene_monitor::JointStateUpdateCallback callback), (override)); | ||
MOCK_METHOD(void, resetJointStateSubscription, (), (override)); | ||
MOCK_METHOD(std::string, getJointStateTopicName, (), (const, override)); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@griswaldbrooks If we start using these in a bunch of tests would it be normal to put them in a header that's included by our tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would, assuming they're exactly the same and reused many times. Normally, my threshold for reuse is about 2. For these, my gut says about 3.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally approve. The only thing I saw was the const in the parameter list that @jliukkonen pointed out and the name of the test. Thank you for doing this!
} | ||
} | ||
|
||
TEST(TrajectoryMonitorTests, test1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Name the test something. Maybe SleepAtleastOnce
?
|
||
// WHEN we call the startTrajectoryMonitor function | ||
trajectory_monitor.startTrajectoryMonitor(); | ||
waitFor(10s, [&]() { return callback_called == true; }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will wait for up to 10s for callback_called to equal true. The actual time this takes is unknown but should be <1ms.
@griswaldbrooks will you give this a quick review. I believe this is ready for merging. |
The failing rolling-testing CI job is a problem with the OSRF debians for rolling testing. |
@tylerjw will do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
namespace planning_scene_monitor | ||
{ | ||
using TrajectoryStateAddedCallback = | ||
boost::function<void(const moveit::core::RobotStateConstPtr&, const rclcpp::Time&)>; | ||
using TrajectoryStateAddedCallback = std::function<void(const moveit::core::RobotStateConstPtr&, const rclcpp::Time&)>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Include-what-you-use would want
#include <rclcpp/rclcpp.hpp> // or #include <rclcpp/time.hpp>
and whatever declares moveit::core::RobotStateConstPtr
but I can't seem to find that...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is #include <moveit/robot_state/robot_state.h>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be, though I suspect it's that MOVEIT
macro, which might be there?
*/ | ||
virtual void sleep() = 0; | ||
}; | ||
|
||
/** @brief Constructor. | ||
*/ | ||
TrajectoryMonitor(const CurrentStateMonitorConstPtr& state_monitor, double sampling_frequency = 0.0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could be convinced otherwise, but I would think that while we're here, we would use proper doxygen comments, especially on the new ctor.
@@ -96,6 +115,7 @@ class TrajectoryMonitor | |||
void recordStates(); | |||
|
|||
CurrentStateMonitorConstPtr current_state_monitor_; | |||
std::unique_ptr<MiddlewareHandle> middleware_handle_; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tylerjw where is moveit style on class member documentation.
https://moveit.ros.org/documentation/contributing/code/ implies http://wiki.ros.org/CppStyleGuide#Documentation to me
All functions, methods, classes, class variables, enumerations, and constants should be documented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would specifically disagree with the idea of documenting class variables. I would prefer we focus on documenting the public interface. I'm not sure what a useful comment on this line would be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider
CurrentStateMonitorConstPtr current_state_monitor_; //!< Samples joint states.
std::unique_ptr<MiddlewareHandle> middleware_handle_; //!< Interface for communicating with ROS.
It may not be part of the public interface, but it is a public facing member.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, what do you mean by "focus on documenting the public interface"? Everything public facing should be documented imho
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are correct that these are not in an impl class. I would still however focus on documenting the parts of a class that the user can access without inheriting or making it a friend class first (that is what I meant by public interface).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MoveIt is not properly pimpl'ed (even in the few places where it is it's somewhat smelly), otherwise this would be a different matter. As MoveIt is generally extendable/hackable on any level, I'm no big fan of having pimpl/private declarations in the framework for most members anyway, but that's an independent discussion.
Either way, adding brief inline comments makes sense when they add additional information and adding them takes less resources than discussing in long threads whether focusing on other things makes more sense 😏
I don't feel the proposed comments add relevant information, but then that's usually just maintainer bias.
[csm] //!< Samples robot states.
is somewhat more accurate, because CSM also updates from TF where required.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
even in the few places where it is it's somewhat smelly
😏
I probably wouldn't advocate for arbitrary use of pimpl (Especially since I generally advocate against pointers where dynamic memory isn't required. Additionally I think ROS developers over use pointers because that's what they were taught).
I know it has a purpose in maintaining a stable ABI.
I am a big fan of composition, though not simply using pimpl without the "p".
Either way, adding brief inline comments makes sense when they add additional information
Mainly I'm just I'm just trying to clarify a point of policy/bike shedding, since the moveit style guide said/implied that it follows the ROS style guide. At previous companies, we documented every member and interface with no ill effects. One might even argue that if you can't come up with a logical and simple comment for the member, perhaps it shouldn't be there.
Regardless it seems that the style that member comments are optional. Perhaps I should update the style webpage @tylerjw? I wonder what else deviates.
[csm] //!< Samples robot states. is somewhat more accurate, because CSM also updates from TF where required.
This was mainly a dig to @tylerjw from last week at our hackathon where we started this since every one was making fun of the name and didnt' really know what the class did until we read the entire implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally I think ROS developers over use pointers because that's what they were taught
Not just taught, but also forced by existing shared_ptr APIs. Fully agreed. 😟
One might even argue that if you can't come up with a logical and simple comment for the member, perhaps it shouldn't be there.
One might just as well argue that if you can't come up with a comment that's more descriptive than the variable/class name, it might just be that the author did a good job naming them 🐈
we documented every member and interface with no ill effects
Adding explanations always makes sense. Period. Adding comments for syntactic reasons, on the other hand, if they don't do more than repeat the identifier, amounts to a NOLINT
flag for the line, so reviewers don't complain about missing comments. :)
It's hard to find the line between if (exists) { // if it exists
and CurrentStateMonitor csm; //!< Samples states
, especially when you're working with the code for years.
virtual ~MiddlewareHandle() = default; | ||
|
||
/** | ||
* @brief Add sleep using rate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is rate
? I don't see that anywhere else in the public interface or documentation for MiddlewareHandle
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Specify the unit (Hz).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, my point is that in this interface, there is no discussion of rate, sampling frequency, or anything like that. Furthermore, sleep
could be implemented without respect to a ROS rate, so a more abstract comment like
/**
* @brief Sleep the handle for some prescribed amount of time.
*/
virtual void sleep() = 0;
#pragma once | ||
|
||
#include <moveit/planning_scene_monitor/trajectory_monitor.h> | ||
#include <moveit/planning_scene_monitor/current_state_monitor.h> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#include <moveit/planning_scene_monitor/current_state_monitor.h> |
MOCK_METHOD(std::string, getJointStateTopicName, (), (const, override)); | ||
}; | ||
|
||
void waitFor(std::chrono::seconds timeout, std::function<bool()> done) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doxygen comment would be good.
Does passing timeout
by const&
make sense @tylerjw https://stackoverflow.com/questions/45740179/why-arent-stdchronoduration-instances-passed-by-value-in-the-standard-libra or are we going to assume we know it's a POD and there's no point or...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we assume these are POD and leave it as is. I didn't know about that... the more you know 🌈
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@griswaldbrooks I love your recent in-depths-backed-up-by-references reviews! They make for great reading! ❤️
If @tylerjw feels it's unnecessary let's leave it as is. My main complain would be the interface itself, because condition variables/void futures should usually be preferred over polling arbitrary condition functions. 🐈
But in this scope I think it's actually bikeshedding. 🤷
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If @tylerjw feels it's unnecessary let's leave it as is
Agreed. Especially since this is test code. I feel like test code, while still requiring high quality, can skip on a few things. Not in this case but in others I might write two functions that did something very similar with the same name in two different namespaces in the same file.
In production code I would never do such a thing, but I think since the scope of test code tends towards being very local, the value of the implementation being nearby and specific, rather than having to understand different initialization options for example if preferred. Though I might be able to be convinced otherwise.
because condition variables/void futures
I also prefer that but something about it in this scope didn't quite hit right. I think it had something to do with being in the same thread but that sounds funny. The atomic is doing something similar-ish. The function isn't arbitrary since it's the only interface the caller has to know if any samples were collected.
🚴
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They make for great reading!
I'm trying to prevent myself from picking up habits that aren't based on particular guidance.
I've lived through a few codebases where things were done they way they are because they were done they way they were.
// GIVEN a TrajectoryMonitor is started | ||
planning_scene_monitor::CurrentStateMonitorPtr current_state_monitor; | ||
|
||
current_state_monitor = std::make_unique<planning_scene_monitor::CurrentStateMonitor>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the declaration and instantiation on different lines? (this sort of practice opens you up to problems).
Also, I'll bet you one keyboard that CurrentStateMonitorPtr
is std::shared_ptr<CurrentStateMonitor>
which would mean you're converting a unique to a shared??? Can you just use auto
or std::unique_ptr<CurrentStateMonitor>
because even if CurrentStateMonitorPtr
is a unique_ptr
the ambiguity is frustrating since the different pointers are used for different ownership lifetimes.
Can you change this to make_shared
?
moveit_ros/planning/planning_scene_monitor/test/trajectory_monitor_tests.cpp
Show resolved
Hide resolved
#include "moveit/utils/robot_model_test_utils.h" | ||
#include "rclcpp/node_interfaces/node_clock_interface.hpp" | ||
#include "rclcpp/node_interfaces/node_topics_interface.hpp" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need any of these?
planning_scene_monitor::TrajectoryMonitor trajectory_monitor{ current_state_monitor, | ||
std::move(mock_trajectory_monitor_middleware_handle), | ||
10.0 }; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add
// Set the trajectory monitor callback so we can block until at least one sample was taken.
In the future @tylerjw , I feel like this is one reuse away from being put into a fixture. Used only once is debatable. I do give pause though after reading https://abseil.io/tips/122
@@ -44,7 +45,16 @@ static const rclcpp::Logger LOGGER = rclcpp::get_logger("moveit_ros.planning_sce | |||
|
|||
planning_scene_monitor::TrajectoryMonitor::TrajectoryMonitor(const CurrentStateMonitorConstPtr& state_monitor, | |||
double sampling_frequency) | |||
: TrajectoryMonitor(state_monitor, std::make_unique<TrajectoryMonitorMiddlewareHandle>(sampling_frequency), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use uniform initialization?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MoveIt does not have a fixed policy on uniform initialization at the moment. I know @rhaschke does not like it very much. I on the other hand am quite a big fan.
It's almost only used in the pilz_industrial_planner
sources (which are currently COLCON_IGNORE
'ed in moveit2 afaik)
and while I would love to see a clang-tidy PR for the whole code base 😎, let's keep the style consistent unless it actually produces cleaner code to use it (e.g. in-class initialization of floating points, etc.).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Arguably, uniform initialization actually solves a problem and is not a manner of style.
While C++ has multiple ways to initialize, the rules can be funny https://isocpp.org/wiki/faq/cpp11-language#uniform-init
And then of course the "most vexing parse" is always avoided.
Personally, I think within a class, the same initialization technique should be used for uniformity (unless of course you're initialing a std::vector
or some such class which has specific initialization semantics for the parens or braces) but I'm not sure why updating to modern idioms shouldn't happen on a class-by-class basis for a codebase. If moveit originally used C-style arrays (such as amcl), and then was ported to C++, I'm not sure why one wouldn't start using std::array
or std::vector
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why updating to modern idioms shouldn't happen on a class-by-class basis for a codebase.
👍
It would be really nice if we could clang-tidy this change to the whole codebase but doing it in each piece as we touch it also seems fine to me. Leaving a piece of code a bit nicer because you were there seems like an ok thing to do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Arguably, uniform initialization actually solves a problem and is not a manner of style. [...] the rules can be funny
Absolutely! It's just that the places you proposed it for are not among the funny rules. And especially for those regular cases opinions differ on what is nicer. Because the whole codebase does not use uniform initialization yet, it makes sense to discuss this separately (maybe add it as a point to the next maintainer meeting and file a separate issue).
--- offtopic ---
actually solves a problem
Currently my favorite problem it *creates* is this
$ cat > foo.cpp <<EOF
#include <iostream>
struct A {
A(const std::initializer_list<double>& x, bool critical= true){
std::cout << critical << "\n";
}
};
int main(){
A a1({0,1,2}, false);
A a2{ {0,1,2}, false };
A a3({0}, false);
A a4{ {0}, false };
return 0;
}
EOF
make foo
./foo
0
0
0
1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, the fun initializer_list constructor parameter. It can both make the creating of objects more intuitive and create a trap for users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see https://godbolt.org/z/GYYqWjoxv
because it's casting the boolean to a double in the initializer list.
A different, but also frustrating example
https://godbolt.org/z/orz8oT4js
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A different, but also frustrating example
Yeah, but at least that's the "classic" one everyone knows about (or at least should after reading Effective Modern C++) :)
: current_state_monitor_(state_monitor) | ||
, middleware_handle_(std::move(middleware_handle)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider (though a separate PR also makes sense) converting to uniform initialization
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
, middleware_handle_(std::move(middleware_handle)) | |
, middleware_handle_{std::move(middleware_handle)} |
⬆️ that is all Griz means btw.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but for all
If you rebase this on main it should no longer fail CI on foxy (it will also no longer run a foxy test) as main is for galactic/rolling now. |
I'm sorry this isn't working because I added another function to the middleware interface for current state monitor. You should be able to fix this by just copying one line for the mock out of the current state monitor test. |
d4318b6
to
7a2f948
Compare
b6565ca
to
d9096cf
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for updating this. I think we should merge this before it goes stale again.
Description
Please explain the changes you made, including a reference to the related issue if applicable
Checklist