Skip to content

Commit f2a151c

Browse files
committed
[#3676] Fix reporting of processed commands in ITs
The MQTT based integration tests have been improved to correctly detect and report the fact that not all one-way commands that have been sent also have been processed by the device. Fixes #3676
1 parent 7e9183a commit f2a151c

File tree

1 file changed

+74
-76
lines changed

1 file changed

+74
-76
lines changed

tests/src/test/java/org/eclipse/hono/tests/mqtt/CommandAndControlMqttIT.java

+74-76
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2019, 2022 Contributors to the Eclipse Foundation
2+
* Copyright (c) 2019 Contributors to the Eclipse Foundation
33
*
44
* See the NOTICE file(s) distributed with this work for additional
55
* information regarding copyright ownership.
@@ -75,7 +75,6 @@
7575
import io.vertx.mqtt.messages.MqttPublishMessage;
7676
import io.vertx.proton.ProtonHelper;
7777

78-
7978
/**
8079
* Integration tests for sending commands to device connected to the MQTT adapter.
8180
*
@@ -164,7 +163,7 @@ public void testSendOneWayCommandSucceeds(
164163
final String commandTargetDeviceId = endpointConfig.isSubscribeAsGateway()
165164
? helper.setupGatewayDeviceBlocking(tenantId, deviceId, 5)
166165
: deviceId;
167-
final Checkpoint commandsReceived = ctx.checkpoint(COMMANDS_TO_SEND);
166+
final var remainingCommandsToBeProcessed = new CountDownLatch(COMMANDS_TO_SEND);
168167

169168
final AtomicInteger counter = new AtomicInteger();
170169
testSendCommandSucceeds(ctx, commandTargetDeviceId, msg -> {
@@ -173,7 +172,7 @@ public void testSendOneWayCommandSucceeds(
173172
ctx.verify(() -> {
174173
endpointConfig.assertCommandPublishTopicStructure(topic, commandTargetDeviceId, true, "setValue");
175174
});
176-
commandsReceived.flag();
175+
remainingCommandsToBeProcessed.countDown();
177176
}, payload -> {
178177
counter.incrementAndGet();
179178
return helper.sendOneWayCommand(
@@ -183,7 +182,7 @@ public void testSendOneWayCommandSucceeds(
183182
"text/plain",
184183
payload,
185184
helper.getSendCommandTimeout(counter.get() == 1));
186-
}, endpointConfig, COMMANDS_TO_SEND, MqttQoS.AT_MOST_ONCE);
185+
}, endpointConfig, remainingCommandsToBeProcessed, MqttQoS.AT_MOST_ONCE);
187186
}
188187

189188
/**
@@ -196,6 +195,7 @@ public void testSendOneWayCommandSucceeds(
196195
*/
197196
@ParameterizedTest(name = IntegrationTestSupport.PARAMETERIZED_TEST_NAME_PATTERN)
198197
@MethodSource("allCombinations")
198+
@Timeout(timeUnit = TimeUnit.SECONDS, value = 10)
199199
public void testSendCommandSucceedsWithQos0(
200200
final MqttCommandEndpointConfiguration endpointConfig,
201201
final VertxTestContext ctx) throws InterruptedException {
@@ -213,6 +213,7 @@ public void testSendCommandSucceedsWithQos0(
213213
*/
214214
@ParameterizedTest(name = IntegrationTestSupport.PARAMETERIZED_TEST_NAME_PATTERN)
215215
@MethodSource("allCombinations")
216+
@Timeout(timeUnit = TimeUnit.SECONDS, value = 10)
216217
public void testSendCommandSucceedsWithQos1(
217218
final MqttCommandEndpointConfiguration endpointConfig,
218219
final VertxTestContext ctx) throws InterruptedException {
@@ -230,6 +231,7 @@ private void testSendCommandSucceeds(
230231
: deviceId;
231232

232233
final AtomicInteger counter = new AtomicInteger();
234+
final var remainingCommandsToBeProcessed = new CountDownLatch(COMMANDS_TO_SEND);
233235
testSendCommandSucceeds(ctx, commandTargetDeviceId, msg -> {
234236
LOGGER.trace("received command [{}]", msg.topicName());
235237
final ResourceIdentifier topic = ResourceIdentifier.fromString(msg.topicName());
@@ -267,9 +269,10 @@ private void testSendCommandSucceeds(
267269
DownstreamMessageAssertions.assertCommandAndControlApiProperties(
268270
response, tenantId, commandTargetDeviceId);
269271
});
272+
remainingCommandsToBeProcessed.countDown();
270273
return (Void) null;
271274
});
272-
}, endpointConfig, COMMANDS_TO_SEND, qos);
275+
}, endpointConfig, remainingCommandsToBeProcessed, qos);
273276
}
274277

