diff --git a/event-statistics/README.md b/event-statistics/README.md index ad734c3ef..8f1bd6a17 100644 --- a/event-statistics/README.md +++ b/event-statistics/README.md @@ -27,7 +27,18 @@ This service also has its own UI where you can see the top winners and the perce ### Team Stats Team stats are accumulated by the number of wins by heroes vs villains. It is calculated as a percentage of hero wins to villain wins. -Team stats are then sent over the `/stats/team` [WebSocket](https://en.wikipedia.org/wiki/WebSocket) by the [`TeamStatsWebSocket`](src/main/java/io/quarkus/sample/superheroes/statistics/endpoint/TeamStatsWebSocket.java) WebSocket class. Every time a new fight event is received, the team stats are re-computed and the new hero-to-winner percentage is emitted to anyone listening on the WebSocket. +Team stats are then sent over the `/stats/team` [WebSocket](https://en.wikipedia.org/wiki/WebSocket) by the [`TeamStatsWebSocket`](src/main/java/io/quarkus/sample/superheroes/statistics/endpoint/TeamStatsWebSocket.java) WebSocket class. Every time a new fight event is received, the team stats are re-computed and a JSON structure is emitted to anyone listening on the WebSocket. + +A sample payload might look like this: + +```json +{ + "heroWins": 15, + "villainWins": 5, + "numberOfFights": 20, + "heroWinRatio": 0.75 +} +``` ### Winner Stats Winner stats are accumulated by the number of wins of each hero or villain. diff --git a/event-statistics/images/event-statistics-ui.png b/event-statistics/images/event-statistics-ui.png index e80b975ac..ca96e5f73 100644 Binary files a/event-statistics/images/event-statistics-ui.png and b/event-statistics/images/event-statistics-ui.png differ diff --git a/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/domain/TeamScore.java b/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/domain/TeamScore.java new file mode 100644 index 000000000..ece9c7a41 --- /dev/null +++ b/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/domain/TeamScore.java @@ -0,0 +1,48 @@ +package io.quarkus.sample.superheroes.statistics.domain; + +import java.util.StringJoiner; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +/** + * Data class for a team score + *

+ * The {@link RegisterForReflection @RegisterForReflection} annotation instructs the native compilation to allow reflection access to the class. Without it, the serialization/deserialization would not work when running the native executable. + *

