-
Notifications
You must be signed in to change notification settings - Fork 304
DialogTestClient for Py #341
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
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
|
|
||
| ============================ | ||
| BotBuilder-Testing SDK for Python | ||
| ============================ | ||
|
|
||
| .. image:: https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI?branchName=master | ||
| :target: https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI | ||
| :align: right | ||
| :alt: Azure DevOps status for master branch | ||
| .. image:: https://badge.fury.io/py/botbuilder-testing.svg | ||
| :target: https://badge.fury.io/py/botbuilder-testing | ||
| :alt: Latest PyPI package version | ||
|
|
||
| Some helper classes useful for testing bots built with Microsoft BotBuilder. | ||
|
|
||
|
|
||
| How to Install | ||
| ============== | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| pip install botbuilder-testing | ||
|
|
||
|
|
||
| Documentation/Wiki | ||
| ================== | ||
|
|
||
| You can find more information on the botbuilder-python project by visiting our `Wiki`_. | ||
|
|
||
| Requirements | ||
| ============ | ||
|
|
||
| * `Python >= 3.7.0`_ | ||
|
|
||
|
|
||
| Source Code | ||
| =========== | ||
| The latest developer version is available in a github repository: | ||
| https://github.com/Microsoft/botbuilder-python/ | ||
|
|
||
|
|
||
| Contributing | ||
| ============ | ||
|
|
||
| This project welcomes contributions and suggestions. Most contributions require you to agree to a | ||
| Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us | ||
| the rights to use your contribution. For details, visit https://cla.microsoft.com. | ||
|
|
||
| When you submit a pull request, a CLA-bot will automatically determine whether you need to provide | ||
| a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions | ||
| provided by the bot. You will only need to do this once across all repos using our CLA. | ||
|
|
||
| This project has adopted the `Microsoft Open Source Code of Conduct`_. | ||
| For more information see the `Code of Conduct FAQ`_ or | ||
| contact `[email protected]`_ with any additional questions or comments. | ||
|
|
||
| Reporting Security Issues | ||
| ========================= | ||
|
|
||
| Security issues and bugs should be reported privately, via email, to the Microsoft Security | ||
| Response Center (MSRC) at `[email protected]`_. You should | ||
| receive a response within 24 hours. If for some reason you do not, please follow up via | ||
| email to ensure we received your original message. Further information, including the | ||
| `MSRC PGP`_ key, can be found in | ||
| the `Security TechCenter`_. | ||
|
|
||
| License | ||
| ======= | ||
|
|
||
| Copyright (c) Microsoft Corporation. All rights reserved. | ||
|
|
||
| Licensed under the MIT_ License. | ||
|
|
||
| .. _Wiki: https://github.com/Microsoft/botbuilder-python/wiki | ||
| .. _Python >= 3.7.0: https://www.python.org/downloads/ | ||
| .. _MIT: https://github.com/Microsoft/vscode/blob/master/LICENSE.txt | ||
| .. _Microsoft Open Source Code of Conduct: https://opensource.microsoft.com/codeofconduct/ | ||
| .. _Code of Conduct FAQ: https://opensource.microsoft.com/codeofconduct/faq/ | ||
| .. [email protected]: mailto:[email protected] | ||
| .. [email protected]: mailto:[email protected] | ||
| .. _MSRC PGP: https://technet.microsoft.com/en-us/security/dn606155 | ||
| .. _Security TechCenter: https://github.com/Microsoft/vscode/blob/master/LICENSE.txt | ||
|
|
||
| .. <https://technet.microsoft.com/en-us/security/default>`_ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. | ||
|
|
||
| from .dialog_test_client import DialogTestClient | ||
| from .dialog_test_logger import DialogTestLogger | ||
|
|
||
|
|
||
| __all__ = ["DialogTestClient", "DialogTestLogger"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. | ||
|
|
||
| import os | ||
|
|
||
|
|
||
| __title__ = "botbuilder-testing" | ||
| __version__ = ( | ||
| os.environ["packageVersion"] if "packageVersion" in os.environ else "4.4.0b1" | ||
| ) | ||
| __uri__ = "https://www.github.com/Microsoft/botbuilder-python" | ||
| __author__ = "Microsoft" | ||
| __description__ = "Microsoft Bot Framework Bot Builder" | ||
| __summary__ = "Microsoft Bot Framework Bot Builder SDK for Python." | ||
| __license__ = "MIT" |
122 changes: 122 additions & 0 deletions
122
libraries/botbuilder-testing/botbuilder/testing/dialog_test_client.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. | ||
|
|
||
| from typing import List, Union | ||
|
|
||
| from botbuilder.core import ( | ||
| AutoSaveStateMiddleware, | ||
| ConversationState, | ||
| MemoryStorage, | ||
| Middleware, | ||
| StatePropertyAccessor, | ||
| TurnContext, | ||
| ) | ||
| from botbuilder.core.adapters import TestAdapter | ||
| from botbuilder.dialogs import Dialog, DialogSet, DialogTurnResult, DialogTurnStatus | ||
| from botbuilder.schema import Activity, ConversationReference | ||
|
|
||
|
|
||
| class DialogTestClient: | ||
| """A client for testing dialogs in isolation.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| channel_or_adapter: Union[str, TestAdapter], | ||
| target_dialog: Dialog, | ||
| initial_dialog_options: object = None, | ||
| middlewares: List[Middleware] = None, | ||
| conversation_state: ConversationState = None, | ||
| ): | ||
| """ | ||
| Create a DialogTestClient to test a dialog without having to create a full-fledged adapter. | ||
|
|
||
| ```python | ||
| client = DialogTestClient("test", MY_DIALOG, MY_OPTIONS) | ||
| reply = await client.send_activity("first message") | ||
| self.assertEqual(reply.text, "first reply", "reply failed") | ||
| ``` | ||
|
|
||
| :param channel_or_adapter: The channel Id or test adapter to be used for the test. | ||
| For channel Id, use 'emulator' or 'test' if you are uncertain of the channel you are targeting. | ||
| Otherwise, it is recommended that you use the id for the channel(s) your bot will be using and | ||
| write a test case for each channel. | ||
| Or, a test adapter instance can be used. | ||
| :type channel_or_adapter: Union[str, TestAdapter] | ||
| :param target_dialog: The dialog to be tested. This will be the root dialog for the test client. | ||
| :type target_dialog: Dialog | ||
| :param initial_dialog_options: (Optional) additional argument(s) to pass to the dialog being started. | ||
| :type initial_dialog_options: object | ||
| :param middlewares: (Optional) The test adapter to use. If this parameter is not provided, the test client will | ||
| use a default TestAdapter. | ||
| :type middlewares: List[Middleware] | ||
| :param conversation_state: (Optional) A ConversationState instance to use in the test client. | ||
| :type conversation_state: ConversationState | ||
| """ | ||
| self.dialog_turn_result: DialogTurnResult = None | ||
| self.conversation_state: ConversationState = ( | ||
| ConversationState(MemoryStorage()) | ||
| if conversation_state is None | ||
| else conversation_state | ||
| ) | ||
| dialog_state = self.conversation_state.create_property("DialogState") | ||
| self._callback = self._get_default_callback( | ||
| target_dialog, initial_dialog_options, dialog_state | ||
| ) | ||
|
|
||
| if isinstance(channel_or_adapter, str): | ||
| conversation_reference = ConversationReference( | ||
| channel_id=channel_or_adapter | ||
| ) | ||
| self.test_adapter = TestAdapter(self._callback, conversation_reference) | ||
| self.test_adapter.use( | ||
| AutoSaveStateMiddleware().add(self.conversation_state) | ||
| ) | ||
| else: | ||
| self.test_adapter = channel_or_adapter | ||
|
|
||
| self._add_user_middlewares(middlewares) | ||
|
|
||
| async def send_activity(self, activity) -> Activity: | ||
| """ | ||
| Send an activity into the dialog. | ||
|
|
||
| :param activity: an activity potentially with text. | ||
| :type activity: | ||
| :return: a TestFlow that can be used to assert replies etc. | ||
| :rtype: Activity | ||
| """ | ||
| await self.test_adapter.receive_activity(activity) | ||
| return self.test_adapter.get_next_activity() | ||
|
|
||
| def get_next_reply(self) -> Activity: | ||
| """ | ||
| Get the next reply waiting to be delivered (if one exists) | ||
|
|
||
| :return: a TestFlow that can be used to assert replies etc. | ||
| :rtype: Activity | ||
| """ | ||
| return self.test_adapter.get_next_activity() | ||
|
|
||
| def _get_default_callback( | ||
| self, | ||
| target_dialog: Dialog, | ||
| initial_dialog_options: object, | ||
| dialog_state: StatePropertyAccessor, | ||
| ): | ||
| async def default_callback(turn_context: TurnContext) -> None: | ||
| dialog_set = DialogSet(dialog_state) | ||
| dialog_set.add(target_dialog) | ||
|
|
||
| dialog_context = await dialog_set.create_context(turn_context) | ||
| self.dialog_turn_result = await dialog_context.continue_dialog() | ||
| if self.dialog_turn_result.status == DialogTurnStatus.Empty: | ||
| self.dialog_turn_result = await dialog_context.begin_dialog( | ||
| target_dialog.id, initial_dialog_options | ||
| ) | ||
|
|
||
| return default_callback | ||
|
|
||
| def _add_user_middlewares(self, middlewares: List[Middleware]) -> None: | ||
| if middlewares is not None: | ||
| for middleware in middlewares: | ||
| self.test_adapter.use(middleware) | ||
103 changes: 103 additions & 0 deletions
103
libraries/botbuilder-testing/botbuilder/testing/dialog_test_logger.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| # Copyright (c) Microsoft Corporation. All rights reserved. | ||
| # Licensed under the MIT License. | ||
|
|
||
| import json | ||
| import logging | ||
| import time | ||
| import uuid | ||
| from datetime import datetime | ||
| from typing import Awaitable, Callable, List | ||
|
|
||
| from botbuilder.core import Middleware, TurnContext | ||
| from botbuilder.schema import Activity, ActivityTypes, ResourceResponse | ||
|
|
||
|
|
||
| class DialogTestLogger(Middleware): | ||
| """ | ||
| A middleware to output incoming and outgoing activities as json strings to the console during | ||
| unit tests. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| log_func: Callable[..., None] = None, | ||
| json_indent: int = 4, | ||
daveta marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| time_func: Callable[[], float] = None, | ||
| ): | ||
| """ | ||
| Initialize a new instance of the dialog test logger. | ||
|
|
||
| :param log_func: A callable method or object that can log a message, | ||
| default to `logging.getLogger(__name__).info`. | ||
| :type log_func: Callable[..., None] | ||
| :param json_indent: An indent for json output, default indent is 4. | ||
| :type json_indent: int | ||
| :param time_func: A time function to record time spans, default to `time.monotonic`. | ||
| :type time_func: Callable[[], float] | ||
| """ | ||
| self._log = logging.getLogger(__name__).info if log_func is None else log_func | ||
| self._stopwatch_state_key = f"stopwatch.{uuid.uuid4()}" | ||
| self._json_indent = json_indent | ||
| self._time_func = time.monotonic if time_func is None else time_func | ||
|
|
||
| async def on_turn( | ||
| self, context: TurnContext, logic: Callable[[TurnContext], Awaitable] | ||
| ): | ||
| context.turn_state[self._stopwatch_state_key] = self._time_func() | ||
| await self._log_incoming_activity(context, context.activity) | ||
| context.on_send_activities(self._send_activities_handler) | ||
| await logic() | ||
|
|
||
| async def _log_incoming_activity( | ||
| self, context: TurnContext, activity: Activity | ||
| ) -> None: | ||
| self._log("") | ||
| if context.activity.type == ActivityTypes.message: | ||
| self._log("User: Text = %s", context.activity.text) | ||
| else: | ||
| self._log_activity_as_json(actor="User", activity=activity) | ||
|
|
||
| timestamp = self._get_timestamp() | ||
| self._log("-> ts: %s", timestamp) | ||
|
|
||
| async def _send_activities_handler( | ||
| self, | ||
| context: TurnContext, | ||
| activities: List[Activity], | ||
| next_send: Callable[[], Awaitable[None]], | ||
| ) -> List[ResourceResponse]: | ||
| for activity in activities: | ||
| await self._log_outgoing_activity(context, activity) | ||
| responses = await next_send() | ||
| return responses | ||
|
|
||
| async def _log_outgoing_activity( | ||
| self, context: TurnContext, activity: Activity | ||
| ) -> None: | ||
| self._log("") | ||
| start_time = context.turn_state[self._stopwatch_state_key] | ||
| if activity.type == ActivityTypes.message: | ||
| message = ( | ||
| f"Bot: Text = {activity.text}\r\n" | ||
| f" Speak = {activity.speak}\r\n" | ||
| f" InputHint = {activity.input_hint}" | ||
| ) | ||
| self._log(message) | ||
| else: | ||
| self._log_activity_as_json(actor="Bot", activity=activity) | ||
|
|
||
| now = self._time_func() | ||
| mms = int(round((now - start_time) * 1000)) | ||
| timestamp = self._get_timestamp() | ||
| self._log("-> ts: %s elapsed %d ms", timestamp, mms) | ||
|
|
||
| def _log_activity_as_json(self, actor: str, activity: Activity) -> None: | ||
| activity_dict = activity.serialize() | ||
| activity_json = json.dumps(activity_dict, indent=self._json_indent) | ||
| message = f"{actor}: Activity = {activity.type}\r\n" f"{activity_json}" | ||
| self._log(message) | ||
|
|
||
| @staticmethod | ||
| def _get_timestamp() -> str: | ||
| timestamp = datetime.now().strftime("%H:%M:%S") | ||
| return timestamp | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| botbuilder-schema>=4.4.0b1 | ||
| botbuilder-core>=4.4.0b1 | ||
| botbuilder-dialogs>=4.4.0b1 | ||
| aiounittest>=1.1.0 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.