Skip to content

Commit

Permalink
a big one - websocket support and async helpers #395
Browse files Browse the repository at this point in the history
  • Loading branch information
ptrthomas committed Oct 30, 2018
1 parent 6ac0bba commit 99ca657
Show file tree
Hide file tree
Showing 19 changed files with 387 additions and 16 deletions.
14 changes: 11 additions & 3 deletions karate-core/src/main/java/com/intuit/karate/ScriptValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ public boolean isBooleanTrue() {
public boolean isPrimitive() {
return type == Type.PRIMITIVE;
}

public Number getAsNumber() {
return getValue(Number.class);
}

public boolean isNumber() {
return type == Type.PRIMITIVE && Number.class.isAssignableFrom(value.getClass());
}

public boolean isFunction() {
return type == Type.JS_FUNCTION;
Expand Down Expand Up @@ -264,14 +272,14 @@ public Map<String, Object> getAsMap() {
}
}

public ScriptValue invokeFunction(ScenarioContext context) {
public ScriptValue invokeFunction(ScenarioContext context, Object callArg) {
ScriptObjectMirror som = getValue(ScriptObjectMirror.class);
return Script.evalFunctionCall(som, null, context);
return Script.evalFunctionCall(som, callArg, context);
}

public Map<String, Object> evalAsMap(ScenarioContext context) {
if (isFunction()) {
ScriptValue sv = invokeFunction(context);
ScriptValue sv = invokeFunction(context, null);
return sv.isMapLike() ? sv.getAsMap() : null;
} else {
return isMapLike() ? getAsMap() : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ public HttpResponse buildResponse(HttpRequest request, long startTime) {
// functions here are outside of the 'transaction' and should not mutate global state !
// typically this is where users can set up an artificial delay or sleep
if (afterScenario != null && afterScenario.isFunction()) {
afterScenario.invokeFunction(context);
afterScenario.invokeFunction(context, null);
}
return response;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import jdk.nashorn.api.scripting.ScriptObjectMirror;

Expand Down Expand Up @@ -492,7 +494,7 @@ public void invokeAfterHookIfConfigured(boolean afterFeature) {
ScriptValue sv = afterFeature ? config.getAfterFeature() : config.getAfterScenario();
if (sv.isFunction()) {
try {
sv.invokeFunction(this);
sv.invokeFunction(this, null);
} catch (Exception e) {
String prefix = afterFeature ? "afterFeature" : "afterScenario";
logger.warn("{} hook failed: {}", prefix, e.getMessage());
Expand Down Expand Up @@ -812,6 +814,40 @@ public void embed(byte[] bytes, String contentType) {
prevEmbed = embed;
}

private final Object LOCK = new Object();
private ExecutorService executor;

public void signal() {
logger.info("signal called");
synchronized(LOCK) {
LOCK.notify();
}
}

public void listen(long timeout, Runnable runnable) {
if (executor == null) {
executor = Executors.newSingleThreadExecutor();
}
logger.trace("submitting listen function");
executor.submit(runnable);
synchronized(LOCK) {
try {
logger.info("entered listen wait state");
LOCK.wait(timeout);
logger.info("exit listen wait state");
} catch (InterruptedException e) {
logger.error("listen timed out: {}", e.getMessage());
}
}
}

public void listen(long timeout, ScriptValue callback) {
if (!callback.isFunction()) {
throw new RuntimeException("listen expression - expected function, but was: " + callback);
}
listen(timeout, () -> callback.invokeFunction(this, null));
}

//==========================================================================

public void driver(String expression) {
Expand Down Expand Up @@ -847,6 +883,9 @@ public void submit(String name) {
}

public void stop() {
if (executor != null) {
executor.shutdownNow();
}
if (driver != null) {
driver.quit();
driver = null;
Expand Down
35 changes: 35 additions & 0 deletions karate-core/src/main/java/com/intuit/karate/core/ScriptBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
import com.intuit.karate.http.HttpResponse;
import com.intuit.karate.http.HttpUtils;
import com.intuit.karate.http.MultiValuedMap;
import com.intuit.karate.netty.WebSocketClient;
import com.intuit.karate.netty.WebSocketListener;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import java.io.File;
Expand Down Expand Up @@ -352,8 +354,41 @@ public void write(Object o, String path) {
ScriptValue sv = new ScriptValue(o);
path = Engine.getBuildDir() + File.separator + path;
FileUtils.writeToFile(new File(path), sv.getAsByteArray());
}

public WebSocketClient websocket(String url, ScriptObjectMirror som) {
if (!som.isFunction()) {
throw new RuntimeException("not a JS function: " + som);
}
ScriptValue sv = new ScriptValue(som);
WebSocketClient client = new WebSocketClient(url, new WebSocketListener() {
@Override
public void onMessage(String text) {
sv.invokeFunction(context, text);
}
@Override
public void onMessage(byte[] bytes) {
this.onMessage(FileUtils.toString(bytes));
}
});
return client;
}

public void signal() {
context.signal();
}

public void listen(long timeout, ScriptObjectMirror som) {
if (!som.isFunction()) {
throw new RuntimeException("not a JS function: " + som);
}
context.listen(timeout, new ScriptValue(som));
}

public void listen(long timeout) {
context.listen(timeout, () -> {});
}

private ScriptValue getValue(String name) {
ScriptValue sv = context.vars.get(name);
return sv == null ? ScriptValue.NULL : sv;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ public class WebSocketClient {
private final Channel channel;
private final EventLoopGroup group;


private boolean waiting;

public WebSocketClient(String url, WebSocketListener listener) {
Expand Down
12 changes: 12 additions & 0 deletions karate-core/src/test/java/com/intuit/karate/ScriptValueTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ public void testTypeDetection() {
assertEquals(JSON, sv.getType());
Object temp = doc.read("$");
assertTrue(temp instanceof List);
sv = new ScriptValue(1);
assertTrue(sv.isPrimitive());
assertTrue(sv.isNumber());
assertEquals(1, sv.getAsNumber().intValue());
sv = new ScriptValue(100L);
assertTrue(sv.isPrimitive());
assertTrue(sv.isNumber());
assertEquals(100, sv.getAsNumber().longValue());
sv = new ScriptValue(1.0);
assertTrue(sv.isPrimitive());
assertTrue(sv.isNumber());
assertEquals(1.0, sv.getAsNumber().doubleValue(), 0);
}

}
4 changes: 2 additions & 2 deletions karate-demo/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
Expand Down Expand Up @@ -71,7 +71,7 @@
<dependency>
<groupId>net.masterthought</groupId>
<artifactId>cucumber-reporting</artifactId>
<version>3.8.0</version>
<version>${cucumber.reporting.version}</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ protected void configure(HttpSecurity http) throws Exception {
"/redirect/**",
"/graphql/**",
"/soap/**",
"/echo/**"
"/echo/**",
"/websocket/**",
"/websocket-controller/**"
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* The MIT License
*
* Copyright 2018 Intuit Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.intuit.karate.demo.config;

import com.intuit.karate.demo.controller.WebSocketHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

/**
*
* @author pthomas3
*/
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(handler(), "/websocket");
}

@Bean
WebSocketHandler handler() {
return new WebSocketHandler();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@
@RequestMapping("/greeting")
public class GreetingController {

private static final String TEMPLATE = "Hello %s!";
private final AtomicInteger counter = new AtomicInteger();

@GetMapping("/reset")
public String reset() {
int value = 0;
Expand All @@ -49,8 +48,8 @@ public String reset() {
}

@GetMapping
public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
return new Greeting(counter.incrementAndGet(), String.format(TEMPLATE, name));
public Greeting getGreeting(@RequestParam(value = "name", defaultValue = "World") String name) {
return new Greeting(counter.incrementAndGet(), "Hello " + name + "!");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* The MIT License
*
* Copyright 2018 Intuit Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.intuit.karate.demo.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.intuit.karate.demo.domain.Greeting;
import com.intuit.karate.demo.domain.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
*
* @author pthomas3
*/
@RestController
@RequestMapping("/websocket-controller")
public class WebSocketController {

private static final Logger logger = LoggerFactory.getLogger(WebSocketController.class);

@Autowired(required = true)
private WebSocketHandler handler;

private final ObjectMapper mapper = new ObjectMapper();

@PostMapping
public String greet(@RequestBody Message message) throws Exception {
long time = System.currentTimeMillis();
Greeting greeting = new Greeting(time, "hello " + message.getText() + " !");
String json = mapper.writeValueAsString(greeting);
handler.broadcast(json);
return "{ \"id\": " + time + " }";
}

}
Loading

0 comments on commit 99ca657

Please sign in to comment.