+ */ +@RegisterForReflection +public class TeamScore { + private final int heroWins; + private final int villainWins; + + public TeamScore(int heroWins, int villainWins) { + this.heroWins = heroWins; + this.villainWins = villainWins; + } + + public int getHeroWins() { + return heroWins; + } + + public int getVillainWins() { + return villainWins; + } + + public int getNumberOfFights() { + return getHeroWins() + getVillainWins(); + } + + public double getHeroWinRatio() { + return ((double) this.heroWins / getNumberOfFights()); + } + + @Override + public String toString() { + return new StringJoiner(", ", TeamScore.class.getSimpleName() + "[", "]") + .add("heroWins=" + heroWins) + .add("villainWins=" + villainWins) + .add("numberOfFights=" + getNumberOfFights()) + .add("heroWinRatio=" + getHeroWinRatio()) + .toString(); + } +} diff --git a/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/endpoint/TeamStatsChannelHolder.java b/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/endpoint/TeamStatsChannelHolder.java index 21b77185d..35f64ecda 100644 --- a/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/endpoint/TeamStatsChannelHolder.java +++ b/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/endpoint/TeamStatsChannelHolder.java @@ -5,6 +5,8 @@ import org.eclipse.microprofile.reactive.messaging.Channel; +import io.quarkus.sample.superheroes.statistics.domain.TeamScore; + import io.smallrye.mutiny.Multi; /** @@ -18,9 +20,9 @@ class TeamStatsChannelHolder { @Inject @Channel("team-stats") - Multi teamStats; + Multi teamStats; - Multi getTeamStats() { + Multi getTeamStats() { return this.teamStats; } } diff --git a/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/endpoint/TeamStatsWebSocket.java b/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/endpoint/TeamStatsWebSocket.java index a0a7eb74e..e3a462e5d 100644 --- a/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/endpoint/TeamStatsWebSocket.java +++ b/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/endpoint/TeamStatsWebSocket.java @@ -14,7 +14,9 @@ import io.quarkus.logging.Log; +import com.fasterxml.jackson.databind.ObjectMapper; import io.smallrye.mutiny.subscription.Cancellable; +import io.smallrye.mutiny.unchecked.Unchecked; /** * WebSocket endpoint for the {@code /stats/team} endpoint. Exposes the {@code team-stats} channel over the socket to anyone listening. @@ -29,10 +31,13 @@ public class TeamStatsWebSocket { private final List sessions = new CopyOnWriteArrayList<>(); private Cancellable cancellable; + @Inject + ObjectMapper mapper; + @Inject TeamStatsChannelHolder teamStatsChannelHolder; - @OnOpen + @OnOpen public void onOpen(Session session) { this.sessions.add(session); } @@ -45,8 +50,8 @@ public void onClose(Session session) { @PostConstruct public void subscribe() { this.cancellable = this.teamStatsChannelHolder.getTeamStats() - .map(ratio -> Double.toString(ratio)) - .subscribe().with(ratio -> this.sessions.forEach(session -> write(session, ratio))); + .map(Unchecked.function(this.mapper::writeValueAsString)) + .subscribe().with(serialized -> this.sessions.forEach(session -> write(session, serialized))); } @PreDestroy diff --git a/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/listener/SuperStats.java b/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/listener/SuperStats.java index 5e103d34a..2c07795e1 100644 --- a/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/listener/SuperStats.java +++ b/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/listener/SuperStats.java @@ -8,6 +8,7 @@ import io.quarkus.sample.superheroes.fight.schema.Fight; import io.quarkus.sample.superheroes.statistics.domain.Score; +import io.quarkus.sample.superheroes.statistics.domain.TeamScore; import io.smallrye.mutiny.Multi; @@ -28,9 +29,9 @@ public class SuperStats { */ @Incoming("fights") @Outgoing("team-stats") - public Multi computeTeamStats(Multi results) { + public Multi computeTeamStats(Multi results) { return results.map(this.stats::add) - .invoke(stats -> LOGGER.debugf("Fight received. Computed the team statistics: %,.010f", stats)); + .invoke(score -> LOGGER.debugf("Fight received. Computed the team statistics: %s", score)); } /** diff --git a/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/listener/TeamStats.java b/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/listener/TeamStats.java index c50eb3436..9be4df58d 100644 --- a/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/listener/TeamStats.java +++ b/event-statistics/src/main/java/io/quarkus/sample/superheroes/statistics/listener/TeamStats.java @@ -1,6 +1,7 @@ package io.quarkus.sample.superheroes.statistics.listener; import io.quarkus.sample.superheroes.fight.schema.Fight; +import io.quarkus.sample.superheroes.statistics.domain.TeamScore; /** * Object keeping track of the number of battles won by heroes and villains @@ -12,9 +13,9 @@ class TeamStats { /** * Adds a {@link Fight} * @param result The {@link Fight} received - * @return The running percentage of battles won by heroes + * @return A {@link TeamScore} containing running battle stats by team */ - double add(Fight result) { + TeamScore add(Fight result) { if (result.getWinnerTeam().equalsIgnoreCase("heroes")) { this.heroes = this.heroes + 1; } @@ -22,7 +23,7 @@ class TeamStats { this.villains = this.villains + 1; } - return ((double) this.heroes / (this.heroes + this.villains)); + return new TeamScore(this.heroes, this.villains); } int getVillainsCount() { diff --git a/event-statistics/src/main/resources/META-INF/resources/index.html b/event-statistics/src/main/resources/META-INF/resources/index.html index f14ca27ca..115505196 100644 --- a/event-statistics/src/main/resources/META-INF/resources/index.html +++ b/event-statistics/src/main/resources/META-INF/resources/index.html @@ -53,13 +53,16 @@

- Heroes + Heroes
- Villains + Villains
+

+ 0 battles +

@@ -84,7 +87,7 @@

var team = new WebSocket("ws://" + host + "/stats/team"); team.onmessage = function(event) { console.log(event.data); - updateRatio(event.data); + updateTeam(JSON.parse(event.data)); }; }); @@ -96,9 +99,12 @@

}); } - function updateRatio(ratio) { - var percent = ratio * 100; - $("#balance").attr("aria-valuenow", ratio * 100).attr("style", "width: " + percent + "%;"); + function updateTeam(teamScore) { + var percent = teamScore.heroWinRatio * 100; + $("#balance").attr("aria-valuenow", percent).attr("style", "width: " + percent + "%;"); + $("#heroProgressLabel").text("Heroes (" + teamScore.heroWins + ")"); + $("#villainProgressLabel").text("Villains (" + teamScore.villainWins + ")") + $("#numBattles").text(teamScore.numberOfFights + " battles"); } diff --git a/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/TeamStatsWebSocketIT.java b/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/TeamStatsWebSocketIT.java deleted file mode 100644 index 3e78de68c..000000000 --- a/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/TeamStatsWebSocketIT.java +++ /dev/null @@ -1,146 +0,0 @@ -package io.quarkus.sample.superheroes.statistics.endpoint; - -/** - * Integration tests for the {@link TeamStatsWebSocket} WebSocket. - *

- * These tests use the {@link io.quarkus.sample.superheroes.statistics.KafkaProducerResource} to create a {@link KafkaProducer}, injected via {@link io.quarkus.sample.superheroes.statistics.InjectKafkaProducer}. The test will publish messages to the Kafka topic while also creating a WebSocket client to listen to messages. Each received message is stored in a {@link java.util.concurrent.BlockingQueue} so that the test can assert the correct messages were produced by the WebSocket. - *

- */ -//@QuarkusIntegrationTest -//@QuarkusTestResource(KafkaProducerResource.class) -public class TeamStatsWebSocketIT { - private static final String HERO_NAME = "Chewbacca"; - private static final String HERO_TEAM_NAME = "heroes"; - private static final String VILLAIN_TEAM_NAME = "villains"; - private static final String VILLAIN_NAME = "Darth Vader"; - -// @TestHTTPResource("/stats/team") -// URI uri; -// -// @InjectKafkaProducer -// KafkaProducer fightsProducer; -// -// @Test -// public void teamStatsWebSocketTestScenario() throws DeploymentException, IOException, InterruptedException { -// // Set up the Queue to handle the messages -// var messages = new LinkedBlockingQueue(); -// -// // Set up the client to connect to the socket -// try (var session = ContainerProvider.getWebSocketContainer().connectToServer(new EndpointTestClient(messages), this.uri)) { -// // Make sure client connected -// assertThat(messages.poll(5, TimeUnit.MINUTES)) -// .isNotNull() -// .isEqualTo("CONNECT"); -// -// // Create 10 fights, split between heroes and villains winning -// var sampleFights = createSampleFights(); -// sampleFights.stream() -// .map(fight -> new ProducerRecord("fights", fight)) -// .forEach(this.fightsProducer::send); -// -// // Wait for our messages to appear in the queue -// await() -// .atMost(Duration.ofMinutes(5)) -// .until(() -> messages.size() == sampleFights.size()); -// -// System.out.println("Messages received by test: " + messages); -// -// // Perform assertions that all expected messages were received -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo(String.valueOf((double) 1/1)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo(String.valueOf((double) 1/2)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo(String.valueOf((double) 2/3)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo(String.valueOf((double) 2/4)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo(String.valueOf((double) 3/5)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo(String.valueOf((double) 3/6)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo(String.valueOf((double) 4/7)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo(String.valueOf((double) 4/8)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo(String.valueOf((double) 5/9)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo(String.valueOf((double) 5/10)); -// } -// } -// -// private static List createSampleFights() { -// return IntStream.range(0, 10) -// .mapToObj(i -> { -// var heroName = HERO_NAME; -// var villainName = VILLAIN_NAME; -// var fight = Fight.builder(); -// -// if (i % 2 == 0) { -// fight = fight.winnerTeam(HERO_TEAM_NAME) -// .loserTeam(VILLAIN_TEAM_NAME) -// .winnerName(heroName) -// .loserName(villainName); -// } -// else { -// fight = fight.winnerTeam(VILLAIN_TEAM_NAME) -// .loserTeam(HERO_TEAM_NAME) -// .winnerName(villainName) -// .loserName(heroName); -// } -// -// return fight.build(); -// }).collect(Collectors.toList()); -// } -// -// @ClientEndpoint -// private class EndpointTestClient { -// private final Logger logger = Logger.getLogger(EndpointTestClient.class); -// private final BlockingQueue messages; -// -// private EndpointTestClient(BlockingQueue messages) { -// this.messages = messages; -// } -// -// @OnOpen -// public void open() { -// this.logger.info("Opening socket"); -// this.messages.offer("CONNECT"); -// } -// -// @OnMessage -// public void message(String msg) { -// this.logger.infof("Got message: %s", msg); -// this.messages.offer(msg); -// } -// -// @OnClose -// public void close(CloseReason closeReason) { -// this.logger.infof("Closing socket: %s", closeReason); -// } -// -// @OnError -// public void error(Throwable error) { -// this.logger.errorf(error, "Socket encountered error"); -// } -// } -} diff --git a/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/TeamStatsWebSocketTests.java b/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/TeamStatsWebSocketTests.java deleted file mode 100644 index 080715049..000000000 --- a/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/TeamStatsWebSocketTests.java +++ /dev/null @@ -1,105 +0,0 @@ -package io.quarkus.sample.superheroes.statistics.endpoint; - -/** - * Tests for the {@link TeamStatsWebSocket} class. - *

- * These tests mock the {@link TeamStatsChannelHolder#getTeamStats()} method to return pre-defined input and then set up a sample WebSocket client to listen to messages sent by the server. Each message received is placed into a {@link java.util.concurrent.BlockingQueue} so that message content can be asserted once the expected number of messages have been received. - *

- */ -//@QuarkusTest -class TeamStatsWebSocketTests { -// @TestHTTPResource("/stats/team") -// URI uri; -// -// @InjectMock -// TeamStatsChannelHolder teamStatsChannelHolder; -// -// @Test -// public void teamStatsWebSocketTestScenario() throws DeploymentException, IOException, InterruptedException { -// // Set up the Queue to handle the messages -// var messages = new LinkedBlockingQueue(); -// -// // Set up a single consumer latch -// // It will wait for the client to connect and subscribe to the stream before emitting items -// var latch = new CountDownLatch(1); -// var delayedUni = Uni.createFrom().voidItem() -// .onItem().delayIt().until(x -> { -// try { -// latch.await(); -// return Uni.createFrom().nullItem(); -// } -// catch (InterruptedException ex) { -// return Uni.createFrom().failure(ex); -// } -// }); -// -// // Delay the emission of the Multi until the client subscribes -// var delayedItemsMulti = Multi.createFrom().items(TeamStatsWebSocketTests::createItems) -// .onItem().call(items -> Uni.createFrom().nullItem().onItem().delayIt().until(o -> delayedUni)); -// -// // Mock TeamStatsChannelHolder.getTeamStats() to return the delayed Multi -// when(this.teamStatsChannelHolder.getTeamStats()).thenReturn(delayedItemsMulti); -// -// // Set up the client to connect to the socket -// try (var session = ContainerProvider.getWebSocketContainer().connectToServer(new EndpointTestClient(messages), this.uri)) { -// // Make sure client connected -// assertThat(messages.poll(5, TimeUnit.MINUTES)) -// .isNotNull() -// .isEqualTo("CONNECT"); -// -// // Client has connected - trigger the Multi subscription -// latch.countDown(); -// -// var expectedItems = createItems() -// .map(String::valueOf) -// .collect(Collectors.toList()); -// -// // Wait for our messages to appear in the queue -// await() -// .atMost(Duration.ofMinutes(5)) -// .until(() -> messages.size() == expectedItems.size()); -// -// System.out.println("Messages received by test: " + messages); -// -// // Perform assertions that all expected messages were received -// assertThat(messages) -// .containsExactlyElementsOf(expectedItems); -// } -// } -// -// private static Stream createItems() { -// return DoubleStream.of(0.0, 0.25, 0.5, 0.75, 1.0).boxed(); -// } -// -// @ClientEndpoint -// private class EndpointTestClient { -// private final Logger logger = Logger.getLogger(EndpointTestClient.class); -// private final BlockingQueue messages; -// -// private EndpointTestClient(BlockingQueue messages) { -// this.messages = messages; -// } -// -// @OnOpen -// public void open() { -// this.logger.info("Opening socket"); -// this.messages.offer("CONNECT"); -// } -// -// @OnMessage -// public void message(String msg) { -// this.logger.infof("Got message: %s", msg); -// this.messages.offer(msg); -// } -// -// @OnClose -// public void close(CloseReason closeReason) { -// this.logger.infof("Closing socket: %s", closeReason); -// } -// -// @OnError -// public void error(Throwable error) { -// this.logger.errorf(error, "Socket encountered error"); -// } -// } -} diff --git a/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/TopWinnerWebSocketIT.java b/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/TopWinnerWebSocketIT.java deleted file mode 100644 index 7f9815124..000000000 --- a/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/TopWinnerWebSocketIT.java +++ /dev/null @@ -1,150 +0,0 @@ -package io.quarkus.sample.superheroes.statistics.endpoint; - -/** - * Integration tests for the {@link TopWinnerWebSocket} WebSocket. - *

- * These tests use the {@link io.quarkus.sample.superheroes.statistics.KafkaProducerResource} to create a {@link KafkaProducer}, injected via {@link io.quarkus.sample.superheroes.statistics.InjectKafkaProducer}. The test will publish messages to the Kafka topic while also creating a WebSocket client to listen to messages. Each received message is stored in a {@link java.util.concurrent.BlockingQueue} so that the test can assert the correct messages were produced by the WebSocket. - *

- */ -//@QuarkusIntegrationTest -//@QuarkusTestResource(KafkaProducerResource.class) -public class TopWinnerWebSocketIT { -// private static final String HERO_NAME = "Chewbacca"; -// private static final String HERO_TEAM_NAME = "heroes"; -// private static final String VILLAIN_TEAM_NAME = "villains"; -// private static final String VILLAIN_NAME = "Darth Vader"; -// -// @TestHTTPResource("/stats/winners") -// URI uri; -// -// @InjectKafkaProducer -// KafkaProducer fightsProducer; -// -// @Test -// public void topWinnerWebSocketTestScenario() throws DeploymentException, IOException, InterruptedException { -// // Set up the Queue to handle the messages -// var messages = new LinkedBlockingQueue(); -// -// // Set up the client to connect to the socket -// try (var session = ContainerProvider.getWebSocketContainer().connectToServer(new EndpointTestClient(messages), this.uri)) { -// // Make sure client connected -// assertThat(messages.poll(5, TimeUnit.MINUTES)) -// .isNotNull() -// .isEqualTo("CONNECT"); -// -// // Create 10 fights, split between heroes and villains winning -// var sampleFights = createSampleFights(); -// sampleFights.stream() -// .map(fight -> new ProducerRecord("fights", fight)) -// .forEach(this.fightsProducer::send); -// -// // Wait for our messages to appear in the queue -// await() -// .atMost(Duration.ofMinutes(5)) -// .until(() -> messages.size() == sampleFights.size()); -// -// System.out.println("Messages received by test: " + messages); -// -// // Perform assertions that all expected messages were received -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo("[%s]", createJsonString(HERO_NAME, 1)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 1), createJsonString(VILLAIN_NAME, 1)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 2), createJsonString(VILLAIN_NAME, 1)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 2), createJsonString(VILLAIN_NAME, 2)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 3), createJsonString(VILLAIN_NAME, 2)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 3), createJsonString(VILLAIN_NAME, 3)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 4), createJsonString(VILLAIN_NAME, 3)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 4), createJsonString(VILLAIN_NAME, 4)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 5), createJsonString(VILLAIN_NAME, 4)); -// -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 5), createJsonString(VILLAIN_NAME, 5)); -// } -// } -// -// private static String createJsonString(String name, int score) { -// return String.format("{\"name\":\"%s\",\"score\":%d}", name, score); -// } -// -// private static List createSampleFights() { -// return IntStream.range(0, 10) -// .mapToObj(i -> { -// var heroName = HERO_NAME; -// var villainName = VILLAIN_NAME; -// var fight = Fight.builder(); -// -// if (i % 2 == 0) { -// fight = fight.winnerTeam(HERO_TEAM_NAME) -// .loserTeam(VILLAIN_TEAM_NAME) -// .winnerName(heroName) -// .loserName(villainName); -// } -// else { -// fight = fight.winnerTeam(VILLAIN_TEAM_NAME) -// .loserTeam(HERO_TEAM_NAME) -// .winnerName(villainName) -// .loserName(heroName); -// } -// -// return fight.build(); -// }).collect(Collectors.toList()); -// } -// -// @ClientEndpoint -// private class EndpointTestClient { -// private final Logger logger = Logger.getLogger(EndpointTestClient.class); -// private final BlockingQueue messages; -// -// private EndpointTestClient(BlockingQueue messages) { -// this.messages = messages; -// } -// -// @OnOpen -// public void open() { -// this.logger.info("Opening socket"); -// this.messages.offer("CONNECT"); -// } -// -// @OnMessage -// public void message(String msg) { -// this.logger.infof("Got message: %s", msg); -// this.messages.offer(msg); -// } -// -// @OnClose -// public void close(CloseReason closeReason) { -// this.logger.infof("Closing socket: %s", closeReason); -// } -// -// @OnError -// public void error(Throwable error) { -// this.logger.errorf(error, "Socket encountered error"); -// } -// } -} diff --git a/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/TopWinnerWebSocketTests.java b/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/TopWinnerWebSocketTests.java deleted file mode 100644 index c63561ca0..000000000 --- a/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/TopWinnerWebSocketTests.java +++ /dev/null @@ -1,116 +0,0 @@ -package io.quarkus.sample.superheroes.statistics.endpoint; - -/** - * Tests for the {@link TopWinnerWebSocket} class. - *

- * These tests mock the {@link TopWinnerStatsChannelHolder#getWinners()} method to return pre-defined input and then set up a sample WebSocket client to listen to messages sent by the server. Each message received is placed into a {@link java.util.concurrent.BlockingQueue} so that message content can be asserted once the expected number of messages have been received. - *

- */ -//@QuarkusTest -class TopWinnerWebSocketTests { -// @TestHTTPResource("/stats/winners") -// URI uri; -// -// @InjectMock -// TopWinnerStatsChannelHolder topWinnerStatsChannelHolder; -// -// @Inject -// ObjectMapper objectMapper; -// -// @Test -// public void topWinnerWebSocketTestScenario() throws DeploymentException, IOException, InterruptedException { -// // Set up the Queue to handle the messages -// var messages = new LinkedBlockingQueue(); -// -// // Set up a single consumer latch -// // It will wait for the client to connect and subscribe to the stream before emitting items -// var latch = new CountDownLatch(1); -// var delayedUni = Uni.createFrom().voidItem().onItem().delayIt() -// .until(x -> { -// try { -// latch.await(); -// return Uni.createFrom().nullItem(); -// } -// catch (InterruptedException ex) { -// return Uni.createFrom().failure(ex); -// } -// }); -// -// // Delay the emission of the Multi until the client subscribes -// var delayedItemsMulti = Multi.createFrom().items(TopWinnerWebSocketTests::createItems) -// .onItem().call(scores -> Uni.createFrom().nullItem().onItem().delayIt().until(o -> delayedUni)); -// -// // Mock TopWinnerStatsChannelHolder.getWinners() to return the delayed Multi -// when(this.topWinnerStatsChannelHolder.getWinners()).thenReturn(delayedItemsMulti); -// -// // Set up the client to connect to the socket -// try (var session = ContainerProvider.getWebSocketContainer().connectToServer(new EndpointTestClient(messages), this.uri)) { -// // Make sure client connected -// assertThat(messages.poll(5, TimeUnit.MINUTES)) -// .isNotNull() -// .isEqualTo("CONNECT"); -// -// // Client has connected - trigger the Multi subscription -// latch.countDown(); -// -// var expectedItems = createItems().collect(Collectors.toList()); -// -// // Wait for our messages to appear in the queue -// await() -// .atMost(Duration.ofMinutes(5)) -// .until(() -> messages.size() == expectedItems.size()); -// -// System.out.println("Messages received by test: " + messages); -// -// // Perform assertions that all expected messages were received -// expectedItems.stream() -// .map(Unchecked.function(this.objectMapper::writeValueAsString)) -// .forEach(expectedMsg -> -// assertThat(messages.poll()) -// .isNotNull() -// .isEqualTo(expectedMsg) -// ); -// } -// } -// -// private static Stream> createItems() { -// return Stream.of( -// List.of(new Score("Chewbacca", 5)), -// List.of(new Score("Darth Vader", 2)), -// List.of(new Score("Han Solo", 1)), -// List.of(new Score("Palpatine", 3)) -// ); -// } -// -// @ClientEndpoint -// private class EndpointTestClient { -// private final Logger logger = Logger.getLogger(EndpointTestClient.class); -// private final BlockingQueue messages; -// -// private EndpointTestClient(BlockingQueue messages) { -// this.messages = messages; -// } -// -// @OnOpen -// public void open() { -// this.logger.info("Opening socket"); -// this.messages.offer("CONNECT"); -// } -// -// @OnMessage -// public void message(String msg) { -// this.logger.infof("Got message: %s", msg); -// this.messages.offer(msg); -// } -// -// @OnClose -// public void close(CloseReason closeReason) { -// this.logger.infof("Closing socket: %s", closeReason); -// } -// -// @OnError -// public void error(Throwable error) { -// this.logger.errorf(error, "Socket encountered error"); -// } -// } -} diff --git a/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/WebSocketsIT.java b/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/WebSocketsIT.java index 1c9b359f9..303879c33 100644 --- a/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/WebSocketsIT.java +++ b/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/WebSocketsIT.java @@ -30,10 +30,14 @@ import io.quarkus.sample.superheroes.fight.schema.Fight; import io.quarkus.sample.superheroes.statistics.InjectKafkaProducer; import io.quarkus.sample.superheroes.statistics.KafkaProducerResource; +import io.quarkus.sample.superheroes.statistics.domain.TeamScore; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.test.junit.QuarkusIntegrationTest; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.smallrye.mutiny.unchecked.Unchecked; + /** * Integration tests for the {@link TeamStatsWebSocket} and {@link TopWinnerWebSocket} WebSockets. *

@@ -47,6 +51,7 @@ public class WebSocketsIT { private static final String HERO_TEAM_NAME = "heroes"; private static final String VILLAIN_TEAM_NAME = "villains"; private static final String VILLAIN_NAME = "Darth Vader"; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @TestHTTPResource("/stats/team") URI teamStatsUri; @@ -96,43 +101,43 @@ private static void validateTeamStats(BlockingQueue teamStatsMessages) { // Perform assertions that all expected teamStatsMessages were received assertThat(teamStatsMessages.poll()) .isNotNull() - .isEqualTo(String.valueOf((double) 1/1)); + .isEqualTo(createTeamScoreJsonString(new TeamScore(1, 0))); assertThat(teamStatsMessages.poll()) .isNotNull() - .isEqualTo(String.valueOf((double) 1/2)); + .isEqualTo(createTeamScoreJsonString(new TeamScore(1, 1))); assertThat(teamStatsMessages.poll()) .isNotNull() - .isEqualTo(String.valueOf((double) 2/3)); + .isEqualTo(createTeamScoreJsonString(new TeamScore(2, 1))); assertThat(teamStatsMessages.poll()) .isNotNull() - .isEqualTo(String.valueOf((double) 2/4)); + .isEqualTo(createTeamScoreJsonString(new TeamScore(2, 2))); assertThat(teamStatsMessages.poll()) .isNotNull() - .isEqualTo(String.valueOf((double) 3/5)); + .isEqualTo(createTeamScoreJsonString(new TeamScore(3, 2))); assertThat(teamStatsMessages.poll()) .isNotNull() - .isEqualTo(String.valueOf((double) 3/6)); + .isEqualTo(createTeamScoreJsonString(new TeamScore(3, 3))); assertThat(teamStatsMessages.poll()) .isNotNull() - .isEqualTo(String.valueOf((double) 4/7)); + .isEqualTo(createTeamScoreJsonString(new TeamScore(4, 3))); assertThat(teamStatsMessages.poll()) .isNotNull() - .isEqualTo(String.valueOf((double) 4/8)); + .isEqualTo(createTeamScoreJsonString(new TeamScore(4, 4))); assertThat(teamStatsMessages.poll()) .isNotNull() - .isEqualTo(String.valueOf((double) 5/9)); + .isEqualTo(createTeamScoreJsonString(new TeamScore(5, 4))); assertThat(teamStatsMessages.poll()) .isNotNull() - .isEqualTo(String.valueOf((double) 5/10)); + .isEqualTo(createTeamScoreJsonString(new TeamScore(5, 5))); } private static void validateTopWinnerStats(BlockingQueue topWinnerMessages) { @@ -141,43 +146,43 @@ private static void validateTopWinnerStats(BlockingQueue topWinnerMessag // Perform assertions that all expected topWinnerMessages were received assertThat(topWinnerMessages.poll()) .isNotNull() - .isEqualTo("[%s]", createJsonString(HERO_NAME, 1)); + .isEqualTo("[%s]", createScoreJsonString(HERO_NAME, 1)); assertThat(topWinnerMessages.poll()) .isNotNull() - .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 1), createJsonString(VILLAIN_NAME, 1)); + .isEqualTo("[%s,%s]", createScoreJsonString(HERO_NAME, 1), createScoreJsonString(VILLAIN_NAME, 1)); assertThat(topWinnerMessages.poll()) .isNotNull() - .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 2), createJsonString(VILLAIN_NAME, 1)); + .isEqualTo("[%s,%s]", createScoreJsonString(HERO_NAME, 2), createScoreJsonString(VILLAIN_NAME, 1)); assertThat(topWinnerMessages.poll()) .isNotNull() - .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 2), createJsonString(VILLAIN_NAME, 2)); + .isEqualTo("[%s,%s]", createScoreJsonString(HERO_NAME, 2), createScoreJsonString(VILLAIN_NAME, 2)); assertThat(topWinnerMessages.poll()) .isNotNull() - .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 3), createJsonString(VILLAIN_NAME, 2)); + .isEqualTo("[%s,%s]", createScoreJsonString(HERO_NAME, 3), createScoreJsonString(VILLAIN_NAME, 2)); assertThat(topWinnerMessages.poll()) .isNotNull() - .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 3), createJsonString(VILLAIN_NAME, 3)); + .isEqualTo("[%s,%s]", createScoreJsonString(HERO_NAME, 3), createScoreJsonString(VILLAIN_NAME, 3)); assertThat(topWinnerMessages.poll()) .isNotNull() - .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 4), createJsonString(VILLAIN_NAME, 3)); + .isEqualTo("[%s,%s]", createScoreJsonString(HERO_NAME, 4), createScoreJsonString(VILLAIN_NAME, 3)); assertThat(topWinnerMessages.poll()) .isNotNull() - .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 4), createJsonString(VILLAIN_NAME, 4)); + .isEqualTo("[%s,%s]", createScoreJsonString(HERO_NAME, 4), createScoreJsonString(VILLAIN_NAME, 4)); assertThat(topWinnerMessages.poll()) .isNotNull() - .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 5), createJsonString(VILLAIN_NAME, 4)); + .isEqualTo("[%s,%s]", createScoreJsonString(HERO_NAME, 5), createScoreJsonString(VILLAIN_NAME, 4)); assertThat(topWinnerMessages.poll()) .isNotNull() - .isEqualTo("[%s,%s]", createJsonString(HERO_NAME, 5), createJsonString(VILLAIN_NAME, 5)); + .isEqualTo("[%s,%s]", createScoreJsonString(HERO_NAME, 5), createScoreJsonString(VILLAIN_NAME, 5)); } private static void waitForClientsToStart(BlockingQueue teamStatsMessages, BlockingQueue topWinnerMessages) { @@ -187,7 +192,11 @@ private static void waitForClientsToStart(BlockingQueue teamStatsMessage .until(() -> "CONNECT".equals(teamStatsMessages.poll()) && "CONNECT".equals(topWinnerMessages.poll())); } - private static String createJsonString(String name, int score) { + private static String createTeamScoreJsonString(TeamScore teamScore) { + return Unchecked.supplier(() -> OBJECT_MAPPER.writeValueAsString(teamScore)).get(); + } + + private static String createScoreJsonString(String name, int score) { return String.format("{\"name\":\"%s\",\"score\":%d}", name, score); } diff --git a/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/WebSocketsTests.java b/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/WebSocketsTests.java index 25d94c285..0a7ba7ccb 100644 --- a/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/WebSocketsTests.java +++ b/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/endpoint/WebSocketsTests.java @@ -13,7 +13,6 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.DoubleStream; import java.util.stream.Stream; import javax.inject.Inject; @@ -30,6 +29,7 @@ import org.junit.jupiter.api.Test; import io.quarkus.sample.superheroes.statistics.domain.Score; +import io.quarkus.sample.superheroes.statistics.domain.TeamScore; import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.mockito.InjectMock; @@ -68,7 +68,7 @@ public void testScenarios() throws DeploymentException, IOException { var teamStatsMessages = new LinkedBlockingQueue(); var topWinnerMessages = new LinkedBlockingQueue(); - // Set up a single consumer latch for each websocked + // Set up a single consumer latch for each websocket // It will wait for the client to connect and subscribe to the stream before emitting items var teamStatsLatch = createLatch(); var topWinnersLatch = createLatch(); @@ -92,10 +92,7 @@ public void testScenarios() throws DeploymentException, IOException { // Wait for each client to emit a CONNECT message waitForClientsToStart(teamStatsMessages, teamStatsLatch, topWinnerMessages, topWinnersLatch); - var expectedTeamStats = createTeamStatsItems() - .map(String::valueOf) - .collect(Collectors.toList()); - + var expectedTeamStats = createTeamStatsItems().collect(Collectors.toList()); var expectedTopWinners = createTopWinnerItems().collect(Collectors.toList()); // Wait for our messages to appear in the queue @@ -109,12 +106,17 @@ public void testScenarios() throws DeploymentException, IOException { } } - private static void validateTeamStats(BlockingQueue teamStatsMessages, List expectedItems) { + private void validateTeamStats(BlockingQueue teamStatsMessages, List expectedItems) { System.out.println("Team Stats Messages received by test: " + teamStatsMessages); - // Perform assertions that all expected messages were received - assertThat(teamStatsMessages) - .containsExactlyElementsOf(expectedItems); + // Perform assertions that all expected messages were received + expectedItems.stream() + .map(Unchecked.function(this.objectMapper::writeValueAsString)) + .forEach(expectedMsg -> + assertThat(teamStatsMessages.poll()) + .isNotNull() + .isEqualTo(expectedMsg) + ); } private void validateTopWinnerStats(BlockingQueue topWinnerMessages, List> expectedItems) { @@ -162,8 +164,14 @@ private static CountDownLatch createLatch() { return new CountDownLatch(1); } - private static Stream createTeamStatsItems() { - return DoubleStream.of(0.0, 0.25, 0.5, 0.75, 1.0).boxed(); + private static Stream createTeamStatsItems() { + return Stream.of( + new TeamScore(0, 4), + new TeamScore(1, 3), + new TeamScore(2, 2), + new TeamScore(3, 1), + new TeamScore(4, 0) + ); } private static Stream> createTopWinnerItems() { diff --git a/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/listener/SuperStatsTests.java b/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/listener/SuperStatsTests.java index a5d89df6a..2ee6143a9 100644 --- a/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/listener/SuperStatsTests.java +++ b/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/listener/SuperStatsTests.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.*; +import java.time.Duration; import java.time.Instant; import java.util.List; import java.util.stream.IntStream; @@ -11,6 +12,7 @@ import io.quarkus.sample.superheroes.fight.schema.Fight; import io.quarkus.sample.superheroes.statistics.domain.Score; +import io.quarkus.sample.superheroes.statistics.domain.TeamScore; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.helpers.test.AssertSubscriber; @@ -34,22 +36,34 @@ public void computeTeamStats() { var fights = createSampleFights(true); // Call computeTeamStats and assert the items - this.superStats.computeTeamStats(Multi.createFrom().items(fights)) + var scores = this.superStats.computeTeamStats(Multi.createFrom().items(fights)) .subscribe().withSubscriber(AssertSubscriber.create(10)) .assertSubscribed() - .assertItems( - (double) 1/1, - (double) 1/2, - (double) 2/3, - (double) 2/4, - (double) 3/5, - (double) 3/6, - (double) 4/7, - (double) 4/8, - (double) 5/9, - (double) 5/10 - ) - .assertCompleted(); + .awaitItems(10, Duration.ofSeconds(20)) + .assertCompleted() + .getItems(); + + assertThat(scores) + .isNotNull() + .hasSize(10) + .extracting( + TeamScore::getHeroWins, + TeamScore::getVillainWins, + TeamScore::getNumberOfFights, + TeamScore::getHeroWinRatio + ) + .containsExactly( + tuple(1, 0, 1, (double) 1/1), + tuple(1, 1, 2, (double) 1/2), + tuple(2, 1, 3, (double) 2/3), + tuple(2, 2, 4, (double) 2/4), + tuple(3, 2, 5, (double) 3/5), + tuple(3, 3, 6, (double) 3/6), + tuple(4, 3, 7, (double) 4/7), + tuple(4, 4, 8, (double) 4/8), + tuple(5, 4, 9, (double) 5/9), + tuple(5, 5, 10, (double) 5/10) + ); // Get the computed stats and assert that 5 heroes and 5 villains won var stats = this.superStats.getTeamStats(); diff --git a/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/listener/TeamStatsTests.java b/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/listener/TeamStatsTests.java index 6d9b6ed1b..32b2d5c7c 100644 --- a/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/listener/TeamStatsTests.java +++ b/event-statistics/src/test/java/io/quarkus/sample/superheroes/statistics/listener/TeamStatsTests.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test; import io.quarkus.sample.superheroes.fight.schema.Fight; +import io.quarkus.sample.superheroes.statistics.domain.TeamScore; /** * Tests for the {@link io.quarkus.sample.superheroes.statistics.listener.TeamStats} class. Not a {@link io.quarkus.test.junit.QuarkusTest @QuarkusTest} because the test can simply call the methods with the appropriate input. @@ -43,34 +44,104 @@ class TeamStatsTests { @Test public void teamStatsScenario() { assertThat(this.teamStats.add(HERO_WINNER)) - .isEqualTo((double) 1/1); + .isNotNull() + .extracting( + TeamScore::getHeroWins, + TeamScore::getVillainWins, + TeamScore::getNumberOfFights, + TeamScore::getHeroWinRatio + ) + .containsExactly(1, 0, 1, (double) 1/1); assertThat(this.teamStats.add(VILLAIN_WINNER)) - .isEqualTo((double) 1/2); + .isNotNull() + .extracting( + TeamScore::getHeroWins, + TeamScore::getVillainWins, + TeamScore::getNumberOfFights, + TeamScore::getHeroWinRatio + ) + .containsExactly(1, 1, 2, (double) 1/2); assertThat(this.teamStats.add(HERO_WINNER)) - .isEqualTo((double) 2/3); + .isNotNull() + .extracting( + TeamScore::getHeroWins, + TeamScore::getVillainWins, + TeamScore::getNumberOfFights, + TeamScore::getHeroWinRatio + ) + .containsExactly(2, 1, 3, (double) 2/3); assertThat(this.teamStats.add(VILLAIN_WINNER)) - .isEqualTo((double) 2/4); + .isNotNull() + .extracting( + TeamScore::getHeroWins, + TeamScore::getVillainWins, + TeamScore::getNumberOfFights, + TeamScore::getHeroWinRatio + ) + .containsExactly(2, 2, 4, (double) 2/4); assertThat(this.teamStats.add(HERO_WINNER)) - .isEqualTo((double) 3/5); + .isNotNull() + .extracting( + TeamScore::getHeroWins, + TeamScore::getVillainWins, + TeamScore::getNumberOfFights, + TeamScore::getHeroWinRatio + ) + .containsExactly(3, 2, 5, (double) 3/5); assertThat(this.teamStats.add(VILLAIN_WINNER)) - .isEqualTo((double) 3/6); + .isNotNull() + .extracting( + TeamScore::getHeroWins, + TeamScore::getVillainWins, + TeamScore::getNumberOfFights, + TeamScore::getHeroWinRatio + ) + .containsExactly(3, 3, 6, (double) 3/6); assertThat(this.teamStats.add(HERO_WINNER)) - .isEqualTo((double) 4/7); + .isNotNull() + .extracting( + TeamScore::getHeroWins, + TeamScore::getVillainWins, + TeamScore::getNumberOfFights, + TeamScore::getHeroWinRatio + ) + .containsExactly(4, 3, 7, (double) 4/7); assertThat(this.teamStats.add(VILLAIN_WINNER)) - .isEqualTo((double) 4/8); + .isNotNull() + .extracting( + TeamScore::getHeroWins, + TeamScore::getVillainWins, + TeamScore::getNumberOfFights, + TeamScore::getHeroWinRatio + ) + .containsExactly(4, 4, 8, (double) 4/8); assertThat(this.teamStats.add(HERO_WINNER)) - .isEqualTo((double) 5/9); + .isNotNull() + .extracting( + TeamScore::getHeroWins, + TeamScore::getVillainWins, + TeamScore::getNumberOfFights, + TeamScore::getHeroWinRatio + ) + .containsExactly(5, 4, 9, (double) 5/9); assertThat(this.teamStats.add(VILLAIN_WINNER)) - .isEqualTo((double) 5/10); + .isNotNull() + .extracting( + TeamScore::getHeroWins, + TeamScore::getVillainWins, + TeamScore::getNumberOfFights, + TeamScore::getHeroWinRatio + ) + .containsExactly(5, 5, 10, (double) 5/10); assertThat(this.teamStats.getHeroesCount()) .isEqualTo(5);