275278
private void testSendCommandSucceeds(
@@ -278,27 +281,29 @@ private void testSendCommandSucceeds(
278281
final Handler<MqttPublishMessage> commandConsumer,
279282
final Function<Buffer, Future<Void>> commandSender,
280283
final MqttCommandEndpointConfiguration endpointConfig,
281-
final int totalNoOfCommandsToSend,
284+
final CountDownLatch remainingCommandsToBeProcessed,
282285
final MqttQoS subscribeQos) throws InterruptedException {
283286

284287
final VertxTestContext setup = new VertxTestContext();
285288
final Checkpoint ready = setup.checkpoint(2);
286289

287290
helper.registry
288-
.addDeviceToTenant(tenantId, deviceId, password)
289-
.compose(ok -> connectToAdapter(IntegrationTestSupport.getUsername(deviceId, tenantId), password))
290-
.compose(ok -> createConsumer(tenantId, msg -> {
291-
// expect empty notification with TTD -1
292-
setup.verify(() -> assertThat(msg.getContentType()).isEqualTo(EventConstants.CONTENT_TYPE_EMPTY_NOTIFICATION));
293-
final TimeUntilDisconnectNotification notification = msg.getTimeUntilDisconnectNotification().orElse(null);
294-
LOGGER.info("received notification [{}]", notification);
295-
setup.verify(() -> assertThat(notification).isNotNull());
296-
if (notification.getTtd() == -1) {
297-
ready.flag();
298-
}
299-
}))
300-
.compose(conAck -> subscribeToCommands(commandTargetDeviceId, commandConsumer, endpointConfig, subscribeQos))
301-
.onComplete(setup.succeeding(ok -> ready.flag()));
291+
.addDeviceToTenant(tenantId, deviceId, password)
292+
.compose(ok -> connectToAdapter(IntegrationTestSupport.getUsername(deviceId, tenantId), password))
293+
.compose(ok -> createConsumer(tenantId, msg -> {
294+
// expect empty notification with TTD -1
295+
setup.verify(() -> assertThat(msg.getContentType())
296+
.isEqualTo(EventConstants.CONTENT_TYPE_EMPTY_NOTIFICATION));
297+
final TimeUntilDisconnectNotification notification = msg.getTimeUntilDisconnectNotification()
298+
.orElse(null);
299+
LOGGER.info("received notification [{}]", notification);
300+
setup.verify(() -> assertThat(notification).isNotNull());
301+
if (notification.getTtd() == -1) {
302+
ready.flag();
303+
}
304+
}))
305+
.compose(conAck -> subscribeToCommands(commandTargetDeviceId, commandConsumer, endpointConfig, subscribeQos))
306+
.onComplete(setup.succeeding(ok -> ready.flag()));
302307

303308
assertWithMessage("setup of adapter finished within %s seconds", IntegrationTestSupport.getTestSetupTimeout())
304309
.that(setup.awaitCompletion(IntegrationTestSupport.getTestSetupTimeout(), TimeUnit.SECONDS))
@@ -307,30 +312,23 @@ private void testSendCommandSucceeds(
307312
ctx.failNow(setup.causeOfFailure());
308313
return;
309314
}
310-
311-
final Checkpoint sendCommandsSucceeded = ctx.checkpoint();
312-
final CountDownLatch commandsSucceeded = new CountDownLatch(totalNoOfCommandsToSend);
315+
final var totalNoOfCommandsToSend = remainingCommandsToBeProcessed.getCount();
313316
final AtomicInteger commandsSent = new AtomicInteger(0);
314-
final AtomicLong lastReceivedTimestamp = new AtomicLong(0);
317+
final AtomicLong lastSentTimestamp = new AtomicLong(0);
315318
final long start = System.currentTimeMillis();
316319

317320
while (commandsSent.get() < totalNoOfCommandsToSend) {
318321
final CountDownLatch commandSent = new CountDownLatch(1);
319322
context.runOnContext(go -> {
320-
commandsSent.getAndIncrement();
323+
commandsSent.incrementAndGet();
321324
final Buffer msg = commandsSent.get() % 2 == 0
322325
? Buffer.buffer("value: " + commandsSent.get())
323326
: null; // use 'null' payload for half the commands, ensuring such commands also get forwarded
324327
commandSender.apply(msg).onComplete(sendAttempt -> {
325328
if (sendAttempt.failed()) {
326329
LOGGER.info("error sending command {}", commandsSent.get(), sendAttempt.cause());
327-
} else {
328-
lastReceivedTimestamp.set(System.currentTimeMillis());
329-
commandsSucceeded.countDown();
330-
if (commandsSucceeded.getCount() % 20 == 0) {
331-
LOGGER.info("commands succeeded: {}", totalNoOfCommandsToSend - commandsSucceeded.getCount());
332-
}
333330
}
331+
lastSentTimestamp.set(System.currentTimeMillis());
334332
if (commandsSent.get() % 20 == 0) {
335333
LOGGER.info("commands sent: " + commandsSent.get());
336334
}
@@ -342,20 +340,20 @@ private void testSendCommandSucceeds(
342340
}
343341

344342
final long timeToWait = totalNoOfCommandsToSend * 200;
345-
if (!commandsSucceeded.await(timeToWait, TimeUnit.MILLISECONDS)) {
346-
LOGGER.info("Timeout of {} milliseconds reached, stop waiting for commands to succeed", timeToWait);
343+
if (!remainingCommandsToBeProcessed.await(timeToWait, TimeUnit.MILLISECONDS)) {
344+
LOGGER.info("Timeout of {} milliseconds reached, stop waiting for commands to be processed", timeToWait);
347345
}
348-
if (lastReceivedTimestamp.get() == 0L) {
346+
if (lastSentTimestamp.get() == 0L) {
349347
// no message has been received at all
350-
lastReceivedTimestamp.set(System.currentTimeMillis());
348+
lastSentTimestamp.set(System.currentTimeMillis());
351349
}
352-
final long commandsCompleted = totalNoOfCommandsToSend - commandsSucceeded.getCount();
353-
LOGGER.info("commands sent: {}, commands succeeded: {} after {} milliseconds",
354-
commandsSent.get(), commandsCompleted, lastReceivedTimestamp.get() - start);
355-
if (commandsCompleted == commandsSent.get()) {
356-
sendCommandsSucceeded.flag();
350+
final long commandsProcessed = totalNoOfCommandsToSend - remainingCommandsToBeProcessed.getCount();
351+
LOGGER.info("commands sent: {}, commands processed: {} after {} milliseconds",
352+
commandsSent.get(), commandsProcessed, lastSentTimestamp.get() - start);
353+
if (commandsProcessed == commandsSent.get()) {
354+
ctx.completeNow();
357355
} else {
358-
ctx.failNow(new IllegalStateException("did not complete all commands sent"));
356+
ctx.failNow(new IllegalStateException("device did not process all commands that had been sent"));
359357
}
360358
}
361359

@@ -394,18 +392,18 @@ public void testSendCommandViaAmqpFailsForMalformedMessage(
394392
ready.flag();
395393
}
396394
})
397-
.compose(consumer -> helper.registry.addDeviceToTenant(tenantId, deviceId, password))
398-
.compose(ok -> connectToAdapter(IntegrationTestSupport.getUsername(deviceId, tenantId), password))
399-
.compose(conAck -> subscribeToCommands(commandTargetDeviceId, msg -> {
400-
// all commands should get rejected because they fail to pass the validity check
401-
ctx.failNow(new IllegalStateException("should not have received command"));
402-
}, endpointConfig, MqttQoS.AT_MOST_ONCE))
403-
.compose(ok -> helper.createGenericAmqpMessageSender(endpointConfig.getNorthboundEndpoint(), tenantId))
404-
.onComplete(setup.succeeding(genericSender -> {
405-
LOGGER.debug("created generic sender for sending commands [target address: {}]", linkTargetAddress);
406-
amqpCmdSenderRef.set(genericSender);
407-
ready.flag();
408-
}));
395+
.compose(consumer -> helper.registry.addDeviceToTenant(tenantId, deviceId, password))
396+
.compose(ok -> connectToAdapter(IntegrationTestSupport.getUsername(deviceId, tenantId), password))
397+
.compose(conAck -> subscribeToCommands(commandTargetDeviceId, msg -> {
398+
// all commands should get rejected because they fail to pass the validity check
399+
ctx.failNow(new IllegalStateException("should not have received command"));
400+
}, endpointConfig, MqttQoS.AT_MOST_ONCE))
401+
.compose(ok -> helper.createGenericAmqpMessageSender(endpointConfig.getNorthboundEndpoint(), tenantId))
402+
.onComplete(setup.succeeding(genericSender -> {
403+
LOGGER.debug("created generic sender for sending commands [target address: {}]", linkTargetAddress);
404+
amqpCmdSenderRef.set(genericSender);
405+
ready.flag();
406+
}));
409407

410408
assertWithMessage("setup of adapter finished within %s seconds", IntegrationTestSupport.getTestSetupTimeout())
411409
.that(setup.awaitCompletion(IntegrationTestSupport.getTestSetupTimeout(), TimeUnit.SECONDS))
@@ -423,10 +421,11 @@ public void testSendCommandViaAmqpFailsForMalformedMessage(
423421
messageWithoutSubject.setAddress(messageAddress);
424422
messageWithoutSubject.setMessageId("message-id");
425423
messageWithoutSubject.setReplyTo("reply/to/address");
426-
amqpCmdSenderRef.get().sendAndWaitForOutcome(messageWithoutSubject, NoopSpan.INSTANCE).onComplete(ctx.failing(t -> {
427-
ctx.verify(() -> assertThat(t).isInstanceOf(ClientErrorException.class));
428-
failedAttempts.flag();
429-
}));
424+
amqpCmdSenderRef.get().sendAndWaitForOutcome(messageWithoutSubject, NoopSpan.INSTANCE)
425+
.onComplete(ctx.failing(t -> {
426+
ctx.verify(() -> assertThat(t).isInstanceOf(ClientErrorException.class));
427+
failedAttempts.flag();
428+
}));
430429

431430
LOGGER.debug("sending command message lacking message ID and correlation ID");
432431
final Message messageWithoutId = ProtonHelper.message("input data");
@@ -465,12 +464,13 @@ public void testSendCommandViaKafkaFailsForMalformedMessage(
465464
final VertxTestContext setup = new VertxTestContext();
466465
final Checkpoint ready = setup.checkpoint(2);
467466

468-
final Future<MessageConsumer> kafkaAsyncErrorResponseConsumer = helper.createDeliveryFailureCommandResponseConsumer(
469-
ctx,
470-
tenantId,
471-
HttpURLConnection.HTTP_BAD_REQUEST,
472-
response -> expectedCommandResponses.countDown(),
473-
null);
467+
final Future<MessageConsumer> kafkaAsyncErrorResponseConsumer = helper
468+
.createDeliveryFailureCommandResponseConsumer(
469+
ctx,
470+
tenantId,
471+
HttpURLConnection.HTTP_BAD_REQUEST,
472+
response -> expectedCommandResponses.countDown(),
473+
null);
474474

475475
createConsumer(tenantId, msg -> {
476476
// expect empty notification with TTD -1
@@ -505,8 +505,7 @@ public void testSendCommandViaKafkaFailsForMalformedMessage(
505505
final Map<String, Object> properties1 = Map.of(
506506
MessageHelper.APP_PROPERTY_DEVICE_ID, deviceId,
507507
MessageHelper.SYS_PROPERTY_CONTENT_TYPE, MessageHelper.CONTENT_TYPE_OCTET_STREAM,
508-
KafkaRecordHelper.HEADER_RESPONSE_REQUIRED, true
509-
);
508+
KafkaRecordHelper.HEADER_RESPONSE_REQUIRED, true);
510509
kafkaSenderRef.get().sendAndWaitForOutcome(commandTopic, tenantId, deviceId, Buffer.buffer(), properties1)
511510
.onComplete(ctx.succeeding(ok -> {}));
512511

@@ -516,8 +515,7 @@ public void testSendCommandViaKafkaFailsForMalformedMessage(
516515
MessageHelper.SYS_PROPERTY_CORRELATION_ID, correlationId,
517516
MessageHelper.APP_PROPERTY_DEVICE_ID, deviceId,
518517
MessageHelper.SYS_PROPERTY_CONTENT_TYPE, MessageHelper.CONTENT_TYPE_OCTET_STREAM,
519-
KafkaRecordHelper.HEADER_RESPONSE_REQUIRED, true
520-
);
518+
KafkaRecordHelper.HEADER_RESPONSE_REQUIRED, true);
521519
kafkaSenderRef.get().sendAndWaitForOutcome(commandTopic, tenantId, deviceId, Buffer.buffer(), properties2)
522520
.onComplete(ctx.succeeding(ok -> {}));
523521

@@ -576,7 +574,7 @@ public void testSendCommandFailsForCommandNotAcknowledgedByDevice(
576574
counter.incrementAndGet();
577575
return helper.sendCommand(tenantId, commandTargetDeviceId, "setValue", "text/plain", payload,
578576
helper.getSendCommandTimeout(counter.get() == 1))
579-
.mapEmpty();
577+
.mapEmpty();
580578
};
581579

582580
helper.registry
@@ -703,16 +701,16 @@ private Future<Void> injectMqttClientPubAckBlocker(final AtomicBoolean outboundP
703701
final NetSocketInternal connection = (NetSocketInternal) connectionMethod.invoke(mqttClient);
704702
connection.channelHandlerContext().pipeline().addBefore("handler", "OutboundPubAckBlocker",
705703
new ChannelOutboundHandlerAdapter() {
706-
@Override
704+
@Override
707705
public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise)
708-
throws Exception {
706+
throws Exception {
709707
if (outboundPubAckBlocked.get() && msg instanceof io.netty.handler.codec.mqtt.MqttPubAckMessage) {
710708
LOGGER.debug("suppressing PubAck, message id: {}", ((MqttPubAckMessage) msg).variableHeader().messageId());
711-
} else {
712-
super.write(ctx, msg, promise);
713-
}
714-
}
715-
});
709+
} else {
710+
super.write(ctx, msg, promise);
711+
}
712+
}
713+
});
716714
return Future.succeededFuture();
717715
} catch (final Exception e) {
718716
LOGGER.error("failed to inject PubAck blocking handler");

0 commit comments

Comments
 (0)