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

Additional testing for Nima's websocket implementation #5595

Merged
merged 2 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2022 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.nima.tests.integration.websocket.webserver;

import java.util.Random;
import java.util.StringTokenizer;

/**
* A websocket action of sending or receiving a text or binary message.
* A websocket conversation comprises a list of these actions.
*/
class WsAction {

enum Operation {
SND, RCV
}

enum OperationType {
TEXT, BINARY
}

Operation op;
OperationType opType;
String message;

WsAction() {
}

WsAction(Operation op, OperationType opType, String message) {
this.op = op;
this.opType = opType;
this.message = message;
}

WsAction dual() {
return new WsAction(op == Operation.SND ? Operation.RCV : Operation.SND, opType, message);
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof WsAction wsAction)) {
return false;
}
return op == wsAction.op && opType == wsAction.opType && message.equals(wsAction.message);
}

@Override
public String toString() {
return op + " " + opType + " '" + message + "'";
}

static WsAction fromString(String s) {
WsAction action = new WsAction();
StringTokenizer st = new StringTokenizer(s, " '", true);
action.op = Operation.valueOf(st.nextToken());
assert st.nextToken().equals(" ");
action.opType = OperationType.valueOf(st.nextToken());
assert st.nextToken().equals(" ");
assert st.nextToken().equals("'");
StringBuilder sb = new StringBuilder();
while (st.hasMoreTokens()) {
String t = st.nextToken();
if (t.equals("'")) {
break;
}
sb.append(t);
}
action.message = sb.toString();
return action;
}

static WsAction createRandom() {
Random random = new Random();
WsAction action = new WsAction();
action.op = random.nextInt(2) == 0 ? Operation.RCV : Operation.SND;
action.opType = random.nextInt(2) == 0 ? OperationType.BINARY : OperationType.TEXT;
action.message = randomString(random.nextInt(10, 20), random);
return action;
}

private static String randomString(int length, Random random) {
int leftLimit = 97; // letter 'a'
int rightLimit = 122; // letter 'z'
return random.ints(leftLimit, rightLimit + 1)
.limit(length)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2022 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.nima.tests.integration.websocket.webserver;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.StringTokenizer;

/**
* A sequence of {@link WsAction}s collectively referred to as a conversation.
*/
class WsConversation {

private final Collection<WsAction> actions = new ArrayList<>();

void addAction(WsAction action) {
actions.add(action);
}

Iterator<WsAction> actions() {
return actions.iterator();
}

WsConversation dual() {
return fromDual(this);
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
Iterator<WsAction> it = actions();
while (it.hasNext()) {
sb.append(it.next().toString()).append("\n");
}
return sb.toString();
}

static WsConversation createRandom(int size) {
WsConversation conversation = new WsConversation();
for (int i = 0; i < size; i++) {
conversation.addAction(WsAction.createRandom());
}
return conversation;
}

static WsConversation fromDual(WsConversation other) {
WsConversation conversation = new WsConversation();
Iterator<WsAction> it = other.actions();
while (it.hasNext()) {
conversation.addAction(it.next().dual());
}
return conversation;
}

static WsConversation fromString(String s) {
WsConversation conversation = new WsConversation();
StringTokenizer st = new StringTokenizer(s, "\n");
while (st.hasMoreTokens()) {
conversation.addAction(WsAction.fromString(st.nextToken()));
}
return conversation;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright (c) 2022 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package io.helidon.nima.tests.integration.websocket.webserver;

import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;;

import static io.helidon.nima.tests.integration.websocket.webserver.WsAction.Operation.RCV;
import static io.helidon.nima.tests.integration.websocket.webserver.WsAction.OperationType.BINARY;
import static io.helidon.nima.tests.integration.websocket.webserver.WsAction.OperationType.TEXT;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
* A websocket client that is driven by a conversation instance.
*/
class WsConversationClient implements Runnable, AutoCloseable {
private static final Long WAIT_SECONDS = 10L;
private static final Logger LOGGER = Logger.getLogger(WsConversationClient.class.getName());

private final WebSocket socket;
private final WsConversation conversation;
private final WsConversationListener listener;

WsConversationClient(WebSocket socket, WsConversationListener listener, WsConversation conversation) {
this.socket = socket;
this.conversation = conversation;
this.listener = listener;
}

@Override
public void run() {
Iterator<WsAction> it = conversation.actions();
while (it.hasNext()) {
WsAction action = it.next();
switch (action.op) {
case SND -> sendMessage(action);
case RCV -> waitMessage(action);
}
}
}

@Override
public void close() {
socket.sendClose(WebSocket.NORMAL_CLOSURE, "bye");
}

private void sendMessage(WsAction action) {
switch (action.opType) {
case TEXT -> socket.sendText(action.message, true);
case BINARY -> socket.sendBinary(ByteBuffer.wrap(action.message.getBytes(UTF_8)), true);
}
LOGGER.log(Level.FINE, () -> "Client: " + action);
}

private void waitMessage(WsAction action) {
try {
LOGGER.log(Level.FINE, () -> "Client: " + action);
WsAction r = listener.received.poll(WAIT_SECONDS, TimeUnit.SECONDS);
assert r != null;
if (!r.equals(action)) {
socket.abort();
}
} catch (Exception e) {
socket.abort();
}
}

static class WsConversationListener implements java.net.http.WebSocket.Listener {
BlockingQueue<WsAction> received;

@Override
public void onOpen(java.net.http.WebSocket webSocket) {
webSocket.request(Integer.MAX_VALUE);
received = new LinkedBlockingQueue<>();
}

@Override
public CompletionStage<?> onBinary(WebSocket webSocket, ByteBuffer data, boolean last) {
byte[] bytes = new byte[data.remaining()];
data.get(bytes);
WsAction action = new WsAction(RCV, BINARY, new String(bytes, UTF_8));
received.add(action);
return CompletableFuture.completedFuture(null);
}

@Override
public CompletionStage<?> onText(java.net.http.WebSocket webSocket, CharSequence data, boolean last) {
WsAction action = new WsAction(RCV, TEXT, data.toString());
received.add(action);
return CompletableFuture.completedFuture(null);
}

@Override
public CompletionStage<?> onClose(java.net.http.WebSocket webSocket, int statusCode, String reason) {
received = null;
return null;
}
}
}