Skip to content

Commit

Permalink
Merge pull request #145 from botstory/feature/break-loop-on-unmatched…
Browse files Browse the repository at this point in the history
…-message

Feature/break loop on unmatched message
  • Loading branch information
hyzhak committed Feb 28, 2017
2 parents 9fa9ad0 + 3c3c1df commit e0ca350
Show file tree
Hide file tree
Showing 9 changed files with 354 additions and 43 deletions.
2 changes: 1 addition & 1 deletion botstory/ast/forking.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def validate(self, message):
for case_id, validator in self.cases.items():
if validator.validate(message):
return case_id
return False
return None

def serialize(self):
return [{
Expand Down
8 changes: 6 additions & 2 deletions botstory/ast/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,16 @@ def get_story_by_topic(self, topic, stack=None):
# so we're trying to find root of last story and than
# get right sub story

# TODO: should it be:
# parent = self.get_story_by_topic(stack[-1]['topic'], stack[:-1])
parent = self.get_story_by_topic(stack[-1]['topic'], stack[:-1])
if not parent:
return None

# is topic name matching storyline?
try:
return next(filter(lambda part: getattr(part, 'topic', None) == topic, parent.story_line))
except StopIteration:
pass

if hasattr(parent, 'local_scope'):
# for loop.StoriesLoopNode
return parent.get_child_by_validation_result(topic)
Expand Down
34 changes: 16 additions & 18 deletions botstory/ast/loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,19 @@


# 1. once we trap on StoriesLoopNode we should stop execution
# and wait any user import
# and wait any user input
# 2. once we got any user import we should come to StoriesLoopNode
# add check whether input match received request.
# 3. execute matched story


class BreakLoop:
"""
break outsize of story loop
"""
type = 'BreakLoop'


class StoryLoopAPI:
"""
loop (scope) concept similar to switch (forking)
Expand All @@ -31,40 +38,31 @@ def fn(one_loop):
scope_node = StoriesLoopNode(one_loop)
self.parser_instance.add_to_current_node(scope_node)
self.parser_instance.compile_scope(scope_node, one_loop)
# TODO: crawl scope for matchers and handlers

# 1) we already have hierarchy of stories and stack of execution
# it works that way -- we trying to match request to some story validator
# by getting story one-by-one from stack

# 2) we cold add validator for catching all stories from one scope

# 3) if we didn't match scope we bubble up to previous scope

# 4) if we match scope-validator we should choose one of its story

return one_loop

return fn


class StoriesLoopNode:
def __init__(self, target):
self.target = target
self.local_scope = ast.library.StoriesScope()
self.story_line = []
self.target = target
self.topic = target.__name__

@property
def __name__(self):
return self.target.__name__

def by_topic(self, topic):
stories = self.local_scope.by_topic(topic)
return stories[0] if len(stories) > 0 else None

@property
def children(self):
return self.local_scope.stories

def should_loop(self):
return True

def __call__(self, *args, **kwargs):
def children_matcher(self):
return ScopeMatcher(forking.Switch(self.local_scope.all_filters()))

def get_child_by_validation_result(self, topic):
Expand Down
209 changes: 207 additions & 2 deletions botstory/ast/loop_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from botstory.ast import callable
from botstory.ast import loop
from botstory.utils import answer, SimpleTrigger


Expand Down Expand Up @@ -206,7 +206,7 @@ def show_local_help(ctx):
def job_story():
@story.part()
def do_some_job(ctx):
return callable.EndOfStory()
return loop.BreakLoop()

await talk.pure_text('start job')
await talk.pure_text('?')
Expand All @@ -215,3 +215,208 @@ def do_some_job(ctx):

assert trigger_show_local_help.value == 1
assert trigger_show_global_help.is_triggered is True


@pytest.mark.asyncio
async def test_breaking_the_loop_inside_of_other_loop():
trigger_global = SimpleTrigger()
trigger_inner = SimpleTrigger()
trigger_outer = SimpleTrigger()

with answer.Talk() as talk:
story = talk.story

@story.on('action1')
def global_action1():
@story.part()
def trigger_action1(ctx):
pass

@story.on('action2') # 1
def global_action2():
@story.loop()
def outer_loop():
@story.on('action1') # 2
def outer_action1():
@story.loop()
def inner_loop():
@story.on('action1') # 3
def inner_action1():
@story.part()
def inner_action1_part(ctx):
return loop.BreakLoop()

@story.on('action2')
def inner_action2():
@story.part()
def inner_action2_part(ctx):
pass

@story.on('action3') # 4 (wrong)
def inner_action3():
@story.part()
def inner_action3_part(ctx):
trigger_inner.passed()

@story.on('action2')
def outer_action2():
@story.part()
def do_some_job(ctx):
pass

@story.on('action3') # 4 (correct)
def outer_action3():
@story.part()
def do_some_job(ctx):
trigger_outer.passed()

@story.on('action3') # 4 (wrong)
def global_action3():
@story.part()
def do_some_job(ctx):
trigger_global.passed()

await talk.pure_text('action2')
await talk.pure_text('action1')
await talk.pure_text('action1')
await talk.pure_text('action3')

assert trigger_global.is_triggered is False
assert trigger_inner.is_triggered is False
assert trigger_outer.is_triggered is True


@pytest.mark.asyncio
async def test_prevent_message_propagation_from_outer_loop_to_an_inner_loop():
trigger_1 = SimpleTrigger(0)
trigger_2 = SimpleTrigger(0)
trigger_3 = SimpleTrigger(0)

with answer.Talk() as talk:
story = talk.story

@story.on('action1') # 1
def global_action1():
@story.part()
def pre_1(ctx):
trigger_1.inc()

@story.loop()
def outer_loop():
@story.on('action1') # 2, 4
def outer_action1():
@story.part()
def pre_2(ctx):
trigger_2.inc()

@story.loop()
def inner_loop():
@story.on('action1') # 3
def inner_action1():
@story.part()
def inner_action1_part(ctx):
trigger_3.inc()
return loop.BreakLoop()

@story.part()
def pre_3(ctx):
pass

await talk.pure_text('action1')
await talk.pure_text('action1')
await talk.pure_text('action1')
await talk.pure_text('action1')

assert trigger_1.value == 1
assert trigger_2.value == 2
assert trigger_3.value == 1


@pytest.mark.asyncio
async def test_break_loop_on_unmatched_message():
action1_trigger = SimpleTrigger(0)
action2_after_part_trigger = SimpleTrigger(0)

with answer.Talk() as talk:
story = talk.story

@story.on('action1')
def action1():
@story.part()
def action1_part(ctx):
pass

@story.on('action2')
def action2():
@story.part()
def action2_part(ctx):
pass

@story.loop()
def action2_loop():
@story.on('action1')
def action2_loop_action1():
@story.part()
def action2_loop_action1_part(ctx):
action1_trigger.inc()

@story.on('action2')
def action2_loop_action2():
@story.part()
def action2_loop_action2_part(ctx):
pass

@story.part()
def action2_after_part(ctx):
action2_after_part_trigger.inc()

await talk.pure_text('action2')
await talk.pure_text('action1')
await talk.pure_text('action1')
await talk.pure_text('action3')

assert action1_trigger.value == 2
assert action2_after_part_trigger.value == 1


@pytest.mark.asyncio
async def test_break_loop_on_unmatched_message_and_jump_to_another_story():
action1_trigger = SimpleTrigger(0)
action3_trigger = SimpleTrigger(0)

with answer.Talk() as talk:
story = talk.story

@story.on('action2')
def action2():
@story.part()
def action2_part(ctx):
pass

@story.loop()
def action2_loop():
@story.on('action1')
def action2_loop_action1():
@story.part()
def action2_loop_action1_part(ctx):
action1_trigger.inc()

@story.on('action2')
def action2_loop_action2():
@story.part()
def action2_loop_action2_part(ctx):
pass

@story.on('action3')
def action3():
@story.part()
def action3_part(ctx):
action3_trigger.inc()

await talk.pure_text('action2')
await talk.pure_text('action1')
await talk.pure_text('action1')
await talk.pure_text('action3')

assert action1_trigger.value == 2
assert action3_trigger.value == 1
Loading

0 comments on commit e0ca350

Please sign in to comment.