/*
* Copyright (C) 2016 Square, Inc.
*
* 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 okhttp3.internal.ws;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okhttp3.internal.Util;
import okhttp3.internal.platform.Platform;
import okio.ByteString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public final class WebSocketRecorder extends WebSocketListener {
private final String name;
private final BlockingQueue<Object> events = new LinkedBlockingQueue<>();
private WebSocketListener delegate;
public WebSocketRecorder(String name) {
this.name = name;
}
/** Sets a delegate for handling the next callback to this listener. Cleared after invoked. */
public void setNextEventDelegate(WebSocketListener delegate) {
this.delegate = delegate;
}
@Override public void onOpen(WebSocket webSocket, Response response) {
Platform.get().log(Platform.INFO, "[WS " + name + "] onOpen", null);
WebSocketListener delegate = this.delegate;
if (delegate != null) {
this.delegate = null;
delegate.onOpen(webSocket, response);
} else {
events.add(new Open(webSocket, response));
}
}
@Override public void onMessage(WebSocket webSocket, ByteString bytes) {
Platform.get().log(Platform.INFO, "[WS " + name + "] onMessage", null);
WebSocketListener delegate = this.delegate;
if (delegate != null) {
this.delegate = null;
delegate.onMessage(webSocket, bytes);
} else {
Message event = new Message(bytes);
events.add(event);
}
}
@Override public void onMessage(WebSocket webSocket, String text) {
Platform.get().log(Platform.INFO, "[WS " + name + "] onMessage", null);
WebSocketListener delegate = this.delegate;
if (delegate != null) {
this.delegate = null;
delegate.onMessage(webSocket, text);
} else {
Message event = new Message(text);
events.add(event);
}
}
@Override public void onClosing(WebSocket webSocket, int code, String reason) {
Platform.get().log(Platform.INFO, "[WS " + name + "] onClose " + code, null);
WebSocketListener delegate = this.delegate;
if (delegate != null) {
this.delegate = null;
delegate.onClosing(webSocket, code, reason);
} else {
events.add(new Closing(code, reason));
}
}
@Override public void onClosed(WebSocket webSocket, int code, String reason) {
Platform.get().log(Platform.INFO, "[WS " + name + "] onClose " + code, null);
WebSocketListener delegate = this.delegate;
if (delegate != null) {
this.delegate = null;
delegate.onClosed(webSocket, code, reason);
} else {
events.add(new Closed(code, reason));
}
}
@Override public void onFailure(WebSocket webSocket, Throwable t, Response response) {
Platform.get().log(Platform.INFO, "[WS " + name + "] onFailure", t);
WebSocketListener delegate = this.delegate;
if (delegate != null) {
this.delegate = null;
delegate.onFailure(webSocket, t, response);
} else {
events.add(new Failure(t, response));
}
}
private Object nextEvent() {
try {
Object event = events.poll(10, TimeUnit.SECONDS);
if (event == null) {
throw new AssertionError("Timed out waiting for event.");
}
return event;
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
public void assertTextMessage(String payload) {
Object actual = nextEvent();
assertEquals(new Message(payload), actual);
}
public void assertBinaryMessage(ByteString payload) {
Object actual = nextEvent();
assertEquals(new Message(payload), actual);
}
public void assertPing(ByteString payload) {
Object actual = nextEvent();
assertEquals(new Ping(payload), actual);
}
public void assertPong(ByteString payload) {
Object actual = nextEvent();
assertEquals(new Pong(payload), actual);
}
public void assertClosing(int code, String reason) {
Object actual = nextEvent();
assertEquals(new Closing(code, reason), actual);
}
public void assertClosed(int code, String reason) {
Object actual = nextEvent();
assertEquals(new Closed(code, reason), actual);
}
public void assertExhausted() {
assertTrue("Remaining events: " + events, events.isEmpty());
}
public WebSocket assertOpen() {
Object event = nextEvent();
if (!(event instanceof Open)) {
throw new AssertionError("Expected Open but was " + event);
}
return ((Open) event).webSocket;
}
public void assertFailure(Throwable t) {
Object event = nextEvent();
if (!(event instanceof Failure)) {
throw new AssertionError("Expected Failure but was " + event);
}
Failure failure = (Failure) event;
assertNull(failure.response);
assertSame(t, failure.t);
}
public void assertFailure(Class<? extends IOException> cls, String... messages) {
Object event = nextEvent();
if (!(event instanceof Failure)) {
throw new AssertionError("Expected Failure but was " + event);
}
Failure failure = (Failure) event;
assertNull(failure.response);
assertEquals(cls, failure.t.getClass());
if (messages.length > 0) {
assertTrue(failure.t.getMessage(), Arrays.asList(messages).contains(failure.t.getMessage()));
}
}
public void assertFailure() {
Object event = nextEvent();
if (!(event instanceof Failure)) {
throw new AssertionError("Expected Failure but was " + event);
}
}
public void assertFailure(int code, String body, Class<? extends IOException> cls, String message)
throws IOException {
Object event = nextEvent();
if (!(event instanceof Failure)) {
throw new AssertionError("Expected Failure but was " + event);
}
Failure failure = (Failure) event;
assertEquals(code, failure.response.code());
if (body != null) {
assertEquals(body, failure.responseBody);
}
assertEquals(cls, failure.t.getClass());
assertEquals(message, failure.t.getMessage());
}
/** Expose this recorder as a frame callback and shim in "ping" events. */
public WebSocketReader.FrameCallback asFrameCallback() {
return new WebSocketReader.FrameCallback() {
@Override public void onReadMessage(String text) throws IOException {
onMessage(null, text);
}
@Override public void onReadMessage(ByteString bytes) throws IOException {
onMessage(null, bytes);
}
@Override public void onReadPing(ByteString payload) {
events.add(new Ping(payload));
}
@Override public void onReadPong(ByteString payload) {
events.add(new Pong(payload));
}
@Override public void onReadClose(int code, String reason) {
onClosing(null, code, reason);
}
};
}
static final class Open {
final WebSocket webSocket;
final Response response;
Open(WebSocket webSocket, Response response) {
this.webSocket = webSocket;
this.response = response;
}
@Override public String toString() {
return "Open[" + response + "]";
}
}
static final class Failure {
final Throwable t;
final Response response;
final String responseBody;
Failure(Throwable t, Response response) {
this.t = t;
this.response = response;
String responseBody = null;
if (response != null) {
try {
responseBody = response.body().string();
} catch (IOException ignored) {
}
}
this.responseBody = responseBody;
}
@Override public String toString() {
if (response == null) {
return "Failure[" + t + "]";
}
return "Failure[" + response + "]";
}
}
static final class Message {
public final ByteString bytes;
public final String string;
public Message(ByteString bytes) {
this.bytes = bytes;
this.string = null;
}
public Message(String string) {
this.bytes = null;
this.string = string;
}
@Override public String toString() {
return "Message[" + (bytes != null ? bytes : string) + "]";
}
@Override public int hashCode() {
return (bytes != null ? bytes : string).hashCode();
}
@Override public boolean equals(Object other) {
return other instanceof Message
&& Util.equal(((Message) other).bytes, bytes)
&& Util.equal(((Message) other).string, string);
}
}
static final class Ping {
public final ByteString payload;
public Ping(ByteString payload) {
this.payload = payload;
}
@Override public String toString() {
return "Ping[" + payload + "]";
}
@Override public int hashCode() {
return payload.hashCode();
}
@Override public boolean equals(Object other) {
return other instanceof Ping
&& ((Ping) other).payload.equals(payload);
}
}
static final class Pong {
public final ByteString payload;
public Pong(ByteString payload) {
this.payload = payload;
}
@Override public String toString() {
return "Pong[" + payload + "]";
}
@Override public int hashCode() {
return payload.hashCode();
}
@Override public boolean equals(Object other) {
return other instanceof Pong
&& ((Pong) other).payload.equals(payload);
}
}
static final class Closing {
public final int code;
public final String reason;
Closing(int code, String reason) {
this.code = code;
this.reason = reason;
}
@Override public String toString() {
return "Closing[" + code + " " + reason + "]";
}
@Override public int hashCode() {
return code * 37 + reason.hashCode();
}
@Override public boolean equals(Object other) {
return other instanceof Closing
&& ((Closing) other).code == code
&& ((Closing) other).reason.equals(reason);
}
}
static final class Closed {
public final int code;
public final String reason;
Closed(int code, String reason) {
this.code = code;
this.reason = reason;
}
@Override public String toString() {
return "Closed[" + code + " " + reason + "]";
}
@Override public int hashCode() {
return code * 37 + reason.hashCode();
}
@Override public boolean equals(Object other) {
return other instanceof Closed
&& ((Closed) other).code == code
&& ((Closed) other).reason.equals(reason);
}
}
}