Skip to content
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

Merged
merged 15 commits into from
Nov 15, 2021

Conversation

Abishalini
Copy link
Contributor

@Abishalini Abishalini commented Jul 23, 2021

Description

Please explain the changes you made, including a reference to the related issue if applicable

Checklist

  • Required by CI: Code is auto formatted using clang-format
  • Extend the tutorials / documentation reference
  • Document API changes relevant to the user in the MIGRATION.md notes
  • Create tests, which fail without this PR reference
  • Include a screenshot if changing a GUI
  • While waiting for someone to review your request, please help review another open pull request to support the maintainers

@codecov
Copy link

codecov bot commented Jul 23, 2021

Codecov Report

Merging #570 (3a0030b) into main (55a74e9) will increase coverage by 0.19%.
The diff coverage is 27.78%.

Impacted file tree graph

@@            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     
Impacted Files Coverage Δ
...moveit/planning_scene_monitor/trajectory_monitor.h 75.00% <0.00%> (ø)
...nitor/src/trajectory_monitor_middleware_handle.cpp 0.00% <0.00%> (ø)
.../planning_scene_monitor/src/trajectory_monitor.cpp 66.08% <62.50%> (+66.08%) ⬆️
...dl_kinematics_plugin/src/kdl_kinematics_plugin.cpp 75.93% <0.00%> (+1.12%) ⬆️
...anning_scene_monitor/src/current_state_monitor.cpp 77.14% <0.00%> (+1.80%) ⬆️
...eit/planning_scene_monitor/current_state_monitor.h 100.00% <0.00%> (+22.23%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 55a74e9...3a0030b. Read the comment docs.

Copy link
Contributor

@jliukkonen jliukkonen left a 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);
Copy link
Contributor

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)
Copy link
Contributor

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;
Copy link
Contributor

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?

Copy link
Member

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.

Copy link
Contributor Author

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; });
Copy link
Contributor

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?

Copy link
Member

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}
)
Copy link
Contributor

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.

Comment on lines 57 to 69
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));
};
Copy link
Member

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?

Copy link
Contributor

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.

Copy link
Member

@tylerjw tylerjw left a 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)
Copy link
Member

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; });
Copy link
Member

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.

@tylerjw
Copy link
Member

tylerjw commented Jul 26, 2021

@griswaldbrooks will you give this a quick review. I believe this is ready for merging.

@tylerjw
Copy link
Member

tylerjw commented Jul 26, 2021

The failing rolling-testing CI job is a problem with the OSRF debians for rolling testing.

@griswaldbrooks
Copy link
Contributor

@tylerjw will do

Copy link
Contributor

@griswaldbrooks griswaldbrooks left a 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&)>;
Copy link
Contributor

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...

Copy link
Contributor Author

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>

Copy link
Contributor

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);
Copy link
Contributor

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_;
Copy link
Contributor

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.

Copy link
Member

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?

Copy link
Contributor

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.

Copy link
Contributor

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

Copy link
Member

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).

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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
Copy link
Contributor

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specify the unit (Hz).

Copy link
Contributor

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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#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)
Copy link
Contributor

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...

Copy link
Member

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 🌈

Copy link
Contributor

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. 🤷

Copy link
Contributor

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.

🚴

Copy link
Contributor

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>(
Copy link
Contributor

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.

https://stackoverflow.com/questions/37884728/does-c11-unique-ptr-and-shared-ptr-able-to-convert-to-each-others-type

Can you change this to make_shared?

Comment on lines 44 to 46
#include "moveit/utils/robot_model_test_utils.h"
#include "rclcpp/node_interfaces/node_clock_interface.hpp"
#include "rclcpp/node_interfaces/node_topics_interface.hpp"
Copy link
Contributor

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 };

Copy link
Contributor

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),
Copy link
Contributor

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?

Copy link
Contributor

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.).

Copy link
Contributor

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.

Copy link
Member

@tylerjw tylerjw Jul 28, 2021

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.

Copy link
Contributor

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

Copy link
Member

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.

Copy link
Contributor

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

Copy link
Contributor

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))
Copy link
Contributor

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
, middleware_handle_(std::move(middleware_handle))
, middleware_handle_{std::move(middleware_handle)}

⬆️ that is all Griz means btw.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but for all

@tylerjw
Copy link
Member

tylerjw commented Aug 2, 2021

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.

@tylerjw
Copy link
Member

tylerjw commented Aug 11, 2021

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.

Copy link
Member

@tylerjw tylerjw left a 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.

@tylerjw tylerjw merged commit 542c5f7 into moveit:main Nov 15, 2021
@Abishalini Abishalini deleted the trajectory_monitor_DI branch November 18, 2021 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants