Skip to content
This repository was archived by the owner on Jan 5, 2026. It is now read-only.

Commit c5b11f3

Browse files
authored
Merge pull request #237 from microsoft/axsuarez/botstate-tests
added missing BotState tests and some state fixes
2 parents be59bf5 + 4bdd50d commit c5b11f3

File tree

5 files changed

+167
-28
lines changed

5 files changed

+167
-28
lines changed

libraries/botbuilder-core/botbuilder/core/bot_state.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ async def save_changes(self, turn_context: TurnContext, force: bool = False) ->
9494

9595
cached_state = turn_context.turn_state.get(self._context_service_key)
9696

97-
if force or (cached_state != None and cached_state.is_changed == True):
97+
if force or (cached_state is not None and cached_state.is_changed):
9898
storage_key = self.get_storage_key(turn_context)
9999
changes : Dict[str, object] = { storage_key: cached_state.state }
100100
await self._storage.write(changes)
@@ -132,7 +132,7 @@ async def delete(self, turn_context: TurnContext) -> None:
132132
await self._storage.delete({ storage_key })
133133

134134
@abstractmethod
135-
async def get_storage_key(self, turn_context: TurnContext) -> str:
135+
def get_storage_key(self, turn_context: TurnContext) -> str:
136136
raise NotImplementedError()
137137

138138
async def get_property_value(self, turn_context: TurnContext, property_name: str):

libraries/botbuilder-core/botbuilder/core/conversation_state.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,18 @@ def __init__(self, storage: Storage):
2121
Where to store
2222
namespace: str
2323
"""
24-
def call_get_storage_key(context):
25-
key = self.get_storage_key(context)
26-
if key is None:
27-
raise AttributeError(self.no_key_error_message)
28-
else:
29-
return key
3024

3125
super(ConversationState, self).__init__(storage, 'ConversationState')
3226

33-
3427
def get_storage_key(self, context: TurnContext):
35-
activity = context.activity
36-
channel_id = getattr(activity, 'channel_id', None)
37-
conversation_id = getattr(activity.conversation, 'id', None) if hasattr(activity, 'conversation') else None
28+
channel_id = context.activity.channel_id or self.__raise_type_error("invalid activity-missing channel_id")
29+
conversation_id = context.activity.conversation.id or self.__raise_type_error(
30+
"invalid activity-missing conversation.id")
3831

3932
storage_key = None
4033
if channel_id and conversation_id:
4134
storage_key = "%s/conversations/%s" % (channel_id,conversation_id)
4235
return storage_key
36+
37+
def __raise_type_error(self, err: str = 'NoneType found while expecting value'):
38+
raise TypeError(err)

libraries/botbuilder-core/botbuilder/core/memory_storage.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from typing import Dict, List
55
from .storage import Storage, StoreItem
6+
from copy import deepcopy
67

78

89
class MemoryStorage(Storage):
@@ -35,7 +36,6 @@ async def write(self, changes: Dict[str, StoreItem]):
3536
# iterate over the changes
3637
for (key, change) in changes.items():
3738
new_value = change
38-
old_state = None
3939
old_state_etag = None
4040

4141
# Check if the a matching key already exists in self.memory
@@ -51,13 +51,13 @@ async def write(self, changes: Dict[str, StoreItem]):
5151
new_state = new_value
5252

5353
# Set ETag if applicable
54-
if isinstance(new_value, StoreItem):
54+
if hasattr(new_value, 'e_tag'):
5555
if old_state_etag is not None and new_value.e_tag != "*" and new_value.e_tag < old_state_etag:
5656
raise KeyError("Etag conflict.\nOriginal: %s\r\nCurrent: %s" % \
5757
(new_value.e_tag, old_state_etag) )
5858
new_state.e_tag = str(self._e_tag)
5959
self._e_tag += 1
60-
self.memory[key] = new_state
60+
self.memory[key] = deepcopy(new_state)
6161

6262
except Exception as e:
6363
raise e

libraries/botbuilder-core/botbuilder/core/user_state.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,6 @@ def __init__(self, storage: Storage, namespace=''):
2121
"""
2222
self.namespace = namespace
2323

