Added ShowTypingMiddleware and tests

This commit is contained in:
Tracy Boehrer 2019-09-24 17:19:53 -05:00
Родитель 338492b949
Коммит 77a7d8fcd7
6 изменённых файлов: 233 добавлений и 48 удалений

Просмотреть файл

@ -0,0 +1,104 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.microsoft.bot.builder;
import com.microsoft.bot.connector.ExecutorFactory;
import com.microsoft.bot.schema.Activity;
import com.microsoft.bot.schema.ActivityTypes;
import com.microsoft.bot.schema.ConversationReference;
import com.microsoft.bot.schema.ResourceResponse;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
/**
* When added, this middleware will send typing activities back to the user when a Message activity
* is received to let them know that the bot has received the message and is working on the response.
* You can specify a delay in milliseconds before the first typing activity is sent and then a frequency,
* also in milliseconds which determines how often another typing activity is sent. Typing activities
* will continue to be sent until your bot sends another message back to the user.
*/
public class ShowTypingMiddleware implements Middleware {
/**
* Initial delay before sending first typing indicator. Defaults to 500ms.
*/
private long delay;
/**
* Rate at which additional typing indicators will be sent. Defaults to every 2000ms.
*/
private long period;
public ShowTypingMiddleware() {
this(500, 2000);
}
/**
* Initializes a new instance of the ShowTypingMiddleware class.
*
* @param withDelay Initial delay before sending first typing indicator.
* @param withPeriod Rate at which additional typing indicators will be sent.
*/
public ShowTypingMiddleware(long withDelay, long withPeriod) {
if (withDelay < 0) {
throw new IllegalArgumentException("Delay must be greater than or equal to zero");
}
if (withPeriod < 0) {
throw new IllegalArgumentException("Repeat period must be greater than zero");
}
delay = withDelay;
period = withPeriod;
}
/**
* Processes an incoming activity.
*
* @param turnContext The context object for this turn.
* @param next The delegate to call to continue the bot middleware pipeline.
* @return A task that represents the work queued to execute.
*/
@Override
public CompletableFuture<Void> onTurn(TurnContext turnContext, NextDelegate next) {
if (!turnContext.getActivity().isType(ActivityTypes.MESSAGE)) {
return next.next();
}
// do not await task - we want this to run in the background and we will cancel it when its done
CompletableFuture sendFuture = sendTyping(turnContext, delay, period);
return next.next()
.thenAccept(result -> sendFuture.cancel(true));
}
private static CompletableFuture<Void> sendTyping(TurnContext turnContext, long delay, long period) {
return CompletableFuture.runAsync(() -> {
try {
Thread.sleep(delay);
while (!Thread.currentThread().isInterrupted()) {
sendTypingActivity(turnContext).join();
Thread.sleep(period);
}
} catch (InterruptedException e) {
// do nothing
}
}, ExecutorFactory.getExecutor());
}
private static CompletableFuture<ResourceResponse[]> sendTypingActivity(TurnContext turnContext) {
// create a TypingActivity, associate it with the conversation and send immediately
Activity typingActivity = new Activity(ActivityTypes.TYPING) {{
setRelatesTo(turnContext.getActivity().getRelatesTo());
}};
// sending the Activity directly on the Adapter avoids other Middleware and avoids setting the Responded
// flag, however, this also requires that the conversation reference details are explicitly added.
ConversationReference conversationReference = turnContext.getActivity().getConversationReference();
typingActivity.applyConversationReference(conversationReference);
// make sure to send the Activity directly on the Adapter rather than via the TurnContext
return turnContext.getAdapter().sendActivities(turnContext, Collections.singletonList(typingActivity));
}
}

Просмотреть файл

