Add more comprehensive wl_output tests - #445
Conversation
Replace the two existing wl_output tests with a more thorough set that verifies each property advertised in the initial output advertisement. A mockable WlOutputListener wraps the wl_output events so the tests can assert the advertised geometry, current mode, scale and name via EXPECT_CALL, and confirm the advertisement is terminated by exactly one done event. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR expands wl_output protocol coverage by replacing broad state-based assertions with event-level expectations, allowing tests to validate each advertised property and the done termination event.
Changes:
- Introduces a mockable
WlOutputListenerwrapper that forwardswl_outputevents into virtual methods. - Adds targeted tests for geometry, current mode, scale, name, and “exactly one done” after initial advertisement.
- Refactors the test fixture to include a helper that dispatches until the initial
wl_output.doneis observed.
| EXPECT_CALL(listener, done(_)).WillRepeatedly(Invoke([&done](auto) { done = true; })); | ||
| client.dispatch_until([&done]() { return done; }); |
There was a problem hiding this comment.
This WillRepeatedly lambda may continue to be invoked after the method returns. The reference to the done local will be out of scope at that point, so you're overwriting whatever happens to take its place on the stack.
You probably want a Mock::VerifyAndClearExpectations call to clean things up, and perhaps install a new handler if you want to handle further done events. Check create_xdg_shell_stable_surface for a similar pattern.
There was a problem hiding this comment.
I changed it to ON_CALL/WillByDefault as suggested by Gemini. This is used elsewhere. Do you think this is appropriate?
There was a problem hiding this comment.
I don't think WillByDefault makes things any better, since the callback will still be attached to the mock after the captured reference is out of scope.
Maybe this would be the most concise and correct version?
EXPECT_CALL(listener, done()).WillOnce(Invoke([&done](auto) { done = true; })).RetiresOnSaturation();
That should remove the expectation from the mock after the event is dispatched, so it doesn't last past the end of the function call.
The done listener method is only stubbed to drive the dispatch loop, not asserted, so ON_CALL/WillByDefault expresses the intent better than an EXPECT_CALL with an implicit any-cardinality WillRepeatedly.
Replace the abstract base class plus separate mock subclass and the hand-written static thunks with a single mock class that uses MOCK_METHOD directly and forwards events via inline FORWARD_TO_MOCK lambdas, matching the listener pattern used by the other tests. The redundant wl_output* argument is dropped from each event since a listener is bound to a single output.
jhenstridge
left a comment
There was a problem hiding this comment.
There's a few things that need fixing here, but it mostly comes down to two main issues:
- All the tests that rely on the done event require at least version 2 of the interface.
- The use of
NiceMockmeans unexpected calls will be ignored. It might be worth adding a catch allEXPECT_CALL(listener, method(_)).Times(0);expectation to explicitly fail on events with bad arguments.
| { | ||
| bool done = false; | ||
| ON_CALL(listener, done()).WillByDefault(Invoke([&done]() { done = true; })); | ||
| client.dispatch_until([&done]() { return done; }); |
There was a problem hiding this comment.
There should probably be a VerifyAndClearExpectations call here to ensure the closure is not called again after the done local is out of scope.
I realise that you're only calling this method at the end of test methods, but it would make it more difficult to misuse.
| auto const output = client.bind_if_supported<wl_output>(wlcs::AnyVersion); | ||
| NiceMock<WlOutputListener> listener{output}; | ||
|
|
||
| EXPECT_CALL(listener, scale(Ge(1))); |
There was a problem hiding this comment.
The protocol documentation says this event is optional, so this doesn't seem quite right.
Adding .Times(AtMost(1)) to this expectation would handle the case of the event not being sent. But because you're using NiceMock, any event received with factor < 1 is going to be ignored due to not matching any expectations. So maybe the following would be better?
| EXPECT_CALL(listener, scale(Ge(1))); | |
| EXPECT_CALL(listener, scale(_)).Times(0); | |
| EXPECT_CALL(listener, scale(Ge(1))).Times(AtMost(1)); |
The first expectation will fail on any bad scale events, and the second will accept any good scale events.
It might be worth doing the same for the other tests: while they will fail if an event with the expected arguments doesn't arrive, they won't tell you about unexpected events.
| { | ||
| wlcs::Client client{the_server()}; | ||
|
|
||
| auto const output = client.bind_if_supported<wl_output>(wlcs::AnyVersion); |
There was a problem hiding this comment.
The scale event was added in version 2 of the interface.
| auto const output = client.bind_if_supported<wl_output>(wlcs::AnyVersion); | |
| auto const output = client.bind_if_supported<wl_output>(wlcs::AtLeastVersion{WL_OUTPUT_SCALE_SINCE_VERSION}); |
| { | ||
| wlcs::Client client{the_server()}; | ||
|
|
||
| auto const output = client.bind_if_supported<wl_output>(wlcs::AnyVersion); |
There was a problem hiding this comment.
The done event was added in version 2 of the interface.
| auto const output = client.bind_if_supported<wl_output>(wlcs::AnyVersion); | |
| auto const output = client.bind_if_supported<wl_output>(wlcs::AtLeastVersion{WL_OUTPUT_DONE_SINCE_VERSION}); |
In fact, any tests using the receive_initial_properties helper will fail without that version, so I don't think AnyVersion is appropriate for any of the tests here.
| auto const output = client.bind_if_supported<wl_output>(wlcs::AtLeastVersion{WL_OUTPUT_NAME_SINCE_VERSION}); | ||
| NiceMock<WlOutputListener> listener{output}; | ||
|
|
||
| EXPECT_CALL(listener, name(StrNe(""))); |
There was a problem hiding this comment.
It might also be useful to check that no two outputs return the same name, but I'm not sure how you'd bind the other outputs via bind_if_supported.
Replace the two existing wl_output tests with a more thorough set that verifies each property advertised in the initial output advertisement.
A mockable WlOutputListener wraps the wl_output events so the tests can assert the advertised geometry, current mode, scale and name via EXPECT_CALL, and confirm the advertisement is terminated by exactly one done event.