24-
def call_get_storage_key(context):
25-
key = self.get_storage_key(context)
26-
if key is None:
27-
raise AttributeError(self.no_key_error_message)
28-
else:
29-
return key
30-
3124
super(UserState, self).__init__(storage, "UserState")
3225

3326
def get_storage_key(self, context: TurnContext) -> str:
@@ -36,11 +29,14 @@ def get_storage_key(self, context: TurnContext) -> str:
3629
:param context:
3730
:return:
3831
"""
39-
activity = context.activity
40-
channel_id = getattr(activity, 'channel_id', None)
41-
user_id = getattr(activity.from_property, 'id', None) if hasattr(activity, 'from_property') else None
32+
channel_id = context.activity.channel_id or self.__raise_type_error("invalid activity-missing channelId")
33+
user_id = context.activity.from_property.id or self.__raise_type_error(
34+
"invalid activity-missing from_property.id")
4235

4336
storage_key = None
4437
if channel_id and user_id:
4538
storage_key = "%s/users/%s" % (channel_id, user_id)
4639
return storage_key
40+
41+
def __raise_type_error(self, err: str = 'NoneType found while expecting value'):
42+
raise TypeError(err)

libraries/botbuilder-core/tests/test_bot_state.py

Lines changed: 150 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import aiounittest
44
from unittest.mock import MagicMock
55

6-
from botbuilder.core import TurnContext, BotState, MemoryStorage, UserState
6+
from botbuilder.core import BotState, ConversationState, MemoryStorage, Storage, StoreItem, TurnContext, UserState
77
from botbuilder.core.adapters import TestAdapter
8-
from botbuilder.schema import Activity
8+
from botbuilder.schema import Activity, ConversationAccount
99

1010
from test_utilities import TestUtilities
1111

@@ -23,6 +23,23 @@ def key_factory(context):
2323
assert context is not None
2424
return STORAGE_KEY
2525

26+
class BotStateForTest(BotState):
27+
def __init__(self, storage: Storage):
28+
super().__init__(storage, f"BotState:BotState")
29+
30+
def get_storage_key(self, turn_context: TurnContext) -> str:
31+
return f"botstate/{turn_context.activity.channel_id}/{turn_context.activity.conversation.id}/BotState"
32+
33+
34+
class CustomState(StoreItem):
35+
def __init__(self, custom_string: str = None, e_tag: str = '*'):
36+
super().__init__(custom_string=custom_string, e_tag=e_tag)
37+
38+
39+
class TestPocoState:
40+
def __init__(self, value=None):
41+
self.value = value
42+
2643

2744
class TestBotState(aiounittest.AsyncTestCase):
2845
storage = MemoryStorage()
@@ -334,4 +351,134 @@ async def test_LoadSaveDelete(self):
334351
obj2 = dictionary["EmptyContext/users/empty@empty.context.org"]
335352
self.assertEqual("hello-2", obj2["property-a"])
336353
with self.assertRaises(KeyError) as _:
337-
obj2["property-b"]
354+
obj2["property-b"]
355+
356+
async def test_state_use_bot_state_directly(self):
357+
async def exec_test(context: TurnContext):
358+
bot_state_manager = BotStateForTest(MemoryStorage())
359+
test_property = bot_state_manager.create_property("test")
360+
361+
# read initial state object
362+
await bot_state_manager.load(context)
363+
364+
custom_state = await test_property.get(context, lambda: CustomState())
365+
366+
# this should be a 'CustomState' as nothing is currently stored in storage
367+
assert isinstance(custom_state, CustomState)
368+
369+
# amend property and write to storage
370+
custom_state.custom_string = "test"
371+
await bot_state_manager.save_changes(context)
372+
373+
custom_state.custom_string = "asdfsadf"
374+
375+
# read into context again
376+
await bot_state_manager.load(context, True)
377+
378+
custom_state = await test_property.get(context)
379+
380+
# check object read from value has the correct value for custom_string
381+
assert custom_state.custom_string == "test"
382+
383+
adapter = TestAdapter(exec_test)
384+
await adapter.send('start')
385+
386+
async def test_user_state_bad_from_throws(self):
387+
dictionary = {}
388+
user_state = UserState(MemoryStorage(dictionary))
389+
context = TestUtilities.create_empty_context()
390+
context.activity.from_property = None
391+
test_property = user_state.create_property("test")
392+
with self.assertRaises(AttributeError):
393+
await test_property.get(context)
394+
395+
async def test_conversation_state_bad_converation_throws(self):
396+
dictionary = {}
397+
user_state = ConversationState(MemoryStorage(dictionary))
398+
context = TestUtilities.create_empty_context()
399+
context.activity.conversation = None
400+
test_property = user_state.create_property("test")
401+
with self.assertRaises(AttributeError):
402+
await test_property.get(context)
403+
404+
async def test_clear_and_save(self):
405+
turn_context = TestUtilities.create_empty_context()
406+
turn_context.activity.conversation = ConversationAccount(id="1234")
407+
408+
storage = MemoryStorage({})
409+
410+
# Turn 0
411+
bot_state1 = ConversationState(storage)
412+
(await bot_state1
413+
.create_property("test-name")
414+
.get(turn_context, lambda: TestPocoState())).value = "test-value"
415+
await bot_state1.save_changes(turn_context)
416+
417+
# Turn 1
418+
bot_state2 = ConversationState(storage)
419+
value1 = (await bot_state2
420+
.create_property("test-name")
421+
.get(turn_context, lambda: TestPocoState(value="default-value"))).value
422+
423+
assert "test-value" == value1
424+
425+
# Turn 2
426+
bot_state3 = ConversationState(storage)
427+
await bot_state3.clear_state(turn_context)
428+
await bot_state3.save_changes(turn_context)
429+
430+
# Turn 3
431+
bot_state4 = ConversationState(storage)
432+
value2 = (await bot_state4
433+
.create_property("test-name")
434+
.get(turn_context, lambda: TestPocoState(value="default-value"))).value
435+
436+
assert "default-value", value2
437+
438+
async def test_bot_state_delete(self):
439+
turn_context = TestUtilities.create_empty_context()
440+
turn_context.activity.conversation = ConversationAccount(id="1234")
441+
442+
storage = MemoryStorage({})
443+
444+
# Turn 0
445+
bot_state1 = ConversationState(storage)
446+
(await bot_state1
447+
.create_property("test-name")
448+
.get(turn_context, lambda: TestPocoState())).value = "test-value"
449+
await bot_state1.save_changes(turn_context)
450+
451+
# Turn 1
452+
bot_state2 = ConversationState(storage)
453+
value1 = (await bot_state2
454+
.create_property("test-name")
455+
.get(turn_context, lambda: TestPocoState(value="default-value"))).value
456+
457+
assert "test-value" == value1
458+
459+
# Turn 2
460+
bot_state3 = ConversationState(storage)
461+
await bot_state3.delete(turn_context)
462+
463+
# Turn 3
464+
bot_state4 = ConversationState(storage)
465+
value2 = (await bot_state4
466+
.create_property("test-name")
467+
.get(turn_context, lambda: TestPocoState(value="default-value"))).value
468+
469+
assert "default-value" == value2
470+
471+
async def test_bot_state_get(self):
472+
turn_context = TestUtilities.create_empty_context()
473+
turn_context.activity.conversation = ConversationAccount(id="1234")
474+
475+
storage = MemoryStorage({})
476+
477+
conversation_state = ConversationState(storage)
478+
(await conversation_state
479+
.create_property("test-name")
480+
.get(turn_context, lambda: TestPocoState())).value = "test-value"
481+
482+
result = conversation_state.get(turn_context)
483+
484+
assert "test-value" == result["test-name"].value

0 commit comments

Comments
 (0)