@ -0,0 +1,96 @@
package com.microsoft.bot.builder;
import com.microsoft.bot.builder.adapters.TestAdapter;
import com.microsoft.bot.builder.adapters.TestFlow;
import com.microsoft.bot.schema.Activity;
import com.microsoft.bot.schema.ActivityTypes;
import org.junit.Assert;
import org.junit.Test;
import java.util.concurrent.CompletableFuture;
public class ShowTypingMiddlewareTests {
@Test
public void ShowTyping_TestMiddleware_1_Second_Interval() {
TestAdapter adapter = new TestAdapter()
.use(new ShowTypingMiddleware(100, 1000));
new TestFlow(adapter, (turnContext -> {
try {
Thread.sleep(2500);
} catch (InterruptedException e) {
// do nothing
}
Assert.assertFalse(turnContext.getResponded());
turnContext.sendActivity("Message send after delay").join();
return CompletableFuture.completedFuture(null);
}))
.send("foo")
.assertReply(this::validateTypingActivity)
.assertReply(this::validateTypingActivity)
.assertReply(this::validateTypingActivity)
.assertReply("Message send after delay")
.startTest().join();
}
@Test
public void ShowTyping_TestMiddleware_Context_Completes_Before_Typing_Interval() {
TestAdapter adapter = new TestAdapter()
.use(new ShowTypingMiddleware(100, 5000));
new TestFlow(adapter, (turnContext -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// do nothing
}
turnContext.sendActivity("Message send after delay").join();
return CompletableFuture.completedFuture(null);
}))
.send("foo")
.assertReply(this::validateTypingActivity)
.assertReply("Message send after delay")
.startTest().join();
}
@Test
public void ShowTyping_TestMiddleware_ImmediateResponse_5SecondInterval() {
TestAdapter adapter = new TestAdapter()
.use(new ShowTypingMiddleware(2000, 5000));
new TestFlow(adapter, (turnContext -> {
turnContext.sendActivity("Message send after delay").join();
return CompletableFuture.completedFuture(null);
}))
.send("foo")
.assertReply("Message send after delay")
.startTest().join();
}
@Test(expected = IllegalArgumentException.class)
public void ShowTyping_TestMiddleware_NegativeDelay() {
TestAdapter adapter = new TestAdapter()
.use(new ShowTypingMiddleware(-100, 5000));
}
@Test(expected = IllegalArgumentException.class)
public void ShowTyping_TestMiddleware_ZeroFrequency() {
TestAdapter adapter = new TestAdapter()
.use(new ShowTypingMiddleware(-100, 0));
}
@Test(expected = IllegalArgumentException.class)
public void ShowTyping_TestMiddleware_NegativePerion() {
TestAdapter adapter = new TestAdapter()
.use(new ShowTypingMiddleware(500, -500));
}
private void validateTypingActivity(Activity obj) {
if (!obj.isType(ActivityTypes.TYPING)) {
throw new RuntimeException("Activity was not of type TypingActivity");
}
}
}

Просмотреть файл

@ -1,16 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.microsoft.bot.builder;
public class StateSettings {
private boolean lastWriterWins = true;
public boolean getLastWriterWins() {
return this.lastWriterWins;
}
public void setLast(boolean lastWriterWins) {
this.lastWriterWins = lastWriterWins;
}
}

Просмотреть файл

@ -95,7 +95,7 @@ public class TranscriptMiddlewareTest {
}
@Test
public void Transcript_LogUpdateActivities() throws InterruptedException {
public void Transcript_LogUpdateActivities() {
MemoryTranscriptStore transcriptStore = new MemoryTranscriptStore();
TestAdapter adapter = (new TestAdapter()).use(new TranscriptLoggerMiddleware(transcriptStore));
final String[] conversationId = {null};
@ -117,25 +117,23 @@ public class TranscriptMiddlewareTest {
return CompletableFuture.completedFuture(null);
})
.send("foo")
.delay(50)
.send("update")
.delay(50)
.assertReply("new response")
.startTest().join();
Thread.sleep(500);
PagedResult pagedResult = transcriptStore.getTranscriptActivities("test", conversationId[0]).join();
Assert.assertEquals(4, pagedResult.getItems().size());
Assert.assertEquals("foo", ((Activity) pagedResult.getItems().get(0)).getText());
Assert.assertEquals("response", ((Activity) pagedResult.getItems().get(1)).getText());
if (!StringUtils.equals(((Activity)pagedResult.getItems().get(2)).getText(), "new response")) {
Assert.fail("fail");
}
Assert.assertEquals("new response", ((Activity)pagedResult.getItems().get(2)).getText());
Assert.assertEquals("update", ((Activity)pagedResult.getItems().get(3)).getText());
Assert.assertEquals( ((Activity)pagedResult.getItems().get(1)).getId(), ((Activity) pagedResult.getItems().get(2)).getId());
}
@Test
public final void Transcript_LogDeleteActivities() throws InterruptedException {
public final void Transcript_LogDeleteActivities() {
MemoryTranscriptStore transcriptStore = new MemoryTranscriptStore();
TestAdapter adapter = (new TestAdapter()).use(new TranscriptLoggerMiddleware(transcriptStore));
final String[] conversationId = {null};
@ -153,11 +151,11 @@ public class TranscriptMiddlewareTest {
return CompletableFuture.completedFuture(null);
})
.send("foo")
.delay(50)
.assertReply("response")
.send("deleteIt")
.startTest().join();
Thread.sleep(500);
PagedResult pagedResult = transcriptStore.getTranscriptActivities("test", conversationId[0]).join();
for (Object act : pagedResult.getItems()) {
System.out.printf("Here is the object: %s : Type: %s\n", act.getClass().getTypeName(), ((Activity) act).getType());
@ -175,7 +173,7 @@ public class TranscriptMiddlewareTest {
}
@Test
public void Transcript_TestDateLogUpdateActivities() throws InterruptedException {
public void Transcript_TestDateLogUpdateActivities() {
OffsetDateTime dateTimeStartOffset1 = OffsetDateTime.now();
OffsetDateTime dateTimeStartOffset2 = OffsetDateTime.now(ZoneId.of("UTC"));
@ -200,12 +198,12 @@ public class TranscriptMiddlewareTest {
return CompletableFuture.completedFuture(null);
})
.send("foo")
.delay(50)
.send("update")
.delay(50)
.assertReply("new response")
.startTest().join();
Thread.sleep(500);
PagedResult pagedResult = transcriptStore.getTranscriptActivities("test", conversationId[0], null, dateTimeStartOffset1).join();
Assert.assertEquals(4, pagedResult.getItems().size());
Assert.assertEquals("foo", ((Activity) pagedResult.getItems().get(0)).getText());

Просмотреть файл

@ -57,8 +57,7 @@ public class TestAdapter extends BotAdapter {
return this;
}
public CompletableFuture<Void> processActivity(Activity activity,
BotCallbackHandler callback) {
public CompletableFuture<Void> processActivity(Activity activity, BotCallbackHandler callback) {
return CompletableFuture.supplyAsync(() -> {
synchronized (conversationReference()) {
// ready for next reply
@ -69,6 +68,7 @@ public class TestAdapter extends BotAdapter {
activity.setRecipient(conversationReference().getBot());
activity.setConversation(conversationReference().getConversation());
activity.setServiceUrl(conversationReference().getServiceUrl());
Integer next = nextId++;
activity.setId(next.toString());
}
@ -104,11 +104,11 @@ public class TestAdapter extends BotAdapter {
responses.add(new ResourceResponse(activity.getId()));
System.out.println(String.format("TestAdapter:SendActivities(tid:%s):Count:%s", Thread.currentThread().getId(), activities.size()));
System.out.println(String.format("TestAdapter:SendActivities, Count:%s (tid:%s)", activities.size(), Thread.currentThread().getId()));
for (Activity act : activities) {
System.out.printf(":--------\n: To:%s\n", act.getRecipient().getName());
System.out.printf(": From:%s\n", (act.getFrom() == null) ? "No from set" : act.getFrom().getName());
System.out.printf(": Text:%s\n:---------", (act.getText() == null) ? "No text set" : act.getText());
System.out.printf(" :--------\n : To:%s\n", act.getRecipient().getName());
System.out.printf(" : From:%s\n", (act.getFrom() == null) ? "No from set" : act.getFrom().getName());
System.out.printf(" : Text:%s\n :---------\n", (act.getText() == null) ? "No text set" : act.getText());
}
// This is simulating DELAY

Просмотреть файл

@ -62,7 +62,7 @@ public class TestFlow {
return new TestFlow(
testTask
.thenCompose(result -> {
System.out.print(String.format("USER SAYS: %s (Thread Id: %s)\n", userSays, Thread.currentThread().getId()));
System.out.print(String.format("USER SAYS: %s (tid: %s)\n", userSays, Thread.currentThread().getId()));
return this.adapter.sendTextToBot(userSays, this.callback);
}), this);
}
@ -79,7 +79,7 @@ public class TestFlow {
return new TestFlow(
testTask.thenCompose(result -> {
System.out.printf("TestFlow(%s): Send with User Activity! %s", Thread.currentThread().getId(), userActivity.getText());
System.out.printf("TestFlow: Send with User Activity! %s (tid:%s)", userActivity.getText(), Thread.currentThread().getId());
return this.adapter.processActivity(userActivity, this.callback);
}
), this);
@ -92,17 +92,18 @@ public class TestFlow {
* @return
*/
public TestFlow delay(int ms) {
return new TestFlow(CompletableFuture.supplyAsync(() ->
{
System.out.printf("TestFlow(%s): Delay(%s ms) called. ", Thread.currentThread().getId(), ms);
System.out.flush();
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
return new TestFlow(
testTask
.thenCompose(result -> {
System.out.printf("TestFlow: Delay(%s ms) called. (tid:%s)\n", ms, Thread.currentThread().getId());
System.out.flush();
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
}
return null;
}, ExecutorFactory.getExecutor()), this);
}
return CompletableFuture.completedFuture(null);
}), this);
}
/**
@ -164,7 +165,7 @@ public class TestFlow {
public TestFlow assertReply(Consumer<Activity> validateActivity, String description, int timeout) {
return new TestFlow(testTask
.thenApply(result -> {
System.out.println(String.format("AssertReply: Starting loop : %s (Thread:%s)", description, Thread.currentThread().getId()));
System.out.println(String.format("AssertReply: Starting loop : %s (tid:%s)", description, Thread.currentThread().getId()));
System.out.flush();
int finalTimeout = Integer.MAX_VALUE;
@ -189,10 +190,12 @@ public class TestFlow {
// System.out.flush();
if (replyActivity != null) {
System.out.printf("AssertReply(tid:%s): Received Reply: %s ", Thread.currentThread().getId(), (replyActivity.getText() == null) ? "No Text set" : replyActivity.getText());
System.out.printf("AssertReply: Received Reply (tid:%s)", Thread.currentThread().getId());
System.out.flush();
System.out.printf("=============\n From: %s\n To:%s\n ==========\n", (replyActivity.getFrom() == null) ? "No from set" : replyActivity.getFrom().getName(),
(replyActivity.getRecipient() == null) ? "No recipient set" : replyActivity.getRecipient().getName());
System.out.printf("\n =============\n From: %s\n To:%s\n Text:%s\n ==========\n",
(replyActivity.getFrom() == null) ? "No from set" : replyActivity.getFrom().getName(),
(replyActivity.getRecipient() == null) ? "No recipient set" : replyActivity.getRecipient().getName(),
(replyActivity.getText() == null) ? "No Text set" : replyActivity.getText());
System.out.flush();
// if we have a reply