From f11e1407197745753168b38300cb75b0420b1561 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 26 Mar 2022 18:07:19 +0100 Subject: [PATCH 01/25] dummy-commit to get the PR back online --- tests/test_conversationhandler.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/test_conversationhandler.py diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py new file mode 100644 index 00000000000..950bfdc6a22 --- /dev/null +++ b/tests/test_conversationhandler.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. From 54408aeafd4ea8975d72572b752e0b65b81415d9 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 26 Mar 2022 19:48:20 +0100 Subject: [PATCH 02/25] up coverage a bit --- tests/test_basepersistence.py | 12 +++++++++++- tests/test_inlinequeryhandler.py | 7 +++++++ tests/test_messagehandler.py | 1 + tests/test_stringregexhandler.py | 9 +++++++-- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/tests/test_basepersistence.py b/tests/test_basepersistence.py index 0a7cab203a5..ec9d198c07e 100644 --- a/tests/test_basepersistence.py +++ b/tests/test_basepersistence.py @@ -29,7 +29,7 @@ import pytest from flaky import flaky -from telegram import User, Chat, InlineKeyboardMarkup, InlineKeyboardButton +from telegram import User, Chat, InlineKeyboardMarkup, InlineKeyboardButton, Bot from telegram.ext import ( ApplicationBuilder, PersistenceInput, @@ -388,6 +388,16 @@ def test_abstract_methods(self): ): BasePersistence() + @default_papp + def test_update_interval_immutable(self, papp): + with pytest.raises(AttributeError, match='can not assign a new value to update_interval'): + papp.persistence.update_interval = 7 + + @default_papp + def test_set_bot_error(self, papp): + with pytest.raises(TypeError, match='when using telegram.ext.ExtBot'): + papp.persistence.set_bot(Bot(papp.bot.token)) + def test_construction_with_bad_persistence(self, caplog, bot): class MyPersistence: def __init__(self): diff --git a/tests/test_inlinequeryhandler.py b/tests/test_inlinequeryhandler.py index 995fc09086c..ccf32276760 100644 --- a/tests/test_inlinequeryhandler.py +++ b/tests/test_inlinequeryhandler.py @@ -142,6 +142,13 @@ async def test_context_pattern(self, app, inline_query): await app.process_update(inline_query) assert self.test_flag + update = Update( + update_id=0, inline_query=InlineQuery(id='id', from_user=None, query='', offset='') + ) + assert not handler.check_update(update) + update.inline_query.query = 'not_a_match' + assert not handler.check_update(update) + @pytest.mark.parametrize('chat_types', [[Chat.SENDER], [Chat.SENDER, Chat.SUPERGROUP], []]) @pytest.mark.parametrize( 'chat_type,result', [(Chat.SENDER, True), (Chat.CHANNEL, False), (None, False)] diff --git a/tests/test_messagehandler.py b/tests/test_messagehandler.py index a727a0905f5..5251b1f2e95 100644 --- a/tests/test_messagehandler.py +++ b/tests/test_messagehandler.py @@ -161,6 +161,7 @@ def test_specific_filters(self, message): def test_other_update_types(self, false_update): handler = MessageHandler(None, self.callback) assert not handler.check_update(false_update) + assert not handler.check_update('string') def test_filters_returns_empty_dict(self): class DataFilter(MessageFilter): diff --git a/tests/test_stringregexhandler.py b/tests/test_stringregexhandler.py index b7db2ec5bbe..56adc5e7fc1 100644 --- a/tests/test_stringregexhandler.py +++ b/tests/test_stringregexhandler.py @@ -19,6 +19,7 @@ import asyncio import pytest +import re from telegram import ( Bot, @@ -97,8 +98,12 @@ async def callback_pattern(self, update, context): self.test_flag = context.matches[0].groupdict() == {'begin': 't', 'end': ' message'} @pytest.mark.asyncio - async def test_basic(self, app): - handler = StringRegexHandler('(?P.*)est(?P.*)', self.callback) + @pytest.mark.parametrize('compile', (True, False)) + async def test_basic(self, app, compile): + pattern = '(?P.*)est(?P.*)' + if compile: + pattern = re.compile('(?P.*)est(?P.*)') + handler = StringRegexHandler(pattern, self.callback) app.add_handler(handler) assert handler.check_update('test message') From c8bedc3684bba840e71fa1828d9952bb40bcff63 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 26 Mar 2022 21:29:37 +0100 Subject: [PATCH 03/25] Test{Pickle, Dict}Persistence --- tests/test_dictpersistence.py | 349 +++++++++++ tests/test_picklepersistence.py | 990 ++++++++++++++++++++++++++++++++ 2 files changed, 1339 insertions(+) create mode 100644 tests/test_dictpersistence.py create mode 100644 tests/test_picklepersistence.py diff --git a/tests/test_dictpersistence.py b/tests/test_dictpersistence.py new file mode 100644 index 00000000000..9f0ac9a8929 --- /dev/null +++ b/tests/test_dictpersistence.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +import os +from pathlib import Path + +import pytest + + +try: + import ujson as json +except ImportError: + import json + +from telegram.ext import DictPersistence + + +@pytest.fixture(autouse=True) +def change_directory(tmp_path: Path): + orig_dir = Path.cwd() + # Switch to a temporary directory, so we don't have to worry about cleaning up files + os.chdir(tmp_path) + yield + # Go back to original directory + os.chdir(orig_dir) + + +@pytest.fixture(autouse=True) +def reset_callback_data_cache(bot): + yield + bot.callback_data_cache.clear_callback_data() + bot.callback_data_cache.clear_callback_queries() + bot.arbitrary_callback_data = False + + +@pytest.fixture(scope="function") +def bot_data(): + return {'test1': 'test2', 'test3': {'test4': 'test5'}} + + +@pytest.fixture(scope="function") +def chat_data(): + return {-12345: {'test1': 'test2', 'test3': {'test4': 'test5'}}, -67890: {3: 'test4'}} + + +@pytest.fixture(scope="function") +def user_data(): + return {12345: {'test1': 'test2', 'test3': {'test4': 'test5'}}, 67890: {3: 'test4'}} + + +@pytest.fixture(scope="function") +def callback_data(): + return [('test1', 1000, {'button1': 'test0', 'button2': 'test1'})], {'test1': 'test2'} + + +@pytest.fixture(scope='function') +def conversations(): + return { + 'name1': {(123, 123): 3, (456, 654): 4}, + 'name2': {(123, 321): 1, (890, 890): 2}, + 'name3': {(123, 321): 1, (890, 890): 2}, + } + + +@pytest.fixture(scope='function') +def user_data_json(user_data): + return json.dumps(user_data) + + +@pytest.fixture(scope='function') +def chat_data_json(chat_data): + return json.dumps(chat_data) + + +@pytest.fixture(scope='function') +def bot_data_json(bot_data): + return json.dumps(bot_data) + + +@pytest.fixture(scope='function') +def callback_data_json(callback_data): + return json.dumps(callback_data) + + +@pytest.fixture(scope='function') +def conversations_json(conversations): + return """{"name1": {"[123, 123]": 3, "[456, 654]": 4}, "name2": + {"[123, 321]": 1, "[890, 890]": 2}, "name3": + {"[123, 321]": 1, "[890, 890]": 2}}""" + + +class TestDictPersistence: + """Just tests the DictPersistence interface. Integration of persistence into Applictation + is tested in TestBasePersistence!""" + + @pytest.mark.asyncio + async def test_slot_behaviour(self, mro_slots, recwarn): + inst = DictPersistence() + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + @pytest.mark.asyncio + async def test_no_json_given(self): + dict_persistence = DictPersistence() + assert await dict_persistence.get_user_data() == {} + assert await dict_persistence.get_chat_data() == {} + assert await dict_persistence.get_bot_data() == {} + assert await dict_persistence.get_callback_data() is None + assert await dict_persistence.get_conversations('noname') == {} + + @pytest.mark.asyncio + async def test_bad_json_string_given(self): + bad_user_data = 'thisisnojson99900()))(' + bad_chat_data = 'thisisnojson99900()))(' + bad_bot_data = 'thisisnojson99900()))(' + bad_callback_data = 'thisisnojson99900()))(' + bad_conversations = 'thisisnojson99900()))(' + with pytest.raises(TypeError, match='user_data'): + DictPersistence(user_data_json=bad_user_data) + with pytest.raises(TypeError, match='chat_data'): + DictPersistence(chat_data_json=bad_chat_data) + with pytest.raises(TypeError, match='bot_data'): + DictPersistence(bot_data_json=bad_bot_data) + with pytest.raises(TypeError, match='callback_data'): + DictPersistence(callback_data_json=bad_callback_data) + with pytest.raises(TypeError, match='conversations'): + DictPersistence(conversations_json=bad_conversations) + + @pytest.mark.asyncio + async def test_invalid_json_string_given(self): + bad_user_data = '["this", "is", "json"]' + bad_chat_data = '["this", "is", "json"]' + bad_bot_data = '["this", "is", "json"]' + bad_conversations = '["this", "is", "json"]' + bad_callback_data_1 = '[[["str", 3.14, {"di": "ct"}]], "is"]' + bad_callback_data_2 = '[[["str", "non-float", {"di": "ct"}]], {"di": "ct"}]' + bad_callback_data_3 = '[[[{"not": "a str"}, 3.14, {"di": "ct"}]], {"di": "ct"}]' + bad_callback_data_4 = '[[["wrong", "length"]], {"di": "ct"}]' + bad_callback_data_5 = '["this", "is", "json"]' + with pytest.raises(TypeError, match='user_data'): + DictPersistence(user_data_json=bad_user_data) + with pytest.raises(TypeError, match='chat_data'): + DictPersistence(chat_data_json=bad_chat_data) + with pytest.raises(TypeError, match='bot_data'): + DictPersistence(bot_data_json=bad_bot_data) + for bad_callback_data in [ + bad_callback_data_1, + bad_callback_data_2, + bad_callback_data_3, + bad_callback_data_4, + bad_callback_data_5, + ]: + with pytest.raises(TypeError, match='callback_data'): + DictPersistence(callback_data_json=bad_callback_data) + with pytest.raises(TypeError, match='conversations'): + DictPersistence(conversations_json=bad_conversations) + + @pytest.mark.asyncio + async def test_good_json_input( + self, user_data_json, chat_data_json, bot_data_json, conversations_json, callback_data_json + ): + dict_persistence = DictPersistence( + user_data_json=user_data_json, + chat_data_json=chat_data_json, + bot_data_json=bot_data_json, + conversations_json=conversations_json, + callback_data_json=callback_data_json, + ) + user_data = await dict_persistence.get_user_data() + assert isinstance(user_data, dict) + assert user_data[12345]['test1'] == 'test2' + assert user_data[67890][3] == 'test4' + + chat_data = await dict_persistence.get_chat_data() + assert isinstance(chat_data, dict) + assert chat_data[-12345]['test1'] == 'test2' + assert chat_data[-67890][3] == 'test4' + + bot_data = await dict_persistence.get_bot_data() + assert isinstance(bot_data, dict) + assert bot_data['test1'] == 'test2' + assert bot_data['test3']['test4'] == 'test5' + assert 'test6' not in bot_data + + callback_data = await dict_persistence.get_callback_data() + + assert isinstance(callback_data, tuple) + assert callback_data[0] == [('test1', 1000, {'button1': 'test0', 'button2': 'test1'})] + assert callback_data[1] == {'test1': 'test2'} + + conversation1 = await dict_persistence.get_conversations('name1') + assert isinstance(conversation1, dict) + assert conversation1[(123, 123)] == 3 + assert conversation1[(456, 654)] == 4 + with pytest.raises(KeyError): + conversation1[(890, 890)] + conversation2 = await dict_persistence.get_conversations('name2') + assert isinstance(conversation1, dict) + assert conversation2[(123, 321)] == 1 + assert conversation2[(890, 890)] == 2 + with pytest.raises(KeyError): + conversation2[(123, 123)] + + @pytest.mark.asyncio + async def test_good_json_input_callback_data_none(self): + dict_persistence = DictPersistence(callback_data_json='null') + assert dict_persistence.callback_data is None + assert dict_persistence.callback_data_json == 'null' + + @pytest.mark.asyncio + async def test_dict_outputs( + self, + user_data, + user_data_json, + chat_data, + chat_data_json, + bot_data, + bot_data_json, + callback_data_json, + conversations, + conversations_json, + ): + dict_persistence = DictPersistence( + user_data_json=user_data_json, + chat_data_json=chat_data_json, + bot_data_json=bot_data_json, + callback_data_json=callback_data_json, + conversations_json=conversations_json, + ) + assert dict_persistence.user_data == user_data + assert dict_persistence.chat_data == chat_data + assert dict_persistence.bot_data == bot_data + assert dict_persistence.bot_data == bot_data + assert dict_persistence.conversations == conversations + + @pytest.mark.asyncio + async def test_json_outputs( + self, user_data_json, chat_data_json, bot_data_json, callback_data_json, conversations_json + ): + dict_persistence = DictPersistence( + user_data_json=user_data_json, + chat_data_json=chat_data_json, + bot_data_json=bot_data_json, + callback_data_json=callback_data_json, + conversations_json=conversations_json, + ) + assert dict_persistence.user_data_json == user_data_json + assert dict_persistence.chat_data_json == chat_data_json + assert dict_persistence.callback_data_json == callback_data_json + assert dict_persistence.conversations_json == conversations_json + + @pytest.mark.asyncio + async def test_updating( + self, + user_data_json, + chat_data_json, + bot_data_json, + callback_data, + callback_data_json, + conversations, + conversations_json, + ): + dict_persistence = DictPersistence( + user_data_json=user_data_json, + chat_data_json=chat_data_json, + bot_data_json=bot_data_json, + callback_data_json=callback_data_json, + conversations_json=conversations_json, + ) + + user_data = await dict_persistence.get_user_data() + user_data[12345]['test3']['test4'] = 'test6' + assert dict_persistence.user_data != user_data + assert dict_persistence.user_data_json != json.dumps(user_data) + await dict_persistence.update_user_data(12345, user_data[12345]) + assert dict_persistence.user_data == user_data + assert dict_persistence.user_data_json == json.dumps(user_data) + await dict_persistence.drop_user_data(67890) + assert 67890 not in dict_persistence.user_data + dict_persistence._user_data = None + await dict_persistence.drop_user_data(123) + assert isinstance(await dict_persistence.get_user_data(), dict) + + chat_data = await dict_persistence.get_chat_data() + chat_data[-12345]['test3']['test4'] = 'test6' + assert dict_persistence.chat_data != chat_data + assert dict_persistence.chat_data_json != json.dumps(chat_data) + await dict_persistence.update_chat_data(-12345, chat_data[-12345]) + assert dict_persistence.chat_data == chat_data + assert dict_persistence.chat_data_json == json.dumps(chat_data) + await dict_persistence.drop_chat_data(-67890) + assert -67890 not in dict_persistence.chat_data + dict_persistence._chat_data = None + await dict_persistence.drop_chat_data(123) + assert isinstance(await dict_persistence.get_chat_data(), dict) + + bot_data = await dict_persistence.get_bot_data() + bot_data['test3']['test4'] = 'test6' + assert dict_persistence.bot_data != bot_data + assert dict_persistence.bot_data_json != json.dumps(bot_data) + await dict_persistence.update_bot_data(bot_data) + assert dict_persistence.bot_data == bot_data + assert dict_persistence.bot_data_json == json.dumps(bot_data) + + callback_data = await dict_persistence.get_callback_data() + callback_data[1]['test3'] = 'test4' + callback_data[0][0][2]['button2'] = 'test41' + assert dict_persistence.callback_data != callback_data + assert dict_persistence.callback_data_json != json.dumps(callback_data) + await dict_persistence.update_callback_data(callback_data) + assert dict_persistence.callback_data == callback_data + assert dict_persistence.callback_data_json == json.dumps(callback_data) + + conversation1 = await dict_persistence.get_conversations('name1') + conversation1[(123, 123)] = 5 + assert not dict_persistence.conversations['name1'] == conversation1 + await dict_persistence.update_conversation('name1', (123, 123), 5) + assert dict_persistence.conversations['name1'] == conversation1 + conversations['name1'][(123, 123)] = 5 + assert ( + dict_persistence.conversations_json + == DictPersistence._encode_conversations_to_json(conversations) + ) + assert await dict_persistence.get_conversations('name1') == conversation1 + + dict_persistence._conversations = None + await dict_persistence.update_conversation('name1', (123, 123), 5) + assert dict_persistence.conversations['name1'] == {(123, 123): 5} + assert await dict_persistence.get_conversations('name1') == {(123, 123): 5} + assert ( + dict_persistence.conversations_json + == DictPersistence._encode_conversations_to_json({"name1": {(123, 123): 5}}) + ) diff --git a/tests/test_picklepersistence.py b/tests/test_picklepersistence.py new file mode 100644 index 00000000000..dfb2e18611e --- /dev/null +++ b/tests/test_picklepersistence.py @@ -0,0 +1,990 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2022 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +import datetime +import os +import pickle +import gzip +from pathlib import Path + +import pytest + +from telegram.warnings import PTBUserWarning + +from telegram import Update, Message, User, Chat, Bot, TelegramObject +from telegram.ext import ( + PicklePersistence, + ContextTypes, + PersistenceInput, +) + + +@pytest.fixture(autouse=True) +def change_directory(tmp_path: Path): + orig_dir = Path.cwd() + # Switch to a temporary directory, so we don't have to worry about cleaning up files + os.chdir(tmp_path) + yield + # Go back to original directory + os.chdir(orig_dir) + + +@pytest.fixture(autouse=True) +def reset_callback_data_cache(bot): + yield + bot.callback_data_cache.clear_callback_data() + bot.callback_data_cache.clear_callback_queries() + bot.arbitrary_callback_data = False + + +@pytest.fixture(scope="function") +def bot_data(): + return {'test1': 'test2', 'test3': {'test4': 'test5'}} + + +@pytest.fixture(scope="function") +def chat_data(): + return {-12345: {'test1': 'test2', 'test3': {'test4': 'test5'}}, -67890: {3: 'test4'}} + + +@pytest.fixture(scope="function") +def user_data(): + return {12345: {'test1': 'test2', 'test3': {'test4': 'test5'}}, 67890: {3: 'test4'}} + + +@pytest.fixture(scope="function") +def callback_data(): + return [('test1', 1000, {'button1': 'test0', 'button2': 'test1'})], {'test1': 'test2'} + + +@pytest.fixture(scope='function') +def conversations(): + return { + 'name1': {(123, 123): 3, (456, 654): 4}, + 'name2': {(123, 321): 1, (890, 890): 2}, + 'name3': {(123, 321): 1, (890, 890): 2}, + } + + +@pytest.fixture(scope='function') +def pickle_persistence(): + return PicklePersistence( + filepath='pickletest', + single_file=False, + on_flush=False, + ) + + +@pytest.fixture(scope='function') +def pickle_persistence_only_bot(): + return PicklePersistence( + filepath='pickletest', + store_data=PersistenceInput(callback_data=False, user_data=False, chat_data=False), + single_file=False, + on_flush=False, + ) + + +@pytest.fixture(scope='function') +def pickle_persistence_only_chat(): + return PicklePersistence( + filepath='pickletest', + store_data=PersistenceInput(callback_data=False, user_data=False, bot_data=False), + single_file=False, + on_flush=False, + ) + + +@pytest.fixture(scope='function') +def pickle_persistence_only_user(): + return PicklePersistence( + filepath='pickletest', + store_data=PersistenceInput(callback_data=False, chat_data=False, bot_data=False), + single_file=False, + on_flush=False, + ) + + +@pytest.fixture(scope='function') +def pickle_persistence_only_callback(): + return PicklePersistence( + filepath='pickletest', + store_data=PersistenceInput(user_data=False, chat_data=False, bot_data=False), + single_file=False, + on_flush=False, + ) + + +@pytest.fixture(scope='function') +def bad_pickle_files(): + for name in [ + 'pickletest_user_data', + 'pickletest_chat_data', + 'pickletest_bot_data', + 'pickletest_callback_data', + 'pickletest_conversations', + 'pickletest', + ]: + Path(name).write_text('(())') + yield True + + +@pytest.fixture(scope='function') +def invalid_pickle_files(): + for name in [ + 'pickletest_user_data', + 'pickletest_chat_data', + 'pickletest_bot_data', + 'pickletest_callback_data', + 'pickletest_conversations', + 'pickletest', + ]: + # Just a random way to trigger pickle.UnpicklingError + # see https://stackoverflow.com/a/44422239/10606962 + with gzip.open(name, 'wb') as file: + pickle.dump([1, 2, 3], file) + yield True + + +@pytest.fixture(scope='function') +def good_pickle_files(user_data, chat_data, bot_data, callback_data, conversations): + data = { + 'user_data': user_data, + 'chat_data': chat_data, + 'bot_data': bot_data, + 'callback_data': callback_data, + 'conversations': conversations, + } + with Path('pickletest_user_data').open('wb') as f: + pickle.dump(user_data, f) + with Path('pickletest_chat_data').open('wb') as f: + pickle.dump(chat_data, f) + with Path('pickletest_bot_data').open('wb') as f: + pickle.dump(bot_data, f) + with Path('pickletest_callback_data').open('wb') as f: + pickle.dump(callback_data, f) + with Path('pickletest_conversations').open('wb') as f: + pickle.dump(conversations, f) + with Path('pickletest').open('wb') as f: + pickle.dump(data, f) + yield True + + +@pytest.fixture(scope='function') +def pickle_files_wo_bot_data(user_data, chat_data, callback_data, conversations): + data = { + 'user_data': user_data, + 'chat_data': chat_data, + 'conversations': conversations, + 'callback_data': callback_data, + } + with Path('pickletest_user_data').open('wb') as f: + pickle.dump(user_data, f) + with Path('pickletest_chat_data').open('wb') as f: + pickle.dump(chat_data, f) + with Path('pickletest_callback_data').open('wb') as f: + pickle.dump(callback_data, f) + with Path('pickletest_conversations').open('wb') as f: + pickle.dump(conversations, f) + with Path('pickletest').open('wb') as f: + pickle.dump(data, f) + yield True + + +@pytest.fixture(scope='function') +def pickle_files_wo_callback_data(user_data, chat_data, bot_data, conversations): + data = { + 'user_data': user_data, + 'chat_data': chat_data, + 'bot_data': bot_data, + 'conversations': conversations, + } + with Path('pickletest_user_data').open('wb') as f: + pickle.dump(user_data, f) + with Path('pickletest_chat_data').open('wb') as f: + pickle.dump(chat_data, f) + with Path('pickletest_bot_data').open('wb') as f: + pickle.dump(bot_data, f) + with Path('pickletest_conversations').open('wb') as f: + pickle.dump(conversations, f) + with Path('pickletest').open('wb') as f: + pickle.dump(data, f) + yield True + + +@pytest.fixture(scope='function') +def update(bot): + user = User(id=321, first_name='test_user', is_bot=False) + chat = Chat(id=123, type='group') + message = Message(1, datetime.datetime.now(), chat, from_user=user, text="Hi there", bot=bot) + return Update(0, message=message) + + +class TestPicklePersistence: + """Just tests the PicklePersistence interface. Integration of persistence into Applictation + is tested in TestBasePersistence!""" + + class DictSub(TelegramObject): # Used for testing our custom (Un)Pickler. + def __init__(self, private, normal, b): + self._private = private + self.normal = normal + self._bot = b + + class SlotsSub(TelegramObject): + __slots__ = ('new_var', '_private') + + def __init__(self, new_var, private): + self.new_var = new_var + self._private = private + + class NormalClass: + def __init__(self, my_var): + self.my_var = my_var + + @pytest.mark.asyncio + async def test_slot_behaviour(self, mro_slots, pickle_persistence): + inst = pickle_persistence + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + @pytest.mark.asyncio + @pytest.mark.parametrize('on_flush', (True, False)) + async def test_on_flush(self, pickle_persistence, on_flush): + pickle_persistence.on_flush = on_flush + pickle_persistence.single_file = True + file_path = Path(pickle_persistence.filepath) + + await pickle_persistence.update_callback_data('somedata') + assert file_path.is_file() != on_flush + + await pickle_persistence.update_bot_data('data') + assert file_path.is_file() != on_flush + + await pickle_persistence.update_user_data(123, 'data') + assert file_path.is_file() != on_flush + + await pickle_persistence.update_chat_data(123, 'data') + assert file_path.is_file() != on_flush + + await pickle_persistence.update_conversation('name', (1, 1), 'new_state') + assert file_path.is_file() != on_flush + + await pickle_persistence.flush() + assert file_path.is_file() + + @pytest.mark.asyncio + async def test_pickle_behaviour_with_slots(self, pickle_persistence): + bot_data = await pickle_persistence.get_bot_data() + bot_data['message'] = Message(3, datetime.datetime.now(), Chat(2, type='supergroup')) + await pickle_persistence.update_bot_data(bot_data) + retrieved = await pickle_persistence.get_bot_data() + assert retrieved == bot_data + + @pytest.mark.asyncio + async def test_no_files_present_multi_file(self, pickle_persistence): + assert await pickle_persistence.get_user_data() == {} + assert await pickle_persistence.get_chat_data() == {} + assert await pickle_persistence.get_bot_data() == {} + assert await pickle_persistence.get_callback_data() is None + assert await pickle_persistence.get_conversations('noname') == {} + + @pytest.mark.asyncio + async def test_no_files_present_single_file(self, pickle_persistence): + pickle_persistence.single_file = True + assert await pickle_persistence.get_user_data() == {} + assert await pickle_persistence.get_chat_data() == {} + assert await pickle_persistence.get_bot_data() == {} + assert await pickle_persistence.get_callback_data() is None + assert await pickle_persistence.get_conversations('noname') == {} + + @pytest.mark.asyncio + async def test_with_bad_multi_file(self, pickle_persistence, bad_pickle_files): + with pytest.raises(TypeError, match='pickletest_user_data'): + await pickle_persistence.get_user_data() + with pytest.raises(TypeError, match='pickletest_chat_data'): + await pickle_persistence.get_chat_data() + with pytest.raises(TypeError, match='pickletest_bot_data'): + await pickle_persistence.get_bot_data() + with pytest.raises(TypeError, match='pickletest_callback_data'): + await pickle_persistence.get_callback_data() + with pytest.raises(TypeError, match='pickletest_conversations'): + await pickle_persistence.get_conversations('name') + + @pytest.mark.asyncio + async def test_with_invalid_multi_file(self, pickle_persistence, invalid_pickle_files): + with pytest.raises(TypeError, match='pickletest_user_data does not contain'): + await pickle_persistence.get_user_data() + with pytest.raises(TypeError, match='pickletest_chat_data does not contain'): + await pickle_persistence.get_chat_data() + with pytest.raises(TypeError, match='pickletest_bot_data does not contain'): + await pickle_persistence.get_bot_data() + with pytest.raises(TypeError, match='pickletest_callback_data does not contain'): + await pickle_persistence.get_callback_data() + with pytest.raises(TypeError, match='pickletest_conversations does not contain'): + await pickle_persistence.get_conversations('name') + + @pytest.mark.asyncio + async def test_with_bad_single_file(self, pickle_persistence, bad_pickle_files): + pickle_persistence.single_file = True + with pytest.raises(TypeError, match='pickletest'): + await pickle_persistence.get_user_data() + with pytest.raises(TypeError, match='pickletest'): + await pickle_persistence.get_chat_data() + with pytest.raises(TypeError, match='pickletest'): + await pickle_persistence.get_bot_data() + with pytest.raises(TypeError, match='pickletest'): + await pickle_persistence.get_callback_data() + with pytest.raises(TypeError, match='pickletest'): + await pickle_persistence.get_conversations('name') + + @pytest.mark.asyncio + async def test_with_invalid_single_file(self, pickle_persistence, invalid_pickle_files): + pickle_persistence.single_file = True + with pytest.raises(TypeError, match='pickletest does not contain'): + await pickle_persistence.get_user_data() + with pytest.raises(TypeError, match='pickletest does not contain'): + await pickle_persistence.get_chat_data() + with pytest.raises(TypeError, match='pickletest does not contain'): + await pickle_persistence.get_bot_data() + with pytest.raises(TypeError, match='pickletest does not contain'): + await pickle_persistence.get_callback_data() + with pytest.raises(TypeError, match='pickletest does not contain'): + await pickle_persistence.get_conversations('name') + + @pytest.mark.asyncio + async def test_with_good_multi_file(self, pickle_persistence, good_pickle_files): + user_data = await pickle_persistence.get_user_data() + assert isinstance(user_data, dict) + assert user_data[12345]['test1'] == 'test2' + assert user_data[67890][3] == 'test4' + + chat_data = await pickle_persistence.get_chat_data() + assert isinstance(chat_data, dict) + assert chat_data[-12345]['test1'] == 'test2' + assert chat_data[-67890][3] == 'test4' + + bot_data = await pickle_persistence.get_bot_data() + assert isinstance(bot_data, dict) + assert bot_data['test1'] == 'test2' + assert bot_data['test3']['test4'] == 'test5' + assert 'test0' not in bot_data + + callback_data = await pickle_persistence.get_callback_data() + assert isinstance(callback_data, tuple) + assert callback_data[0] == [('test1', 1000, {'button1': 'test0', 'button2': 'test1'})] + assert callback_data[1] == {'test1': 'test2'} + + conversation1 = await pickle_persistence.get_conversations('name1') + assert isinstance(conversation1, dict) + assert conversation1[(123, 123)] == 3 + assert conversation1[(456, 654)] == 4 + with pytest.raises(KeyError): + conversation1[(890, 890)] + conversation2 = await pickle_persistence.get_conversations('name2') + assert isinstance(conversation1, dict) + assert conversation2[(123, 321)] == 1 + assert conversation2[(890, 890)] == 2 + with pytest.raises(KeyError): + conversation2[(123, 123)] + + @pytest.mark.asyncio + async def test_with_good_single_file(self, pickle_persistence, good_pickle_files): + pickle_persistence.single_file = True + user_data = await pickle_persistence.get_user_data() + assert isinstance(user_data, dict) + assert user_data[12345]['test1'] == 'test2' + assert user_data[67890][3] == 'test4' + + chat_data = await pickle_persistence.get_chat_data() + assert isinstance(chat_data, dict) + assert chat_data[-12345]['test1'] == 'test2' + assert chat_data[-67890][3] == 'test4' + + bot_data = await pickle_persistence.get_bot_data() + assert isinstance(bot_data, dict) + assert bot_data['test1'] == 'test2' + assert bot_data['test3']['test4'] == 'test5' + assert 'test0' not in bot_data + + callback_data = await pickle_persistence.get_callback_data() + assert isinstance(callback_data, tuple) + assert callback_data[0] == [('test1', 1000, {'button1': 'test0', 'button2': 'test1'})] + assert callback_data[1] == {'test1': 'test2'} + + conversation1 = await pickle_persistence.get_conversations('name1') + assert isinstance(conversation1, dict) + assert conversation1[(123, 123)] == 3 + assert conversation1[(456, 654)] == 4 + with pytest.raises(KeyError): + conversation1[(890, 890)] + conversation2 = await pickle_persistence.get_conversations('name2') + assert isinstance(conversation1, dict) + assert conversation2[(123, 321)] == 1 + assert conversation2[(890, 890)] == 2 + with pytest.raises(KeyError): + conversation2[(123, 123)] + + @pytest.mark.asyncio + async def test_with_multi_file_wo_bot_data(self, pickle_persistence, pickle_files_wo_bot_data): + user_data = await pickle_persistence.get_user_data() + assert isinstance(user_data, dict) + assert user_data[12345]['test1'] == 'test2' + assert user_data[67890][3] == 'test4' + + chat_data = await pickle_persistence.get_chat_data() + assert isinstance(chat_data, dict) + assert chat_data[-12345]['test1'] == 'test2' + assert chat_data[-67890][3] == 'test4' + + bot_data = await pickle_persistence.get_bot_data() + assert isinstance(bot_data, dict) + assert not bot_data.keys() + + callback_data = await pickle_persistence.get_callback_data() + assert isinstance(callback_data, tuple) + assert callback_data[0] == [('test1', 1000, {'button1': 'test0', 'button2': 'test1'})] + assert callback_data[1] == {'test1': 'test2'} + + conversation1 = await pickle_persistence.get_conversations('name1') + assert isinstance(conversation1, dict) + assert conversation1[(123, 123)] == 3 + assert conversation1[(456, 654)] == 4 + with pytest.raises(KeyError): + conversation1[(890, 890)] + conversation2 = await pickle_persistence.get_conversations('name2') + assert isinstance(conversation1, dict) + assert conversation2[(123, 321)] == 1 + assert conversation2[(890, 890)] == 2 + with pytest.raises(KeyError): + conversation2[(123, 123)] + + @pytest.mark.asyncio + async def test_with_multi_file_wo_callback_data( + self, pickle_persistence, pickle_files_wo_callback_data + ): + user_data = await pickle_persistence.get_user_data() + assert isinstance(user_data, dict) + assert user_data[12345]['test1'] == 'test2' + assert user_data[67890][3] == 'test4' + + chat_data = await pickle_persistence.get_chat_data() + assert isinstance(chat_data, dict) + assert chat_data[-12345]['test1'] == 'test2' + assert chat_data[-67890][3] == 'test4' + + bot_data = await pickle_persistence.get_bot_data() + assert isinstance(bot_data, dict) + assert bot_data['test1'] == 'test2' + assert bot_data['test3']['test4'] == 'test5' + assert 'test0' not in bot_data + + callback_data = await pickle_persistence.get_callback_data() + assert callback_data is None + + conversation1 = await pickle_persistence.get_conversations('name1') + assert isinstance(conversation1, dict) + assert conversation1[(123, 123)] == 3 + assert conversation1[(456, 654)] == 4 + with pytest.raises(KeyError): + conversation1[(890, 890)] + conversation2 = await pickle_persistence.get_conversations('name2') + assert isinstance(conversation1, dict) + assert conversation2[(123, 321)] == 1 + assert conversation2[(890, 890)] == 2 + with pytest.raises(KeyError): + conversation2[(123, 123)] + + @pytest.mark.asyncio + async def test_with_single_file_wo_bot_data( + self, pickle_persistence, pickle_files_wo_bot_data + ): + pickle_persistence.single_file = True + user_data = await pickle_persistence.get_user_data() + assert isinstance(user_data, dict) + assert user_data[12345]['test1'] == 'test2' + assert user_data[67890][3] == 'test4' + + chat_data = await pickle_persistence.get_chat_data() + assert isinstance(chat_data, dict) + assert chat_data[-12345]['test1'] == 'test2' + assert chat_data[-67890][3] == 'test4' + + bot_data = await pickle_persistence.get_bot_data() + assert isinstance(bot_data, dict) + assert not bot_data.keys() + + callback_data = await pickle_persistence.get_callback_data() + assert isinstance(callback_data, tuple) + assert callback_data[0] == [('test1', 1000, {'button1': 'test0', 'button2': 'test1'})] + assert callback_data[1] == {'test1': 'test2'} + + conversation1 = await pickle_persistence.get_conversations('name1') + assert isinstance(conversation1, dict) + assert conversation1[(123, 123)] == 3 + assert conversation1[(456, 654)] == 4 + with pytest.raises(KeyError): + conversation1[(890, 890)] + conversation2 = await pickle_persistence.get_conversations('name2') + assert isinstance(conversation1, dict) + assert conversation2[(123, 321)] == 1 + assert conversation2[(890, 890)] == 2 + with pytest.raises(KeyError): + conversation2[(123, 123)] + + @pytest.mark.asyncio + async def test_with_single_file_wo_callback_data( + self, pickle_persistence, pickle_files_wo_callback_data + ): + user_data = await pickle_persistence.get_user_data() + assert isinstance(user_data, dict) + assert user_data[12345]['test1'] == 'test2' + assert user_data[67890][3] == 'test4' + + chat_data = await pickle_persistence.get_chat_data() + assert isinstance(chat_data, dict) + assert chat_data[-12345]['test1'] == 'test2' + assert chat_data[-67890][3] == 'test4' + + bot_data = await pickle_persistence.get_bot_data() + assert isinstance(bot_data, dict) + assert bot_data['test1'] == 'test2' + assert bot_data['test3']['test4'] == 'test5' + assert 'test0' not in bot_data + + callback_data = await pickle_persistence.get_callback_data() + assert callback_data is None + + conversation1 = await pickle_persistence.get_conversations('name1') + assert isinstance(conversation1, dict) + assert conversation1[(123, 123)] == 3 + assert conversation1[(456, 654)] == 4 + with pytest.raises(KeyError): + conversation1[(890, 890)] + conversation2 = await pickle_persistence.get_conversations('name2') + assert isinstance(conversation1, dict) + assert conversation2[(123, 321)] == 1 + assert conversation2[(890, 890)] == 2 + with pytest.raises(KeyError): + conversation2[(123, 123)] + + @pytest.mark.asyncio + async def test_updating_multi_file(self, pickle_persistence, good_pickle_files): + user_data = await pickle_persistence.get_user_data() + user_data[12345]['test3']['test4'] = 'test6' + assert pickle_persistence.user_data != user_data + await pickle_persistence.update_user_data(12345, user_data[12345]) + assert pickle_persistence.user_data == user_data + with Path('pickletest_user_data').open('rb') as f: + user_data_test = dict(pickle.load(f)) + assert user_data_test == user_data + await pickle_persistence.drop_user_data(67890) + assert 67890 not in await pickle_persistence.get_user_data() + + chat_data = await pickle_persistence.get_chat_data() + chat_data[-12345]['test3']['test4'] = 'test6' + assert pickle_persistence.chat_data != chat_data + await pickle_persistence.update_chat_data(-12345, chat_data[-12345]) + assert pickle_persistence.chat_data == chat_data + with Path('pickletest_chat_data').open('rb') as f: + chat_data_test = dict(pickle.load(f)) + assert chat_data_test == chat_data + await pickle_persistence.drop_chat_data(-67890) + assert -67890 not in await pickle_persistence.get_chat_data() + + bot_data = await pickle_persistence.get_bot_data() + bot_data['test3']['test4'] = 'test6' + assert pickle_persistence.bot_data != bot_data + await pickle_persistence.update_bot_data(bot_data) + assert pickle_persistence.bot_data == bot_data + with Path('pickletest_bot_data').open('rb') as f: + bot_data_test = pickle.load(f) + assert bot_data_test == bot_data + + callback_data = await pickle_persistence.get_callback_data() + callback_data[1]['test3'] = 'test4' + assert pickle_persistence.callback_data != callback_data + await pickle_persistence.update_callback_data(callback_data) + assert pickle_persistence.callback_data == callback_data + with Path('pickletest_callback_data').open('rb') as f: + callback_data_test = pickle.load(f) + assert callback_data_test == callback_data + + conversation1 = await pickle_persistence.get_conversations('name1') + conversation1[(123, 123)] = 5 + assert not pickle_persistence.conversations['name1'] == conversation1 + await pickle_persistence.update_conversation('name1', (123, 123), 5) + assert pickle_persistence.conversations['name1'] == conversation1 + assert await pickle_persistence.get_conversations('name1') == conversation1 + with Path('pickletest_conversations').open('rb') as f: + conversations_test = dict(pickle.load(f)) + assert conversations_test['name1'] == conversation1 + + pickle_persistence.conversations = None + await pickle_persistence.update_conversation('name1', (123, 123), 5) + assert pickle_persistence.conversations['name1'] == {(123, 123): 5} + assert await pickle_persistence.get_conversations('name1') == {(123, 123): 5} + + @pytest.mark.asyncio + async def test_updating_single_file(self, pickle_persistence, good_pickle_files): + pickle_persistence.single_file = True + + user_data = await pickle_persistence.get_user_data() + user_data[12345]['test3']['test4'] = 'test6' + assert pickle_persistence.user_data != user_data + await pickle_persistence.update_user_data(12345, user_data[12345]) + assert pickle_persistence.user_data == user_data + with Path('pickletest').open('rb') as f: + user_data_test = dict(pickle.load(f))['user_data'] + assert user_data_test == user_data + await pickle_persistence.drop_user_data(67890) + assert 67890 not in await pickle_persistence.get_user_data() + + chat_data = await pickle_persistence.get_chat_data() + chat_data[-12345]['test3']['test4'] = 'test6' + assert pickle_persistence.chat_data != chat_data + await pickle_persistence.update_chat_data(-12345, chat_data[-12345]) + assert pickle_persistence.chat_data == chat_data + with Path('pickletest').open('rb') as f: + chat_data_test = dict(pickle.load(f))['chat_data'] + assert chat_data_test == chat_data + await pickle_persistence.drop_chat_data(-67890) + assert -67890 not in await pickle_persistence.get_chat_data() + + bot_data = await pickle_persistence.get_bot_data() + bot_data['test3']['test4'] = 'test6' + assert pickle_persistence.bot_data != bot_data + await pickle_persistence.update_bot_data(bot_data) + assert pickle_persistence.bot_data == bot_data + with Path('pickletest').open('rb') as f: + bot_data_test = pickle.load(f)['bot_data'] + assert bot_data_test == bot_data + + callback_data = await pickle_persistence.get_callback_data() + callback_data[1]['test3'] = 'test4' + assert pickle_persistence.callback_data != callback_data + await pickle_persistence.update_callback_data(callback_data) + assert pickle_persistence.callback_data == callback_data + with Path('pickletest').open('rb') as f: + callback_data_test = pickle.load(f)['callback_data'] + assert callback_data_test == callback_data + + conversation1 = await pickle_persistence.get_conversations('name1') + conversation1[(123, 123)] = 5 + assert not pickle_persistence.conversations['name1'] == conversation1 + await pickle_persistence.update_conversation('name1', (123, 123), 5) + assert pickle_persistence.conversations['name1'] == conversation1 + assert await pickle_persistence.get_conversations('name1') == conversation1 + with Path('pickletest').open('rb') as f: + conversations_test = dict(pickle.load(f))['conversations'] + assert conversations_test['name1'] == conversation1 + + pickle_persistence.conversations = None + await pickle_persistence.update_conversation('name1', (123, 123), 5) + assert pickle_persistence.conversations['name1'] == {(123, 123): 5} + assert await pickle_persistence.get_conversations('name1') == {(123, 123): 5} + + @pytest.mark.asyncio + async def test_updating_single_file_no_data(self, pickle_persistence): + pickle_persistence.single_file = True + assert not any( + [ + pickle_persistence.user_data, + pickle_persistence.chat_data, + pickle_persistence.bot_data, + pickle_persistence.callback_data, + pickle_persistence.conversations, + ] + ) + await pickle_persistence.flush() + with pytest.raises(FileNotFoundError, match='pickletest'): + open('pickletest', 'rb') + + @pytest.mark.asyncio + async def test_save_on_flush_multi_files(self, pickle_persistence, good_pickle_files): + # Should run without error + await pickle_persistence.flush() + pickle_persistence.on_flush = True + + user_data = await pickle_persistence.get_user_data() + user_data[54321] = {} + user_data[54321]['test9'] = 'test 10' + assert pickle_persistence.user_data != user_data + + await pickle_persistence.update_user_data(54321, user_data[54321]) + assert pickle_persistence.user_data == user_data + + await pickle_persistence.drop_user_data(0) + assert pickle_persistence.user_data == user_data + + with Path('pickletest_user_data').open('rb') as f: + user_data_test = dict(pickle.load(f)) + assert user_data_test != user_data + + chat_data = await pickle_persistence.get_chat_data() + chat_data[54321] = {} + chat_data[54321]['test9'] = 'test 10' + assert pickle_persistence.chat_data != chat_data + + await pickle_persistence.update_chat_data(54321, chat_data[54321]) + assert pickle_persistence.chat_data == chat_data + + await pickle_persistence.drop_chat_data(0) + assert pickle_persistence.user_data == user_data + + with Path('pickletest_chat_data').open('rb') as f: + chat_data_test = dict(pickle.load(f)) + assert chat_data_test != chat_data + + bot_data = await pickle_persistence.get_bot_data() + bot_data['test6'] = 'test 7' + assert pickle_persistence.bot_data != bot_data + + await pickle_persistence.update_bot_data(bot_data) + assert pickle_persistence.bot_data == bot_data + + with Path('pickletest_bot_data').open('rb') as f: + bot_data_test = pickle.load(f) + assert bot_data_test != bot_data + + callback_data = await pickle_persistence.get_callback_data() + callback_data[1]['test3'] = 'test4' + assert pickle_persistence.callback_data != callback_data + + await pickle_persistence.update_callback_data(callback_data) + assert pickle_persistence.callback_data == callback_data + + with Path('pickletest_callback_data').open('rb') as f: + callback_data_test = pickle.load(f) + assert callback_data_test != callback_data + + conversation1 = await pickle_persistence.get_conversations('name1') + conversation1[(123, 123)] = 5 + assert not pickle_persistence.conversations['name1'] == conversation1 + + await pickle_persistence.update_conversation('name1', (123, 123), 5) + assert pickle_persistence.conversations['name1'] == conversation1 + + with Path('pickletest_conversations').open('rb') as f: + conversations_test = dict(pickle.load(f)) + assert not conversations_test['name1'] == conversation1 + + await pickle_persistence.flush() + with Path('pickletest_user_data').open('rb') as f: + user_data_test = dict(pickle.load(f)) + assert user_data_test == user_data + + with Path('pickletest_chat_data').open('rb') as f: + chat_data_test = dict(pickle.load(f)) + assert chat_data_test == chat_data + + with Path('pickletest_bot_data').open('rb') as f: + bot_data_test = pickle.load(f) + assert bot_data_test == bot_data + + with Path('pickletest_conversations').open('rb') as f: + conversations_test = dict(pickle.load(f)) + assert conversations_test['name1'] == conversation1 + + @pytest.mark.asyncio + async def test_save_on_flush_single_files(self, pickle_persistence, good_pickle_files): + # Should run without error + await pickle_persistence.flush() + + pickle_persistence.on_flush = True + pickle_persistence.single_file = True + + user_data = await pickle_persistence.get_user_data() + user_data[54321] = {} + user_data[54321]['test9'] = 'test 10' + assert pickle_persistence.user_data != user_data + await pickle_persistence.update_user_data(54321, user_data[54321]) + assert pickle_persistence.user_data == user_data + with Path('pickletest').open('rb') as f: + user_data_test = dict(pickle.load(f))['user_data'] + assert user_data_test != user_data + + chat_data = await pickle_persistence.get_chat_data() + chat_data[54321] = {} + chat_data[54321]['test9'] = 'test 10' + assert pickle_persistence.chat_data != chat_data + await pickle_persistence.update_chat_data(54321, chat_data[54321]) + assert pickle_persistence.chat_data == chat_data + with Path('pickletest').open('rb') as f: + chat_data_test = dict(pickle.load(f))['chat_data'] + assert chat_data_test != chat_data + + bot_data = await pickle_persistence.get_bot_data() + bot_data['test6'] = 'test 7' + assert pickle_persistence.bot_data != bot_data + await pickle_persistence.update_bot_data(bot_data) + assert pickle_persistence.bot_data == bot_data + with Path('pickletest').open('rb') as f: + bot_data_test = pickle.load(f)['bot_data'] + assert bot_data_test != bot_data + + callback_data = await pickle_persistence.get_callback_data() + callback_data[1]['test3'] = 'test4' + assert pickle_persistence.callback_data != callback_data + await pickle_persistence.update_callback_data(callback_data) + assert pickle_persistence.callback_data == callback_data + with Path('pickletest').open('rb') as f: + callback_data_test = pickle.load(f)['callback_data'] + assert callback_data_test != callback_data + + conversation1 = await pickle_persistence.get_conversations('name1') + conversation1[(123, 123)] = 5 + assert not pickle_persistence.conversations['name1'] == conversation1 + await pickle_persistence.update_conversation('name1', (123, 123), 5) + assert pickle_persistence.conversations['name1'] == conversation1 + with Path('pickletest').open('rb') as f: + conversations_test = dict(pickle.load(f))['conversations'] + assert not conversations_test['name1'] == conversation1 + + await pickle_persistence.flush() + with Path('pickletest').open('rb') as f: + user_data_test = dict(pickle.load(f))['user_data'] + assert user_data_test == user_data + + with Path('pickletest').open('rb') as f: + chat_data_test = dict(pickle.load(f))['chat_data'] + assert chat_data_test == chat_data + + with Path('pickletest').open('rb') as f: + bot_data_test = pickle.load(f)['bot_data'] + assert bot_data_test == bot_data + + with Path('pickletest').open('rb') as f: + conversations_test = dict(pickle.load(f))['conversations'] + assert conversations_test['name1'] == conversation1 + + @pytest.mark.asyncio + async def test_custom_pickler_unpickler_simple( + self, pickle_persistence, update, good_pickle_files, bot, recwarn + ): + pickle_persistence.set_bot(bot) # assign the current bot to the persistence + data_with_bot = {'current_bot': update.message} + await pickle_persistence.update_chat_data( + 12345, data_with_bot + ) # also calls BotPickler.dumps() + + # Test that regular pickle load fails - + err_msg = ( + "A load persistent id instruction was encountered,\nbut no persistent_load " + "function was specified." + ) + with pytest.raises(pickle.UnpicklingError, match=err_msg): + with open('pickletest_chat_data', 'rb') as f: + pickle.load(f) + + # Test that our custom unpickler works as intended -- inserts the current bot + # We have to create a new instance otherwise unpickling is skipped + pp = PicklePersistence("pickletest", single_file=False, on_flush=False) + pp.set_bot(bot) # Set the bot + assert (await pp.get_chat_data())[12345]['current_bot'].get_bot() is bot + + # Now test that pickling of unknown bots in TelegramObjects will be replaced by None- + assert not len(recwarn) + data_with_bot = {} + async with Bot(bot.token) as other_bot: + data_with_bot['unknown_bot_in_user'] = User(1, 'Dev', False, bot=other_bot) + await pickle_persistence.update_chat_data(12345, data_with_bot) + assert len(recwarn) == 1 + assert recwarn[-1].category is PTBUserWarning + assert str(recwarn[-1].message).startswith("Unknown bot instance found.") + pp = PicklePersistence("pickletest", single_file=False, on_flush=False) + pp.set_bot(bot) + assert (await pp.get_chat_data())[12345]['unknown_bot_in_user']._bot is None + + @pytest.mark.asyncio + async def test_custom_pickler_unpickler_with_custom_objects( + self, bot, pickle_persistence, good_pickle_files + ): + dict_s = self.DictSub("private", 'normal', bot) + slot_s = self.SlotsSub("new_var", 'private_var') + regular = self.NormalClass(12) + + pickle_persistence.set_bot(bot) + await pickle_persistence.update_user_data( + 1232, {'sub_dict': dict_s, 'sub_slots': slot_s, 'r': regular} + ) + pp = PicklePersistence("pickletest", single_file=False, on_flush=False) + pp.set_bot(bot) # Set the bot + data = (await pp.get_user_data())[1232] + sub_dict = data['sub_dict'] + sub_slots = data['sub_slots'] + sub_regular = data['r'] + assert sub_dict._bot is bot + assert sub_dict.normal == dict_s.normal + assert sub_dict._private == dict_s._private + assert sub_slots.new_var == slot_s.new_var + assert sub_slots._private == slot_s._private + assert sub_slots._bot is None # We didn't set the bot, so it shouldn't have it here. + assert sub_regular.my_var == regular.my_var + + @pytest.mark.parametrize( + 'filepath', + ['pickletest', Path('pickletest')], + ids=['str filepath', 'pathlib.Path filepath'], + ) + @pytest.mark.asyncio + async def test_filepath_argument_types(self, filepath): + pick_persist = PicklePersistence( + filepath=filepath, + on_flush=False, + ) + await pick_persist.update_user_data(1, 1) + + assert (await pick_persist.get_user_data())[1] == 1 + assert Path(filepath).is_file() + + @pytest.mark.parametrize('singlefile', [True, False]) + @pytest.mark.parametrize('ud', [int, float, complex]) + @pytest.mark.parametrize('cd', [int, float, complex]) + @pytest.mark.parametrize('bd', [int, float, complex]) + @pytest.mark.asyncio + async def test_with_context_types(self, ud, cd, bd, singlefile): + cc = ContextTypes(user_data=ud, chat_data=cd, bot_data=bd) + persistence = PicklePersistence('pickletest', single_file=singlefile, context_types=cc) + + assert isinstance(await persistence.get_bot_data(), bd) + assert await persistence.get_bot_data() == 0 + + persistence.user_data = None + persistence.chat_data = None + await persistence.drop_user_data(123) + await persistence.drop_chat_data(123) + assert isinstance(await persistence.get_user_data(), dict) + assert isinstance(await persistence.get_chat_data(), dict) + persistence.user_data = None + persistence.chat_data = None + await persistence.update_user_data(1, ud(1)) + await persistence.update_chat_data(1, cd(1)) + await persistence.update_bot_data(bd(1)) + assert (await persistence.get_user_data())[1] == 1 + assert (await persistence.get_chat_data())[1] == 1 + assert await persistence.get_bot_data() == 1 + + await persistence.flush() + persistence = PicklePersistence('pickletest', single_file=singlefile, context_types=cc) + assert isinstance((await persistence.get_user_data())[1], ud) + assert (await persistence.get_user_data())[1] == 1 + assert isinstance((await persistence.get_chat_data())[1], cd) + assert (await persistence.get_chat_data())[1] == 1 + assert isinstance(await persistence.get_bot_data(), bd) + assert await persistence.get_bot_data() == 1 From f0103bd78e060eaff1cc397f91096c6378e23597 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 27 Mar 2022 11:23:02 +0200 Subject: [PATCH 04/25] Bump coverage for persistence a bit --- tests/test_dictpersistence.py | 80 +++++++++++++++++++++++++++------ tests/test_picklepersistence.py | 25 +++++++++++ 2 files changed, 92 insertions(+), 13 deletions(-) diff --git a/tests/test_dictpersistence.py b/tests/test_dictpersistence.py index 9f0ac9a8929..34c5394cbf2 100644 --- a/tests/test_dictpersistence.py +++ b/tests/test_dictpersistence.py @@ -16,9 +16,6 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. -import os -from pathlib import Path - import pytest @@ -30,16 +27,6 @@ from telegram.ext import DictPersistence -@pytest.fixture(autouse=True) -def change_directory(tmp_path: Path): - orig_dir = Path.cwd() - # Switch to a temporary directory, so we don't have to worry about cleaning up files - os.chdir(tmp_path) - yield - # Go back to original directory - os.chdir(orig_dir) - - @pytest.fixture(autouse=True) def reset_callback_data_cache(bot): yield @@ -347,3 +334,70 @@ async def test_updating( dict_persistence.conversations_json == DictPersistence._encode_conversations_to_json({"name1": {(123, 123): 5}}) ) + + @pytest.mark.asyncio + async def test_no_data_on_init( + self, bot_data, user_data, chat_data, conversations, callback_data + ): + dict_persistence = DictPersistence() + + assert dict_persistence.user_data is None + assert dict_persistence.chat_data is None + assert dict_persistence.bot_data is None + assert dict_persistence.conversations is None + assert dict_persistence.callback_data is None + assert dict_persistence.user_data_json == 'null' + assert dict_persistence.chat_data_json == 'null' + assert dict_persistence.bot_data_json == 'null' + assert dict_persistence.conversations_json == 'null' + assert dict_persistence.callback_data_json == 'null' + + await dict_persistence.update_bot_data(bot_data) + await dict_persistence.update_user_data(12345, user_data[12345]) + await dict_persistence.update_chat_data(-12345, chat_data[-12345]) + await dict_persistence.update_conversation('name', (1, 1), 'new_state') + await dict_persistence.update_callback_data(callback_data) + + assert dict_persistence.user_data[12345] == user_data[12345] + assert dict_persistence.chat_data[-12345] == chat_data[-12345] + assert dict_persistence.bot_data == bot_data + assert dict_persistence.conversations['name'] == {(1, 1): 'new_state'} + assert dict_persistence.callback_data == callback_data + + @pytest.mark.asyncio + async def test_no_json_dumping_if_data_did_not_change( + self, bot_data, user_data, chat_data, conversations, callback_data, monkeypatch + ): + dict_persistence = DictPersistence() + + await dict_persistence.update_bot_data(bot_data) + await dict_persistence.update_user_data(12345, user_data[12345]) + await dict_persistence.update_chat_data(-12345, chat_data[-12345]) + await dict_persistence.update_conversation('name', (1, 1), 'new_state') + await dict_persistence.update_callback_data(callback_data) + + assert dict_persistence.user_data_json == json.dumps({12345: user_data[12345]}) + assert dict_persistence.chat_data_json == json.dumps({-12345: chat_data[-12345]}) + assert dict_persistence.bot_data_json == json.dumps(bot_data) + assert ( + dict_persistence.conversations_json + == DictPersistence._encode_conversations_to_json({'name': {(1, 1): 'new_state'}}) + ) + assert dict_persistence.callback_data_json == json.dumps(callback_data) + + flag = False + + def dumps(*args, **kwargs): + nonlocal flag + flag = True + + # Since the data doesn't change, json.dumps shoduln't be called beyond this point! + monkeypatch.setattr(json, 'dumps', dumps) + + await dict_persistence.update_bot_data(bot_data) + await dict_persistence.update_user_data(12345, user_data[12345]) + await dict_persistence.update_chat_data(-12345, chat_data[-12345]) + await dict_persistence.update_conversation('name', (1, 1), 'new_state') + await dict_persistence.update_callback_data(callback_data) + + assert not flag diff --git a/tests/test_picklepersistence.py b/tests/test_picklepersistence.py index dfb2e18611e..fd3a77996bd 100644 --- a/tests/test_picklepersistence.py +++ b/tests/test_picklepersistence.py @@ -988,3 +988,28 @@ async def test_with_context_types(self, ud, cd, bd, singlefile): assert (await persistence.get_chat_data())[1] == 1 assert isinstance(await persistence.get_bot_data(), bd) assert await persistence.get_bot_data() == 1 + + @pytest.mark.asyncio + async def test_no_write_if_data_did_not_change( + self, pickle_persistence, bot_data, user_data, chat_data, conversations, callback_data + ): + pickle_persistence.single_file = True + pickle_persistence.on_flush = False + + await pickle_persistence.update_bot_data(bot_data) + await pickle_persistence.update_user_data(12345, user_data[12345]) + await pickle_persistence.update_chat_data(-12345, chat_data[-12345]) + await pickle_persistence.update_conversation('name', (1, 1), 'new_state') + await pickle_persistence.update_callback_data(callback_data) + + assert pickle_persistence.filepath.is_file() + pickle_persistence.filepath.unlink() + assert not pickle_persistence.filepath.is_file() + + await pickle_persistence.update_bot_data(bot_data) + await pickle_persistence.update_user_data(12345, user_data[12345]) + await pickle_persistence.update_chat_data(-12345, chat_data[-12345]) + await pickle_persistence.update_conversation('name', (1, 1), 'new_state') + await pickle_persistence.update_callback_data(callback_data) + + assert not pickle_persistence.filepath.is_file() From da3e3dbc1336e62bfb709f7c666e0e62411f6fbb Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 27 Mar 2022 11:59:11 +0200 Subject: [PATCH 05/25] A few minor coverage improvements --- tests/test_gamehighscore.py | 2 ++ tests/test_message.py | 40 +++++++++++++++++++++---------- tests/test_poll.py | 12 ++++++++++ tests/test_replykeyboardmarkup.py | 6 +++++ tests/test_voicechat.py | 16 +++++++++---- 5 files changed, 59 insertions(+), 17 deletions(-) diff --git a/tests/test_gamehighscore.py b/tests/test_gamehighscore.py index 570ec235449..900f0f9329f 100644 --- a/tests/test_gamehighscore.py +++ b/tests/test_gamehighscore.py @@ -47,6 +47,8 @@ def test_de_json(self, bot): assert highscore.user == self.user assert highscore.score == self.score + assert GameHighScore.de_json(None, bot) is None + def test_to_dict(self, game_highscore): game_highscore_dict = game_highscore.to_dict() diff --git a/tests/test_message.py b/tests/test_message.py index daa7d377817..dc42291c22b 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -182,6 +182,12 @@ def message(bot): {'sender_chat': Chat(-123, 'discussion_channel')}, {'is_automatic_forward': True}, {'has_protected_content': True}, + { + 'entities': [ + MessageEntity(MessageEntity.BOLD, 0, 1), + MessageEntity(MessageEntity.TEXT_LINK, 2, 3, url='https://ptb.org'), + ] + }, ], ids=[ 'forwarded_user', @@ -234,6 +240,7 @@ def message(bot): 'sender_chat', 'is_automatic_forward', 'has_protected_content', + 'entities', ], ) def message_params(bot, request): @@ -328,6 +335,9 @@ async def test_parse_entity(self): message = Message(1, self.from_user, self.date, self.chat, text=text, entities=[entity]) assert message.parse_entity(entity) == 'http://google.com' + with pytest.raises(RuntimeError, match='Message has no'): + Message(message_id=1, date=self.date, chat=self.chat).parse_entity(entity) + @pytest.mark.asyncio async def test_parse_caption_entity(self): caption = ( @@ -340,6 +350,9 @@ async def test_parse_caption_entity(self): ) assert message.parse_caption_entity(entity) == 'http://google.com' + with pytest.raises(RuntimeError, match='Message has no'): + Message(message_id=1, date=self.date, chat=self.chat).parse_entity(entity) + @pytest.mark.asyncio async def test_parse_entities(self): text = ( @@ -669,18 +682,21 @@ def test_effective_attachment(self, message_params): 'venue', ] - attachment = message_params.effective_attachment - if attachment: - condition = any( - message_params[message_type] is attachment - for message_type in expected_attachment_types - ) - assert condition, 'Got effective_attachment for unexpected type' - else: - condition = any( - message_params[message_type] for message_type in expected_attachment_types - ) - assert not condition, 'effective_attachment was None even though it should not be' + for _ in range(3): + # We run the same test multiple times to make sure that the caching is tested + + attachment = message_params.effective_attachment + if attachment: + condition = any( + message_params[message_type] is attachment + for message_type in expected_attachment_types + ) + assert condition, 'Got effective_attachment for unexpected type' + else: + condition = any( + message_params[message_type] for message_type in expected_attachment_types + ) + assert not condition, 'effective_attachment was None even though it should not be' @pytest.mark.asyncio async def test_reply_text(self, monkeypatch, message): diff --git a/tests/test_poll.py b/tests/test_poll.py index 1c32b66a94d..2f8b042828c 100644 --- a/tests/test_poll.py +++ b/tests/test_poll.py @@ -217,6 +217,18 @@ def test_parse_entity(self, poll): assert poll.parse_explanation_entity(entity) == 'http://google.com' + with pytest.raises(RuntimeError, match='Poll has no'): + Poll( + 'id', + 'question', + [PollOption('text', voter_count=0)], + total_voter_count=0, + is_closed=False, + is_anonymous=False, + type=Poll.QUIZ, + allows_multiple_answers=False, + ).parse_explanation_entity(entity) + def test_parse_entities(self, poll): entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17) entity_2 = MessageEntity(type=MessageEntity.BOLD, offset=13, length=1) diff --git a/tests/test_replykeyboardmarkup.py b/tests/test_replykeyboardmarkup.py index 405a85cd78a..0a69e279d1d 100644 --- a/tests/test_replykeyboardmarkup.py +++ b/tests/test_replykeyboardmarkup.py @@ -100,6 +100,12 @@ def test_expected_values(self, reply_keyboard_markup): assert reply_keyboard_markup.one_time_keyboard == self.one_time_keyboard assert reply_keyboard_markup.selective == self.selective + def test_wrong_keyboard_inputs(self): + with pytest.raises(ValueError): + ReplyKeyboardMarkup([['button1'], 'Button2']) + with pytest.raises(ValueError): + ReplyKeyboardMarkup('button') + def test_to_dict(self, reply_keyboard_markup): reply_keyboard_markup_dict = reply_keyboard_markup.to_dict() diff --git a/tests/test_voicechat.py b/tests/test_voicechat.py index 92251b073df..ef93cb22c86 100644 --- a/tests/test_voicechat.py +++ b/tests/test_voicechat.py @@ -111,14 +111,20 @@ def test_de_json(self, user1, user2, bot): assert voice_chat_participants.users[0].id == user1.id assert voice_chat_participants.users[1].id == user2.id - def test_to_dict(self, user1, user2): - voice_chat_participants = VoiceChatParticipantsInvited([user1, user2]) + @pytest.mark.parametrize('use_users', (True, False)) + def test_to_dict(self, user1, user2, use_users): + voice_chat_participants = VoiceChatParticipantsInvited( + [user1, user2] if use_users else None + ) voice_chat_dict = voice_chat_participants.to_dict() assert isinstance(voice_chat_dict, dict) - assert voice_chat_dict["users"] == [user1.to_dict(), user2.to_dict()] - assert voice_chat_dict["users"][0]["id"] == user1.id - assert voice_chat_dict["users"][1]["id"] == user2.id + if use_users: + assert voice_chat_dict["users"] == [user1.to_dict(), user2.to_dict()] + assert voice_chat_dict["users"][0]["id"] == user1.id + assert voice_chat_dict["users"][1]["id"] == user2.id + else: + assert voice_chat_dict == {} def test_equality(self, user1, user2): a = VoiceChatParticipantsInvited([user1]) From e247a9d63f49f5aedff7393f31a0f52ac14be438 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 30 Mar 2022 22:32:06 +0200 Subject: [PATCH 06/25] Test string representation of errors --- telegram/error.py | 3 +-- tests/test_error.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/telegram/error.py b/telegram/error.py index 69aa9024920..7a8e0004822 100644 --- a/telegram/error.py +++ b/telegram/error.py @@ -74,9 +74,8 @@ def __init__(self, message: str): def __str__(self) -> str: return self.message - # TODO: test this def __repr__(self) -> str: - return f'{self.__class__.__name__}({self.message})' + return f"{self.__class__.__name__}('{self.message}')" def __reduce__(self) -> Tuple[type, Tuple[str]]: return self.__class__, (self.message,) diff --git a/tests/test_error.py b/tests/test_error.py index c425fdf9e3a..25c51f665d3 100644 --- a/tests/test_error.py +++ b/tests/test_error.py @@ -178,3 +178,17 @@ def make_assertion(cls): ) make_assertion(TelegramError) + + def test_string_representations(self): + """We just randomly test a few of the subclasses - should suffice""" + e = TelegramError('This is a message') + assert repr(e) == "TelegramError('This is a message')" + assert str(e) == "This is a message" + + e = RetryAfter(42) + assert repr(e) == "RetryAfter('Flood control exceeded. Retry in 42.0 seconds')" + assert str(e) == 'Flood control exceeded. Retry in 42.0 seconds' + + e = BadRequest('This is a message') + assert repr(e) == "BadRequest('This is a message')" + assert str(e) == "This is a message" From 8546e3ccdee4e6703dfac866e6d3f3989dc0a4f5 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Fri, 1 Apr 2022 19:07:59 +0200 Subject: [PATCH 07/25] A first test on persisting non-blocking conversations --- tests/test_basepersistence.py | 48 ++++++++++++++++++++++++++++++++--- tests/test_trackingdict.py | 7 +++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/tests/test_basepersistence.py b/tests/test_basepersistence.py index ec9d198c07e..cf5bb49f2ee 100644 --- a/tests/test_basepersistence.py +++ b/tests/test_basepersistence.py @@ -209,10 +209,10 @@ def build_update(state: HandlerStates, chat_id: int): return make_message_update(message=str(state.value), user=user, chat=chat) @classmethod - def build_handler(cls, state: HandlerStates): + def build_handler(cls, state: HandlerStates, callback=None): return MessageHandler( filters.Regex(f'^{state.value}$'), - functools.partial(cls.callback, state=state), + callback or functools.partial(cls.callback, state=state), ) @@ -229,7 +229,7 @@ class PappInput(NamedTuple): def build_papp( token: str, store_data: dict = None, update_interval: float = None, fill_data: bool = False ) -> Application: - store_data = PersistenceInput(**store_data) + store_data = PersistenceInput(**(store_data or {})) if update_interval is not None: persistence = TrackingPersistence( store_data=store_data, update_interval=update_interval, fill_data=fill_data @@ -1139,3 +1139,45 @@ async def raise_error(*args, **kwargs): else: assert not papp.persistence.updated_chat_ids assert not papp.persistence.updated_conversations + + @pytest.mark.asyncio + async def test_non_blocking_conversations(self, bot): + papp = build_papp(token=bot.token) + event = asyncio.Event() + + async def callback(_, __): + await event.wait() + return HandlerStates.STATE_1 + + conversation = ConversationHandler( + entry_points=[ + TrackingConversationHandler.build_handler(HandlerStates.END, callback=callback) + ], + states={}, + fallbacks=[], + persistent=True, + name='conv', + block=False, + ) + papp.add_handler(conversation) + + async with papp: + assert papp.persistence.updated_conversations == {} + + await papp.process_update( + TrackingConversationHandler.build_update(HandlerStates.END, 1) + ) + assert papp.persistence.updated_conversations == {} + + await papp.update_persistence() + await asyncio.sleep(0.01) + # Conversation should have been updated with the current state, i.e. None + assert papp.persistence.updated_conversations == {'conv': ({(1, 1): 1})} + assert papp.persistence.conversations == {'conv': {(1, 1): None}} + + papp.persistence.reset_tracking() + event.set() + await asyncio.sleep(0.01) + await papp.update_persistence() + assert papp.persistence.updated_conversations == {'conv': {(1, 1): 1}} + assert papp.persistence.conversations == {'conv': {(1, 1): HandlerStates.STATE_1}} diff --git a/tests/test_trackingdict.py b/tests/test_trackingdict.py index 7f8849a693f..07da2dae5f0 100644 --- a/tests/test_trackingdict.py +++ b/tests/test_trackingdict.py @@ -159,3 +159,10 @@ def test_iter(self, td, data): td.update_no_track({2: 2, 3: 3, 4: 4}) assert not td.pop_accessed_keys() assert list(iter(td)) == list(iter(data)) + + def test_mark_as_accessed(self, td): + td[1] = 2 + assert td.pop_accessed_keys() == {1} + assert td.pop_accessed_keys() == set() + td.mark_as_accessed(1) + assert td.pop_accessed_keys() == {1} From 527f94a92e6d51ca4044c75906c413dabfe855fb Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 3 Apr 2022 12:57:00 +0200 Subject: [PATCH 08/25] Some more tests on CH + persistence --- tests/test_basepersistence.py | 153 ++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/tests/test_basepersistence.py b/tests/test_basepersistence.py index cf5bb49f2ee..5c1c67959ff 100644 --- a/tests/test_basepersistence.py +++ b/tests/test_basepersistence.py @@ -1181,3 +1181,156 @@ async def callback(_, __): await papp.update_persistence() assert papp.persistence.updated_conversations == {'conv': {(1, 1): 1}} assert papp.persistence.conversations == {'conv': {(1, 1): HandlerStates.STATE_1}} + + @pytest.mark.asyncio + async def test_non_blocking_conversations_raises_Exception(self, bot): + papp = build_papp(token=bot.token) + + async def callback_1(_, __): + return HandlerStates.STATE_1 + + async def callback_2(_, __): + raise Exception('Test Exception') + + conversation = ConversationHandler( + entry_points=[ + TrackingConversationHandler.build_handler(HandlerStates.END, callback=callback_1) + ], + states={ + HandlerStates.STATE_1: [ + TrackingConversationHandler.build_handler( + HandlerStates.STATE_1, callback=callback_2 + ) + ] + }, + fallbacks=[], + persistent=True, + name='conv', + block=False, + ) + papp.add_handler(conversation) + + async with papp: + assert papp.persistence.updated_conversations == {} + + await papp.process_update( + TrackingConversationHandler.build_update(HandlerStates.END, 1) + ) + assert papp.persistence.updated_conversations == {} + + await papp.update_persistence() + await asyncio.sleep(0.05) + assert papp.persistence.updated_conversations == {'conv': ({(1, 1): 1})} + # The result of the pending state wasn't retrieved by the CH yet, so we must be in + # state `None` + assert papp.persistence.conversations == {'conv': {(1, 1): None}} + + await papp.process_update( + TrackingConversationHandler.build_update(HandlerStates.STATE_1, 1) + ) + + papp.persistence.reset_tracking() + await asyncio.sleep(0.01) + await papp.update_persistence() + assert papp.persistence.updated_conversations == {'conv': {(1, 1): 1}} + # since the second callback raised an exception, the state must be the previous one! + assert papp.persistence.conversations == {'conv': {(1, 1): HandlerStates.STATE_1}} + + @pytest.mark.asyncio + async def test_non_blocking_conversations_on_stop(self, bot): + papp = build_papp(token=bot.token, update_interval=100) + event = asyncio.Event() + + async def callback(_, __): + await event.wait() + return HandlerStates.STATE_1 + + conversation = ConversationHandler( + entry_points=[ + TrackingConversationHandler.build_handler(HandlerStates.END, callback=callback) + ], + states={}, + fallbacks=[], + persistent=True, + name='conv', + block=False, + ) + papp.add_handler(conversation) + + await papp.initialize() + assert papp.persistence.updated_conversations == {} + await papp.start() + + await papp.process_update(TrackingConversationHandler.build_update(HandlerStates.END, 1)) + assert papp.persistence.updated_conversations == {} + + stop_task = asyncio.create_task(papp.stop()) + assert not stop_task.done() + event.set() + await asyncio.sleep(0.05) + assert stop_task.done() + assert papp.persistence.updated_conversations == {} + + await papp.shutdown() + await asyncio.sleep(0.01) + # The pending state must have been resolved on shutdown! + assert papp.persistence.updated_conversations == {'conv': {(1, 1): 1}} + assert papp.persistence.conversations == {'conv': {(1, 1): HandlerStates.STATE_1}} + + @pytest.mark.asyncio + async def test_non_blocking_conversations_on_improper_stop(self, bot, caplog): + papp = build_papp(token=bot.token, update_interval=100) + event = asyncio.Event() + + async def callback(_, __): + await event.wait() + return HandlerStates.STATE_1 + + conversation = ConversationHandler( + entry_points=[ + TrackingConversationHandler.build_handler(HandlerStates.END, callback=callback) + ], + states={}, + fallbacks=[], + persistent=True, + name='conv', + block=False, + ) + papp.add_handler(conversation) + + await papp.initialize() + assert papp.persistence.updated_conversations == {} + + await papp.process_update(TrackingConversationHandler.build_update(HandlerStates.END, 1)) + assert papp.persistence.updated_conversations == {} + + with caplog.at_level(logging.WARNING): + await papp.shutdown() + await asyncio.sleep(0.01) + # Because the app wasn't running, the pending state isn't ensured to be done on + # shutdown - hence we expect the persistence to be updated with state `None` + assert papp.persistence.updated_conversations == {'conv': {(1, 1): 1}} + assert papp.persistence.conversations == {'conv': {(1, 1): None}} + + # Ensure that we warn the user about this! + found_record = None + for record in caplog.records: + if record.getMessage().startswith('A ConversationHandlers state was not yet resolved'): + found_record = record + break + assert found_record is not None + + @default_papp + @pytest.mark.asyncio + async def test_conversation_ends(self, papp): + async with papp: + assert papp.persistence.updated_conversations == {} + + for state in HandlerStates: + await papp.process_update(TrackingConversationHandler.build_update(state, 1)) + assert papp.persistence.updated_conversations == {} + + await papp.update_persistence() + assert papp.persistence.updated_conversations == {'conv_1': ({(1, 1): 1})} + # This is the important part: the persistence is updated with `None` when the conv ends + assert papp.persistence.conversations == {'conv_1': {(1, 1): None}} From f0fa1dd503133421d720de1d083e857ddd449bd3 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sun, 3 Apr 2022 16:32:56 +0200 Subject: [PATCH 09/25] Test persistence of nested conversations --- tests/test_basepersistence.py | 173 ++++++++++++++++++++++++++++++++-- 1 file changed, 165 insertions(+), 8 deletions(-) diff --git a/tests/test_basepersistence.py b/tests/test_basepersistence.py index 5c1c67959ff..176eda7f2d4 100644 --- a/tests/test_basepersistence.py +++ b/tests/test_basepersistence.py @@ -29,7 +29,7 @@ import pytest from flaky import flaky -from telegram import User, Chat, InlineKeyboardMarkup, InlineKeyboardButton, Bot +from telegram import User, Chat, InlineKeyboardMarkup, InlineKeyboardButton, Bot, Update from telegram.ext import ( ApplicationBuilder, PersistenceInput, @@ -302,13 +302,6 @@ class TestBasePersistence: """Tests basic behavior of BasePersistence and (most importantly) the integration of persistence into the Application.""" - # TODO: Test integration of the more intricate ConversationHandler things once CH itself is - # tested. This includes: - # * pending states, i.e. non-blocking handlers - # * pending states being unresolved on shutdown - # * conversation timeouts - # * nested conversations (can conversations be persistent if their parents aren't?) - def job_callback(self, chat_id: int = None): async def callback(context): if context.user_data: @@ -1334,3 +1327,167 @@ async def test_conversation_ends(self, papp): assert papp.persistence.updated_conversations == {'conv_1': ({(1, 1): 1})} # This is the important part: the persistence is updated with `None` when the conv ends assert papp.persistence.conversations == {'conv_1': {(1, 1): None}} + + @pytest.mark.asyncio + async def test_conversation_timeout(self, bot): + # high update_interval so that we can instead manually call it + papp = build_papp(token=bot.token, update_interval=150) + + async def callback(_, __): + return HandlerStates.STATE_1 + + conversation = ConversationHandler( + entry_points=[ + TrackingConversationHandler.build_handler(HandlerStates.END, callback=callback) + ], + states={HandlerStates.STATE_1: []}, + fallbacks=[], + persistent=True, + name='conv', + conversation_timeout=3, + ) + papp.add_handler(conversation) + + async with papp: + await papp.start() + assert papp.persistence.updated_conversations == {} + + await papp.process_update( + TrackingConversationHandler.build_update(HandlerStates.END, 1) + ) + assert papp.persistence.updated_conversations == {} + await papp.update_persistence() + assert papp.persistence.updated_conversations == {'conv': ({(1, 1): 1})} + assert papp.persistence.conversations == {'conv': {(1, 1): HandlerStates.STATE_1}} + + papp.persistence.reset_tracking() + await asyncio.sleep(4) + # After the timeout the conversation should run the entry point again … + assert conversation.check_update( + TrackingConversationHandler.build_update(HandlerStates.END, 1) + ) + await papp.update_persistence() + # … and persistence should be updated with `None` + assert papp.persistence.updated_conversations == {'conv': {(1, 1): 1}} + assert papp.persistence.conversations == {'conv': {(1, 1): None}} + + await papp.stop() + + @pytest.mark.asyncio + async def test_persistent_nested_conversations(self, bot): + papp = build_papp(token=bot.token, update_interval=150) + + def build_callback( + state: HandlerStates, + ): + async def callback(_: Update, __: CallbackContext) -> HandlerStates: + return state + + return callback + + grand_child = ConversationHandler( + entry_points=[TrackingConversationHandler.build_handler(HandlerStates.END)], + states={ + HandlerStates.STATE_1: [ + TrackingConversationHandler.build_handler( + HandlerStates.STATE_1, callback=build_callback(HandlerStates.END) + ) + ] + }, + fallbacks=[], + persistent=True, + name='grand_child', + map_to_parent={HandlerStates.END: HandlerStates.STATE_2}, + ) + + child = ConversationHandler( + entry_points=[TrackingConversationHandler.build_handler(HandlerStates.END)], + states={ + HandlerStates.STATE_1: [grand_child], + HandlerStates.STATE_2: [ + TrackingConversationHandler.build_handler(HandlerStates.STATE_2) + ], + }, + fallbacks=[], + persistent=True, + name='child', + map_to_parent={HandlerStates.STATE_3: HandlerStates.STATE_2}, + ) + + parent = ConversationHandler( + entry_points=[TrackingConversationHandler.build_handler(HandlerStates.END)], + states={ + HandlerStates.STATE_1: [child], + HandlerStates.STATE_2: [ + TrackingConversationHandler.build_handler( + HandlerStates.STATE_2, callback=build_callback(HandlerStates.END) + ) + ], + }, + fallbacks=[], + persistent=True, + name='parent', + ) + + papp.add_handler(parent) + papp.persistence.conversations['grand_child'][(1, 1)] = HandlerStates.STATE_1 + papp.persistence.conversations['child'][(1, 1)] = HandlerStates.STATE_1 + papp.persistence.conversations['parent'][(1, 1)] = HandlerStates.STATE_1 + + # Should load the stored data into the persistence so that the updates below are handled + # accordingly + await papp.initialize() + assert papp.persistence.updated_conversations == {} + + assert not parent.check_update( + TrackingConversationHandler.build_update(HandlerStates.STATE_2, 1) + ) + assert not parent.check_update( + TrackingConversationHandler.build_update(HandlerStates.END, 1) + ) + assert parent.check_update( + TrackingConversationHandler.build_update(HandlerStates.STATE_1, 1) + ) + + await papp.process_update( + TrackingConversationHandler.build_update(HandlerStates.STATE_1, 1) + ) + assert papp.persistence.updated_conversations == {} + await papp.update_persistence() + assert papp.persistence.updated_conversations == { + 'grand_child': {(1, 1): 1}, + 'child': {(1, 1): 1}, + } + assert papp.persistence.conversations == { + 'grand_child': {(1, 1): None}, + 'child': {(1, 1): HandlerStates.STATE_2}, + 'parent': {(1, 1): HandlerStates.STATE_1}, + } + + papp.persistence.reset_tracking() + await papp.process_update( + TrackingConversationHandler.build_update(HandlerStates.STATE_2, 1) + ) + await papp.update_persistence() + assert papp.persistence.updated_conversations == { + 'parent': {(1, 1): 1}, + 'child': {(1, 1): 1}, + } + assert papp.persistence.conversations == { + 'child': {(1, 1): None}, + 'parent': {(1, 1): HandlerStates.STATE_2}, + } + + papp.persistence.reset_tracking() + await papp.process_update( + TrackingConversationHandler.build_update(HandlerStates.STATE_2, 1) + ) + await papp.update_persistence() + assert papp.persistence.updated_conversations == { + 'parent': {(1, 1): 1}, + } + assert papp.persistence.conversations == { + 'parent': {(1, 1): None}, + } + + await papp.shutdown() From 5d8acfa4285a55c69c3220c32c94909c2bf37756 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Tue, 5 Apr 2022 18:14:33 +0200 Subject: [PATCH 10/25] Slow start on testing ConversationHandler --- tests/test_conversationhandler.py | 1538 +++++++++++++++++++++++++++++ 1 file changed, 1538 insertions(+) diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py index 950bfdc6a22..82f76a489fd 100644 --- a/tests/test_conversationhandler.py +++ b/tests/test_conversationhandler.py @@ -16,3 +16,1541 @@ # # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +"""Persistence of conversations is tested in test_basepersistence.py""" +import pytest + +from telegram.ext import ConversationHandler + + +class TestConversationHandler: + """Persistence of conversations is tested in test_basepersistence.py""" + + raise_app_handler_stop = False + test_flag = False + + def test_slot_behaviour(self, mro_slots): + handler = ConversationHandler(entry_points=[], states={}, fallbacks=[]) + for attr in handler.__slots__: + assert getattr(handler, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(handler)) == len(set(mro_slots(handler))), "duplicate slot" + + def test_init(self): + entry_points = [] + states = {} + fallbacks = [] + map_to_parent = {} + ch = ConversationHandler( + entry_points=entry_points, + states=states, + fallbacks=fallbacks, + per_chat='per_chat', + per_user='per_user', + per_message='per_message', + persistent='persistent', + name='name', + allow_reentry='allow_reentry', + conversation_timeout=42, + map_to_parent=map_to_parent, + ) + assert ch.entry_points is entry_points + assert ch.states is states + assert ch.fallbacks is fallbacks + assert ch.map_to_parent is map_to_parent + assert ch.per_chat == 'per_chat' + assert ch.per_user == 'per_user' + assert ch.per_message == 'per_message' + assert ch.persistent == 'persistent' + assert ch.name == 'name' + assert ch.allow_reentry == 'allow_reentry' + + @pytest.mark.parametrize( + 'attr', + [ + 'entry_points', + 'states', + 'fallbacks', + 'per_chat', + 'per_user', + 'per_message', + 'name', + 'persistent', + 'allow_reentry', + 'conversation_timeout', + 'map_to_parent', + ], + indirect=False, + ) + def test_immutable(self, attr): + ch = ConversationHandler(entry_points=[], states={}, fallbacks=[]) + with pytest.raises(AttributeError, match=f'You can not assign a new value to {attr}'): + setattr(ch, attr, True) + + def test_per_all_false(self): + with pytest.raises(ValueError, match="can't all be 'False'"): + ConversationHandler( + entry_points=[], + states={}, + fallbacks=[], + per_chat=False, + per_user=False, + per_message=False, + ) + + # def test_conversation_handler(self, dp, bot, user1, user2): + # handler = ConversationHandler( + # entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks + # ) + # dp.add_handler(handler) + # + # # User one, starts the state machine. + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.THIRSTY + # + # # The user is thirsty and wants to brew coffee. + # message.text = '/brew' + # message.entities[0].length = len('/brew') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.BREWING + # + # # Lets see if an invalid command makes sure, no state is changed. + # message.text = '/nothing' + # message.entities[0].length = len('/nothing') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.BREWING + # + # # Lets see if the state machine still works by pouring coffee. + # message.text = '/pourCoffee' + # message.entities[0].length = len('/pourCoffee') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.DRINKING + # + # # Let's now verify that for another user, who did not start yet, + # # the state has not been changed. + # message.from_user = user2 + # dp.process_update(Update(update_id=0, message=message)) + # with pytest.raises(KeyError): + # self.current_state[user2.id] + # + # def test_conversation_handler_end(self, caplog, dp, bot, user1): + # handler = ConversationHandler( + # entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks + # ) + # dp.add_handler(handler) + # + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # dp.process_update(Update(update_id=0, message=message)) + # message.text = '/brew' + # message.entities[0].length = len('/brew') + # dp.process_update(Update(update_id=0, message=message)) + # message.text = '/pourCoffee' + # message.entities[0].length = len('/pourCoffee') + # dp.process_update(Update(update_id=0, message=message)) + # message.text = '/end' + # message.entities[0].length = len('/end') + # caplog.clear() + # with caplog.at_level(logging.ERROR): + # dp.process_update(Update(update_id=0, message=message)) + # assert len(caplog.records) == 0 + # assert self.current_state[user1.id] == self.END + # with pytest.raises(KeyError): + # print(handler.conversations[(self.group.id, user1.id)]) + # + # def test_conversation_handler_fallback(self, dp, bot, user1, user2): + # handler = ConversationHandler( + # entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks + # ) + # dp.add_handler(handler) + # + # # first check if fallback will not trigger start when not started + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/eat', + # entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, + # length=len('/eat'))], + # bot=bot, + # ) + # dp.process_update(Update(update_id=0, message=message)) + # with pytest.raises(KeyError): + # self.current_state[user1.id] + # + # # User starts the state machine. + # message.text = '/start' + # message.entities[0].length = len('/start') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.THIRSTY + # + # # The user is thirsty and wants to brew coffee. + # message.text = '/brew' + # message.entities[0].length = len('/brew') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.BREWING + # + # # Now a fallback command is issued + # message.text = '/eat' + # message.entities[0].length = len('/eat') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.THIRSTY + # + # def test_unknown_state_warning(self, dp, bot, user1, recwarn): + # handler = ConversationHandler( + # entry_points=[CommandHandler("start", lambda u, c: 1)], + # states={ + # 1: [TypeHandler(Update, lambda u, c: 69)], + # 2: [TypeHandler(Update, lambda u, c: -1)], + # }, + # fallbacks=self.fallbacks, + # name="xyz", + # ) + # dp.add_handler(handler) + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # dp.process_update(Update(update_id=0, message=message)) + # sleep(0.5) + # dp.process_update(Update(update_id=1, message=message)) + # sleep(0.5) + # assert len(recwarn) == 1 + # assert str(recwarn[0].message) == ( + # "Handler returned state 69 which is unknown to the ConversationHandler xyz." + # ) + # + # def test_conversation_handler_per_chat(self, dp, bot, user1, user2): + # handler = ConversationHandler( + # entry_points=self.entry_points, + # states=self.states, + # fallbacks=self.fallbacks, + # per_user=False, + # ) + # dp.add_handler(handler) + # + # # User one, starts the state machine. + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # dp.process_update(Update(update_id=0, message=message)) + # + # # The user is thirsty and wants to brew coffee. + # message.text = '/brew' + # message.entities[0].length = len('/brew') + # dp.process_update(Update(update_id=0, message=message)) + # + # # Let's now verify that for another user, who did not start yet, + # # the state will be changed because they are in the same group. + # message.from_user = user2 + # message.text = '/pourCoffee' + # message.entities[0].length = len('/pourCoffee') + # dp.process_update(Update(update_id=0, message=message)) + # + # assert handler.conversations[(self.group.id,)] == self.DRINKING + # + # def test_conversation_handler_per_user(self, dp, bot, user1): + # handler = ConversationHandler( + # entry_points=self.entry_points, + # states=self.states, + # fallbacks=self.fallbacks, + # per_chat=False, + # ) + # dp.add_handler(handler) + # + # # User one, starts the state machine. + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # dp.process_update(Update(update_id=0, message=message)) + # + # # The user is thirsty and wants to brew coffee. + # message.text = '/brew' + # message.entities[0].length = len('/brew') + # dp.process_update(Update(update_id=0, message=message)) + # + # # Let's now verify that for the same user in a different group, the state will still be + # # updated + # message.chat = self.second_group + # message.text = '/pourCoffee' + # message.entities[0].length = len('/pourCoffee') + # dp.process_update(Update(update_id=0, message=message)) + # + # assert handler.conversations[(user1.id,)] == self.DRINKING + # + # def test_conversation_handler_per_message(self, dp, bot, user1, user2): + # def entry(update, context): + # return 1 + # + # def one(update, context): + # return 2 + # + # def two(update, context): + # return ConversationHandler.END + # + # handler = ConversationHandler( + # entry_points=[CallbackQueryHandler(entry)], + # states={1: [CallbackQueryHandler(one)], 2: [CallbackQueryHandler(two)]}, + # fallbacks=[], + # per_message=True, + # ) + # dp.add_handler(handler) + # + # # User one, starts the state machine. + # message = Message( + # 0, None, self.group, from_user=user1, text='msg w/ inlinekeyboard', bot=bot + # ) + # + # cbq = CallbackQuery(0, user1, None, message=message, data='data', bot=bot) + # dp.process_update(Update(update_id=0, callback_query=cbq)) + # + # assert handler.conversations[(self.group.id, user1.id, message.message_id)] == 1 + # + # dp.process_update(Update(update_id=0, callback_query=cbq)) + # + # assert handler.conversations[(self.group.id, user1.id, message.message_id)] == 2 + # + # # Let's now verify that for a different user in the same group, the state will not be + # # updated + # cbq.from_user = user2 + # dp.process_update(Update(update_id=0, callback_query=cbq)) + # + # assert handler.conversations[(self.group.id, user1.id, message.message_id)] == 2 + # + # def test_end_on_first_message(self, dp, bot, user1): + # handler = ConversationHandler( + # entry_points=[CommandHandler('start', self.start_end)], states={}, fallbacks=[] + # ) + # dp.add_handler(handler) + # + # # User starts the state machine and immediately ends it. + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # dp.process_update(Update(update_id=0, message=message)) + # assert len(handler.conversations) == 0 + # + # def test_end_on_first_message_async(self, dp, bot, user1): + # handler = ConversationHandler( + # entry_points=[ + # CommandHandler( + # 'start', lambda update, context: dp.run_async(self.start_end, update, + # context) + # ) + # ], + # states={}, + # fallbacks=[], + # ) + # dp.add_handler(handler) + # + # # User starts the state machine with an async function that immediately ends the + # # conversation. Async results are resolved when the users state is queried next time. + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # dp.update_queue.put(Update(update_id=0, message=message)) + # sleep(0.1) + # # Assert that the Promise has been accepted as the new state + # assert len(handler.conversations) == 1 + # + # message.text = 'resolve promise pls' + # message.entities[0].length = len('resolve promise pls') + # dp.update_queue.put(Update(update_id=0, message=message)) + # sleep(0.1) + # # Assert that the Promise has been resolved and the conversation ended. + # assert len(handler.conversations) == 0 + # + # def test_end_on_first_message_async_handler(self, dp, bot, user1): + # handler = ConversationHandler( + # entry_points=[CommandHandler('start', self.start_end, run_async=True)], + # states={}, + # fallbacks=[], + # ) + # dp.add_handler(handler) + # + # # User starts the state machine with an async function that immediately ends the + # # conversation. Async results are resolved when the users state is queried next time. + # message = Message( + # 0, + # None, + # self.group, + # text='/start', + # from_user=user1, + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # dp.update_queue.put(Update(update_id=0, message=message)) + # sleep(0.1) + # # Assert that the Promise has been accepted as the new state + # assert len(handler.conversations) == 1 + # + # message.text = 'resolve promise pls' + # message.entities[0].length = len('resolve promise pls') + # dp.update_queue.put(Update(update_id=0, message=message)) + # sleep(0.1) + # # Assert that the Promise has been resolved and the conversation ended. + # assert len(handler.conversations) == 0 + # + # def test_none_on_first_message(self, dp, bot, user1): + # handler = ConversationHandler( + # entry_points=[CommandHandler('start', self.start_none)], states={}, fallbacks=[] + # ) + # dp.add_handler(handler) + # + # # User starts the state machine and a callback function returns None + # message = Message(0, None, self.group, from_user=user1, text='/start', bot=bot) + # dp.process_update(Update(update_id=0, message=message)) + # assert len(handler.conversations) == 0 + # + # def test_none_on_first_message_async(self, dp, bot, user1): + # handler = ConversationHandler( + # entry_points=[ + # CommandHandler( + # 'start', lambda update, context: dp.run_async(self.start_none, update, + # context) + # ) + # ], + # states={}, + # fallbacks=[], + # ) + # dp.add_handler(handler) + # + # # User starts the state machine with an async function that returns None + # # Async results are resolved when the users state is queried next time. + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # dp.update_queue.put(Update(update_id=0, message=message)) + # sleep(0.1) + # # Assert that the Promise has been accepted as the new state + # assert len(handler.conversations) == 1 + # + # message.text = 'resolve promise pls' + # dp.update_queue.put(Update(update_id=0, message=message)) + # sleep(0.1) + # # Assert that the Promise has been resolved and the conversation ended. + # assert len(handler.conversations) == 0 + # + # def test_none_on_first_message_async_handler(self, dp, bot, user1): + # handler = ConversationHandler( + # entry_points=[CommandHandler('start', self.start_none, run_async=True)], + # states={}, + # fallbacks=[], + # ) + # dp.add_handler(handler) + # + # # User starts the state machine with an async function that returns None + # # Async results are resolved when the users state is queried next time. + # message = Message( + # 0, + # None, + # self.group, + # text='/start', + # from_user=user1, + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # dp.update_queue.put(Update(update_id=0, message=message)) + # sleep(0.1) + # # Assert that the Promise has been accepted as the new state + # assert len(handler.conversations) == 1 + # + # message.text = 'resolve promise pls' + # dp.update_queue.put(Update(update_id=0, message=message)) + # sleep(0.1) + # # Assert that the Promise has been resolved and the conversation ended. + # assert len(handler.conversations) == 0 + # + # def test_per_chat_message_without_chat(self, bot, user1): + # handler = ConversationHandler( + # entry_points=[CommandHandler('start', self.start_end)], states={}, fallbacks=[] + # ) + # cbq = CallbackQuery(0, user1, None, None, bot=bot) + # update = Update(0, callback_query=cbq) + # assert not handler.check_update(update) + # + # def test_channel_message_without_chat(self, bot): + # handler = ConversationHandler( + # entry_points=[MessageHandler(filters.ALL, self.start_end)], states={}, fallbacks=[] + # ) + # message = Message(0, date=None, chat=Chat(0, Chat.CHANNEL, 'Misses Test'), bot=bot) + # + # update = Update(0, channel_post=message) + # assert not handler.check_update(update) + # + # update = Update(0, edited_channel_post=message) + # assert not handler.check_update(update) + # + # def test_all_update_types(self, dp, bot, user1): + # handler = ConversationHandler( + # entry_points=[CommandHandler('start', self.start_end)], states={}, fallbacks=[] + # ) + # message = Message(0, None, self.group, from_user=user1, text='ignore', bot=bot) + # callback_query = CallbackQuery(0, user1, None, message=message, data='data', bot=bot) + # chosen_inline_result = ChosenInlineResult(0, user1, 'query', bot=bot) + # inline_query = InlineQuery(0, user1, 'query', 0, bot=bot) + # pre_checkout_query = PreCheckoutQuery(0, user1, 'USD', 100, [], bot=bot) + # shipping_query = ShippingQuery(0, user1, [], None, bot=bot) + # assert not handler.check_update(Update(0, callback_query=callback_query)) + # assert not handler.check_update(Update(0, chosen_inline_result=chosen_inline_result)) + # assert not handler.check_update(Update(0, inline_query=inline_query)) + # assert not handler.check_update(Update(0, message=message)) + # assert not handler.check_update(Update(0, pre_checkout_query=pre_checkout_query)) + # assert not handler.check_update(Update(0, shipping_query=shipping_query)) + # + # def test_no_jobqueue_warning(self, dp, bot, user1, recwarn): + # handler = ConversationHandler( + # entry_points=self.entry_points, + # states=self.states, + # fallbacks=self.fallbacks, + # conversation_timeout=0.5, + # ) + # # save dp.job_queue in temp variable jqueue + # # and then set dp.job_queue to None. + # jqueue = dp.job_queue + # dp.job_queue = None + # dp.add_handler(handler) + # + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # + # dp.process_update(Update(update_id=0, message=message)) + # sleep(0.5) + # assert len(recwarn) == 1 + # assert ( + # str(recwarn[0].message) + # == "Ignoring `conversation_timeout` because the Dispatcher has no JobQueue." + # ) + # # now set dp.job_queue back to it's original value + # dp.job_queue = jqueue + # + # def test_schedule_job_exception(self, dp, bot, user1, monkeypatch, caplog): + # def mocked_run_once(*a, **kw): + # raise Exception("job error") + # + # class DictJB(JobQueue): + # pass + # + # dp.job_queue = DictJB() + # monkeypatch.setattr(dp.job_queue, "run_once", mocked_run_once) + # handler = ConversationHandler( + # entry_points=self.entry_points, + # states=self.states, + # fallbacks=self.fallbacks, + # conversation_timeout=100, + # ) + # dp.add_handler(handler) + # + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # + # with caplog.at_level(logging.ERROR): + # dp.process_update(Update(update_id=0, message=message)) + # sleep(0.5) + # assert len(caplog.records) == 2 + # assert ( + # caplog.records[0].message + # == "Failed to schedule timeout job due to the following exception:" + # ) + # assert caplog.records[1].message == "job error" + # + # def test_promise_exception(self, dp, bot, user1, caplog): + # """ + # Here we make sure that when a run_async handle raises an + # exception, the state isn't changed. + # """ + # + # def conv_entry(*a, **kw): + # return 1 + # + # def raise_error(*a, **kw): + # raise Exception("promise exception") + # + # handler = ConversationHandler( + # entry_points=[CommandHandler("start", conv_entry)], + # states={1: [MessageHandler(filters.ALL, raise_error)]}, + # fallbacks=self.fallbacks, + # run_async=True, + # ) + # dp.add_handler(handler) + # + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # # start the conversation + # dp.process_update(Update(update_id=0, message=message)) + # sleep(0.1) + # message.text = "error" + # dp.process_update(Update(update_id=0, message=message)) + # sleep(0.1) + # message.text = "resolve promise pls" + # caplog.clear() + # with caplog.at_level(logging.ERROR): + # dp.process_update(Update(update_id=0, message=message)) + # sleep(0.5) + # assert len(caplog.records) == 3 + # assert caplog.records[0].message == "Promise function raised exception" + # assert caplog.records[1].message == "promise exception" + # # assert res is old state + # assert handler.conversations.get((self.group.id, user1.id))[0] == 1 + # + # def test_conversation_timeout(self, dp, bot, user1): + # handler = ConversationHandler( + # entry_points=self.entry_points, + # states=self.states, + # fallbacks=self.fallbacks, + # conversation_timeout=0.5, + # ) + # dp.add_handler(handler) + # + # # Start state machine, then reach timeout + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # dp.process_update(Update(update_id=0, message=message)) + # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY + # sleep(0.75) + # assert handler.conversations.get((self.group.id, user1.id)) is None + # + # # Start state machine, do something, then reach timeout + # dp.process_update(Update(update_id=1, message=message)) + # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY + # message.text = '/brew' + # message.entities[0].length = len('/brew') + # dp.process_update(Update(update_id=2, message=message)) + # assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING + # sleep(0.7) + # assert handler.conversations.get((self.group.id, user1.id)) is None + # + # def test_timeout_not_triggered_on_conv_end_async(self, bot, dp, user1): + # def timeout(*a, **kw): + # self.test_flag = True + # + # self.states.update({ConversationHandler.TIMEOUT: [TypeHandler(Update, timeout)]}) + # handler = ConversationHandler( + # entry_points=self.entry_points, + # states=self.states, + # fallbacks=self.fallbacks, + # conversation_timeout=0.5, + # run_async=True, + # ) + # dp.add_handler(handler) + # + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # # start the conversation + # dp.process_update(Update(update_id=0, message=message)) + # sleep(0.1) + # message.text = '/brew' + # message.entities[0].length = len('/brew') + # dp.process_update(Update(update_id=1, message=message)) + # sleep(0.1) + # message.text = '/pourCoffee' + # message.entities[0].length = len('/pourCoffee') + # dp.process_update(Update(update_id=2, message=message)) + # sleep(0.1) + # message.text = '/end' + # message.entities[0].length = len('/end') + # dp.process_update(Update(update_id=3, message=message)) + # sleep(1) + # # assert timeout handler didn't got called + # assert self.test_flag is False + # + # def test_conversation_timeout_dispatcher_handler_stop(self, dp, bot, user1, recwarn): + # handler = ConversationHandler( + # entry_points=self.entry_points, + # states=self.states, + # fallbacks=self.fallbacks, + # conversation_timeout=0.5, + # ) + # + # def timeout(*args, **kwargs): + # raise DispatcherHandlerStop() + # + # self.states.update({ConversationHandler.TIMEOUT: [TypeHandler(Update, timeout)]}) + # dp.add_handler(handler) + # + # # Start state machine, then reach timeout + # message = Message( + # 0, + # None, + # self.group, + # text='/start', + # from_user=user1, + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # + # dp.process_update(Update(update_id=0, message=message)) + # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY + # sleep(0.9) + # assert handler.conversations.get((self.group.id, user1.id)) is None + # assert len(recwarn) == 1 + # assert str(recwarn[0].message).startswith('DispatcherHandlerStop in TIMEOUT') + # + # def test_conversation_handler_timeout_update_and_context(self, dp, bot, user1): + # context = None + # + # def start_callback(u, c): + # nonlocal context, self + # context = c + # return self.start(u, c) + # + # states = self.states + # timeout_handler = CommandHandler('start', None) + # states.update({ConversationHandler.TIMEOUT: [timeout_handler]}) + # handler = ConversationHandler( + # entry_points=[CommandHandler('start', start_callback)], + # states=states, + # fallbacks=self.fallbacks, + # conversation_timeout=0.5, + # ) + # dp.add_handler(handler) + # + # # Start state machine, then reach timeout + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # update = Update(update_id=0, message=message) + # + # def timeout_callback(u, c): + # nonlocal update, context, self + # self.is_timeout = True + # assert u is update + # assert c is context + # + # timeout_handler.callback = timeout_callback + # + # dp.process_update(update) + # sleep(0.7) + # assert handler.conversations.get((self.group.id, user1.id)) is None + # assert self.is_timeout + # + # @flaky(3, 1) + # def test_conversation_timeout_keeps_extending(self, dp, bot, user1): + # handler = ConversationHandler( + # entry_points=self.entry_points, + # states=self.states, + # fallbacks=self.fallbacks, + # conversation_timeout=0.5, + # ) + # dp.add_handler(handler) + # + # # Start state machine, wait, do something, verify the timeout is extended. + # # t=0 /start (timeout=.5) + # # t=.35 /brew (timeout=.85) + # # t=.5 original timeout + # # t=.6 /pourCoffee (timeout=1.1) + # # t=.85 second timeout + # # t=1.1 actual timeout + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # dp.process_update(Update(update_id=0, message=message)) + # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY + # sleep(0.35) # t=.35 + # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY + # message.text = '/brew' + # message.entities[0].length = len('/brew') + # dp.process_update(Update(update_id=0, message=message)) + # assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING + # sleep(0.25) # t=.6 + # assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING + # message.text = '/pourCoffee' + # message.entities[0].length = len('/pourCoffee') + # dp.process_update(Update(update_id=0, message=message)) + # assert handler.conversations.get((self.group.id, user1.id)) == self.DRINKING + # sleep(0.4) # t=1.0 + # assert handler.conversations.get((self.group.id, user1.id)) == self.DRINKING + # sleep(0.3) # t=1.3 + # assert handler.conversations.get((self.group.id, user1.id)) is None + # + # def test_conversation_timeout_two_users(self, dp, bot, user1, user2): + # handler = ConversationHandler( + # entry_points=self.entry_points, + # states=self.states, + # fallbacks=self.fallbacks, + # conversation_timeout=0.5, + # ) + # dp.add_handler(handler) + # + # # Start state machine, do something as second user, then reach timeout + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # dp.process_update(Update(update_id=0, message=message)) + # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY + # message.text = '/brew' + # message.entities[0].length = len('/brew') + # message.entities[0].length = len('/brew') + # message.from_user = user2 + # dp.process_update(Update(update_id=0, message=message)) + # assert handler.conversations.get((self.group.id, user2.id)) is None + # message.text = '/start' + # message.entities[0].length = len('/start') + # dp.process_update(Update(update_id=0, message=message)) + # assert handler.conversations.get((self.group.id, user2.id)) == self.THIRSTY + # sleep(0.7) + # assert handler.conversations.get((self.group.id, user1.id)) is None + # assert handler.conversations.get((self.group.id, user2.id)) is None + # + # def test_conversation_handler_timeout_state(self, dp, bot, user1): + # states = self.states + # states.update( + # { + # ConversationHandler.TIMEOUT: [ + # CommandHandler('brew', self.passout), + # MessageHandler(~filters.Regex('oding'), self.passout2), + # ] + # } + # ) + # handler = ConversationHandler( + # entry_points=self.entry_points, + # states=states, + # fallbacks=self.fallbacks, + # conversation_timeout=0.5, + # ) + # dp.add_handler(handler) + # + # # CommandHandler timeout + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # dp.process_update(Update(update_id=0, message=message)) + # message.text = '/brew' + # message.entities[0].length = len('/brew') + # dp.process_update(Update(update_id=0, message=message)) + # sleep(0.7) + # assert handler.conversations.get((self.group.id, user1.id)) is None + # assert self.is_timeout + # + # # MessageHandler timeout + # self.is_timeout = False + # message.text = '/start' + # message.entities[0].length = len('/start') + # dp.process_update(Update(update_id=1, message=message)) + # sleep(0.7) + # assert handler.conversations.get((self.group.id, user1.id)) is None + # assert self.is_timeout + # + # # Timeout but no valid handler + # self.is_timeout = False + # dp.process_update(Update(update_id=0, message=message)) + # message.text = '/brew' + # message.entities[0].length = len('/brew') + # dp.process_update(Update(update_id=0, message=message)) + # message.text = '/startCoding' + # message.entities[0].length = len('/startCoding') + # dp.process_update(Update(update_id=0, message=message)) + # sleep(0.7) + # assert handler.conversations.get((self.group.id, user1.id)) is None + # assert not self.is_timeout + # + # def test_conversation_handler_timeout_state_context(self, dp, bot, user1): + # states = self.states + # states.update( + # { + # ConversationHandler.TIMEOUT: [ + # CommandHandler('brew', self.passout_context), + # MessageHandler(~filters.Regex('oding'), self.passout2_context), + # ] + # } + # ) + # handler = ConversationHandler( + # entry_points=self.entry_points, + # states=states, + # fallbacks=self.fallbacks, + # conversation_timeout=0.5, + # ) + # dp.add_handler(handler) + # + # # CommandHandler timeout + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # dp.process_update(Update(update_id=0, message=message)) + # message.text = '/brew' + # message.entities[0].length = len('/brew') + # dp.process_update(Update(update_id=0, message=message)) + # sleep(0.7) + # assert handler.conversations.get((self.group.id, user1.id)) is None + # assert self.is_timeout + # + # # MessageHandler timeout + # self.is_timeout = False + # message.text = '/start' + # message.entities[0].length = len('/start') + # dp.process_update(Update(update_id=1, message=message)) + # sleep(0.7) + # assert handler.conversations.get((self.group.id, user1.id)) is None + # assert self.is_timeout + # + # # Timeout but no valid handler + # self.is_timeout = False + # dp.process_update(Update(update_id=0, message=message)) + # message.text = '/brew' + # message.entities[0].length = len('/brew') + # dp.process_update(Update(update_id=0, message=message)) + # message.text = '/startCoding' + # message.entities[0].length = len('/startCoding') + # dp.process_update(Update(update_id=0, message=message)) + # sleep(0.7) + # assert handler.conversations.get((self.group.id, user1.id)) is None + # assert not self.is_timeout + # + # def test_conversation_timeout_cancel_conflict(self, dp, bot, user1): + # # Start state machine, wait half the timeout, + # # then call a callback that takes more than the timeout + # # t=0 /start (timeout=.5) + # # t=.25 /slowbrew (sleep .5) + # # | t=.5 original timeout (should not execute) + # # | t=.75 /slowbrew returns (timeout=1.25) + # # t=1.25 timeout + # + # def slowbrew(_update, context): + # sleep(0.25) + # # Let's give to the original timeout a chance to execute + # sleep(0.25) + # # By returning None we do not override the conversation state so + # # we can see if the timeout has been executed + # + # states = self.states + # states[self.THIRSTY].append(CommandHandler('slowbrew', slowbrew)) + # states.update({ConversationHandler.TIMEOUT: [MessageHandler(None, self.passout2)]}) + # + # handler = ConversationHandler( + # entry_points=self.entry_points, + # states=states, + # fallbacks=self.fallbacks, + # conversation_timeout=0.5, + # ) + # dp.add_handler(handler) + # + # # CommandHandler timeout + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # bot=bot, + # ) + # dp.process_update(Update(update_id=0, message=message)) + # sleep(0.25) + # message.text = '/slowbrew' + # message.entities[0].length = len('/slowbrew') + # dp.process_update(Update(update_id=0, message=message)) + # assert handler.conversations.get((self.group.id, user1.id)) is not None + # assert not self.is_timeout + # + # sleep(0.7) + # assert handler.conversations.get((self.group.id, user1.id)) is None + # assert self.is_timeout + # + # def test_handlers_generate_warning(self, recwarn): + # """ + # this function tests all handler + per_* setting combinations. + # """ + # + # # the warning message action needs to be set to always, + # # otherwise only the first occurrence will be issued + # filterwarnings(action="always", category=PTBUserWarning) + # + # # this class doesn't do anything, its just not the Update class + # class NotUpdate: + # pass + # + # # this conversation handler has the string, string_regex, Pollhandler and TypeHandler + # # which should all generate a warning no matter the per_* setting. TypeHandler should + # # not when the class is Update + # ConversationHandler( + # entry_points=[StringCommandHandler("code", self.code)], + # states={ + # self.BREWING: [ + # StringRegexHandler("code", self.code), + # PollHandler(self.code), + # TypeHandler(NotUpdate, self.code), + # ], + # }, + # fallbacks=[TypeHandler(Update, self.code)], + # ) + # + # # these handlers should all raise a warning when per_chat is True + # ConversationHandler( + # entry_points=[ShippingQueryHandler(self.code)], + # states={ + # self.BREWING: [ + # InlineQueryHandler(self.code), + # PreCheckoutQueryHandler(self.code), + # PollAnswerHandler(self.code), + # ], + # }, + # fallbacks=[ChosenInlineResultHandler(self.code)], + # per_chat=True, + # ) + # + # # the CallbackQueryHandler should *not* raise when per_message is True, + # # but any other one should + # ConversationHandler( + # entry_points=[CallbackQueryHandler(self.code)], + # states={ + # self.BREWING: [CommandHandler("code", self.code)], + # }, + # fallbacks=[CallbackQueryHandler(self.code)], + # per_message=True, + # ) + # + # # the CallbackQueryHandler should raise when per_message is False + # ConversationHandler( + # entry_points=[CommandHandler("code", self.code)], + # states={ + # self.BREWING: [CommandHandler("code", self.code)], + # }, + # fallbacks=[CallbackQueryHandler(self.code)], + # per_message=False, + # ) + # + # # adding a nested conv to a conversation with timeout should warn + # child = ConversationHandler( + # entry_points=[CommandHandler("code", self.code)], + # states={ + # self.BREWING: [CommandHandler("code", self.code)], + # }, + # fallbacks=[CommandHandler("code", self.code)], + # ) + # + # ConversationHandler( + # entry_points=[CommandHandler("code", self.code)], + # states={ + # self.BREWING: [child], + # }, + # fallbacks=[CommandHandler("code", self.code)], + # conversation_timeout=42, + # ) + # + # # If per_message is True, per_chat should also be True, since msg ids are not unique + # ConversationHandler( + # entry_points=[CallbackQueryHandler(self.code, "code")], + # states={ + # self.BREWING: [CallbackQueryHandler(self.code, "code")], + # }, + # fallbacks=[CallbackQueryHandler(self.code, "code")], + # per_message=True, + # per_chat=False, + # ) + # + # # the overall number of handlers throwing a warning is 13 + # assert len(recwarn) == 13 + # # now we test the messages, they are raised in the order they are inserted + # # into the conversation handler + # assert str(recwarn[0].message) == ( + # "The `ConversationHandler` only handles updates of type `telegram.Update`. " + # "StringCommandHandler handles updates of type `str`." + # ) + # assert str(recwarn[1].message) == ( + # "The `ConversationHandler` only handles updates of type `telegram.Update`. " + # "StringRegexHandler handles updates of type `str`." + # ) + # assert str(recwarn[2].message) == ( + # "PollHandler will never trigger in a conversation since it has no information " + # "about the chat or the user who voted in it. Do you mean the " + # "`PollAnswerHandler`?" + # ) + # assert str(recwarn[3].message) == ( + # "The `ConversationHandler` only handles updates of type `telegram.Update`. " + # "The TypeHandler is set to handle NotUpdate." + # ) + # + # per_faq_link = ( + # " Read this FAQ entry to learn more about the per_* settings: " + # "https://github.com/python-telegram-bot/python-telegram-bot/wiki" + # "/Frequently-Asked-Questions#what-do-the-per_-settings-in-conversationhandler-do." + # ) + # + # assert str(recwarn[4].message) == ( + # "Updates handled by ShippingQueryHandler only have information about the user," + # " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link + # ) + # assert str(recwarn[5].message) == ( + # "Updates handled by ChosenInlineResultHandler only have information about the user," + # " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link + # ) + # assert str(recwarn[6].message) == ( + # "Updates handled by InlineQueryHandler only have information about the user," + # " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link + # ) + # assert str(recwarn[7].message) == ( + # "Updates handled by PreCheckoutQueryHandler only have information about the user," + # " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link + # ) + # assert str(recwarn[8].message) == ( + # "Updates handled by PollAnswerHandler only have information about the user," + # " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link + # ) + # assert str(recwarn[9].message) == ( + # "If 'per_message=True', all entry points, state handlers, and fallbacks must be " + # "'CallbackQueryHandler', since no other handlers have a message context." + # + per_faq_link + # ) + # assert str(recwarn[10].message) == ( + # "If 'per_message=False', 'CallbackQueryHandler' will not be tracked for " + # "every message." + per_faq_link + # ) + # assert str(recwarn[11].message) == ( + # "Using `conversation_timeout` with nested conversations is currently not " + # "supported. You can still try to use it, but it will likely behave differently" + # " from what you expect." + # ) + # + # assert str(recwarn[12].message) == ( + # "If 'per_message=True' is used, 'per_chat=True' should also be used, " + # "since message IDs are not globally unique." + # ) + # + # # this for loop checks if the correct stacklevel is used when generating the warning + # for warning in recwarn: + # assert warning.filename == __file__, "incorrect stacklevel!" + # + # def test_nested_conversation_handler(self, dp, bot, user1, user2): + # self.nested_states[self.DRINKING] = [ + # ConversationHandler( + # entry_points=self.drinking_entry_points, + # states=self.drinking_states, + # fallbacks=self.drinking_fallbacks, + # map_to_parent=self.drinking_map_to_parent, + # ) + # ] + # handler = ConversationHandler( + # entry_points=self.entry_points, states=self.nested_states, fallbacks=self.fallbacks + # ) + # dp.add_handler(handler) + # + # # User one, starts the state machine. + # message = Message( + # 0, + # None, + # self.group, + # from_user=user1, + # text='/start', + # bot=bot, + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # ) + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.THIRSTY + # + # # The user is thirsty and wants to brew coffee. + # message.text = '/brew' + # message.entities[0].length = len('/brew') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.BREWING + # + # # Lets pour some coffee. + # message.text = '/pourCoffee' + # message.entities[0].length = len('/pourCoffee') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.DRINKING + # + # # The user is holding the cup + # message.text = '/hold' + # message.entities[0].length = len('/hold') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.HOLDING + # + # # The user is sipping coffee + # message.text = '/sip' + # message.entities[0].length = len('/sip') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.SIPPING + # + # # The user is swallowing + # message.text = '/swallow' + # message.entities[0].length = len('/swallow') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.SWALLOWING + # + # # The user is holding the cup again + # message.text = '/hold' + # message.entities[0].length = len('/hold') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.HOLDING + # + # # The user wants to replenish the coffee supply + # message.text = '/replenish' + # message.entities[0].length = len('/replenish') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.REPLENISHING + # assert handler.conversations[(0, user1.id)] == self.BREWING + # + # # The user wants to drink their coffee again + # message.text = '/pourCoffee' + # message.entities[0].length = len('/pourCoffee') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.DRINKING + # + # # The user is now ready to start coding + # message.text = '/startCoding' + # message.entities[0].length = len('/startCoding') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.CODING + # + # # The user decides it's time to drink again + # message.text = '/drinkMore' + # message.entities[0].length = len('/drinkMore') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.DRINKING + # + # # The user is holding their cup + # message.text = '/hold' + # message.entities[0].length = len('/hold') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.HOLDING + # + # # The user wants to end with the drinking and go back to coding + # message.text = '/end' + # message.entities[0].length = len('/end') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.END + # assert handler.conversations[(0, user1.id)] == self.CODING + # + # # The user wants to drink once more + # message.text = '/drinkMore' + # message.entities[0].length = len('/drinkMore') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.DRINKING + # + # # The user wants to stop altogether + # message.text = '/stop' + # message.entities[0].length = len('/stop') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.STOPPING + # assert handler.conversations.get((0, user1.id)) is None + # + # def test_conversation_dispatcher_handler_stop(self, dp, bot, user1, user2): + # self.nested_states[self.DRINKING] = [ + # ConversationHandler( + # entry_points=self.drinking_entry_points, + # states=self.drinking_states, + # fallbacks=self.drinking_fallbacks, + # map_to_parent=self.drinking_map_to_parent, + # ) + # ] + # handler = ConversationHandler( + # entry_points=self.entry_points, states=self.nested_states, fallbacks=self.fallbacks + # ) + # + # def test_callback(u, c): + # self.test_flag = True + # + # dp.add_handler(handler) + # dp.add_handler(TypeHandler(Update, test_callback), group=1) + # self.raise_dp_handler_stop = True + # + # # User one, starts the state machine. + # message = Message( + # 0, + # None, + # self.group, + # text='/start', + # bot=bot, + # from_user=user1, + # entities=[ + # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + # ], + # ) + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.THIRSTY + # assert not self.test_flag + # + # # The user is thirsty and wants to brew coffee. + # message.text = '/brew' + # message.entities[0].length = len('/brew') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.BREWING + # assert not self.test_flag + # + # # Lets pour some coffee. + # message.text = '/pourCoffee' + # message.entities[0].length = len('/pourCoffee') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.DRINKING + # assert not self.test_flag + # + # # The user is holding the cup + # message.text = '/hold' + # message.entities[0].length = len('/hold') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.HOLDING + # assert not self.test_flag + # + # # The user is sipping coffee + # message.text = '/sip' + # message.entities[0].length = len('/sip') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.SIPPING + # assert not self.test_flag + # + # # The user is swallowing + # message.text = '/swallow' + # message.entities[0].length = len('/swallow') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.SWALLOWING + # assert not self.test_flag + # + # # The user is holding the cup again + # message.text = '/hold' + # message.entities[0].length = len('/hold') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.HOLDING + # assert not self.test_flag + # + # # The user wants to replenish the coffee supply + # message.text = '/replenish' + # message.entities[0].length = len('/replenish') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.REPLENISHING + # assert handler.conversations[(0, user1.id)] == self.BREWING + # assert not self.test_flag + # + # # The user wants to drink their coffee again + # message.text = '/pourCoffee' + # message.entities[0].length = len('/pourCoffee') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.DRINKING + # assert not self.test_flag + # + # # The user is now ready to start coding + # message.text = '/startCoding' + # message.entities[0].length = len('/startCoding') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.CODING + # assert not self.test_flag + # + # # The user decides it's time to drink again + # message.text = '/drinkMore' + # message.entities[0].length = len('/drinkMore') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.DRINKING + # assert not self.test_flag + # + # # The user is holding their cup + # message.text = '/hold' + # message.entities[0].length = len('/hold') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.HOLDING + # assert not self.test_flag + # + # # The user wants to end with the drinking and go back to coding + # message.text = '/end' + # message.entities[0].length = len('/end') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.END + # assert handler.conversations[(0, user1.id)] == self.CODING + # assert not self.test_flag + # + # # The user wants to drink once more + # message.text = '/drinkMore' + # message.entities[0].length = len('/drinkMore') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.DRINKING + # assert not self.test_flag + # + # # The user wants to stop altogether + # message.text = '/stop' + # message.entities[0].length = len('/stop') + # dp.process_update(Update(update_id=0, message=message)) + # assert self.current_state[user1.id] == self.STOPPING + # assert handler.conversations.get((0, user1.id)) is None + # assert not self.test_flag + # + # def test_conversation_handler_run_async_true(self, dp): + # conv_handler = ConversationHandler( + # entry_points=self.entry_points, + # states=self.states, + # fallbacks=self.fallbacks, + # run_async=True, + # ) + # + # all_handlers = conv_handler.entry_points + conv_handler.fallbacks + # for state_handlers in conv_handler.states.values(): + # all_handlers += state_handlers + # + # for handler in all_handlers: + # assert handler.run_async + # + # def test_conversation_handler_run_async_false(self, dp): + # conv_handler = ConversationHandler( + # entry_points=[CommandHandler('start', self.start_end, run_async=True)], + # states=self.states, + # fallbacks=self.fallbacks, + # run_async=False, + # ) + # + # for handler in conv_handler.entry_points: + # assert handler.run_async + # + # all_handlers = conv_handler.fallbacks + # for state_handlers in conv_handler.states.values(): + # all_handlers += state_handlers + # + # for handler in all_handlers: + # assert not handler.run_async.value From a59a3e01fbebff0a11fe9eb50d4de4f45cd56041 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Thu, 7 Apr 2022 22:07:59 +0200 Subject: [PATCH 11/25] Some CH tests --- tests/test_conversationhandler.py | 1644 +++++++++++++++++------------ 1 file changed, 963 insertions(+), 681 deletions(-) diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py index 82f76a489fd..1b8961a3510 100644 --- a/tests/test_conversationhandler.py +++ b/tests/test_conversationhandler.py @@ -17,17 +17,230 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. """Persistence of conversations is tested in test_basepersistence.py""" +import asyncio +import logging +from warnings import filterwarnings + import pytest -from telegram.ext import ConversationHandler +from telegram import ( + Chat, + Update, + Message, + MessageEntity, + User, + CallbackQuery, + InlineQuery, + ChosenInlineResult, + ShippingQuery, + PreCheckoutQuery, +) +from telegram.ext import ( + ConversationHandler, + CommandHandler, + ApplicationHandlerStop, + TypeHandler, + CallbackContext, + CallbackQueryHandler, + MessageHandler, + filters, + JobQueue, + StringCommandHandler, + StringRegexHandler, + PollHandler, + ShippingQueryHandler, + PreCheckoutQueryHandler, + InlineQueryHandler, + PollAnswerHandler, + ChosenInlineResultHandler, +) +from telegram.warnings import PTBUserWarning + + +@pytest.fixture(scope='class') +def user1(): + return User(first_name='Misses Test', id=123, is_bot=False) + + +@pytest.fixture(scope='class') +def user2(): + return User(first_name='Mister Test', id=124, is_bot=False) + + +def raise_dphs(func): + async def decorator(self, *args, **kwargs): + result = await func(self, *args, **kwargs) + if self.raise_dp_handler_stop: + raise ApplicationHandlerStop(result) + return result + + return decorator class TestConversationHandler: """Persistence of conversations is tested in test_basepersistence.py""" - raise_app_handler_stop = False + # State definitions + # At first we're thirsty. Then we brew coffee, we drink it + # and then we can start coding! + END, THIRSTY, BREWING, DRINKING, CODING = range(-1, 4) + + # Drinking state definitions (nested) + # At first we're holding the cup. Then we sip coffee, and last we swallow it + HOLDING, SIPPING, SWALLOWING, REPLENISHING, STOPPING = map(chr, range(ord('a'), ord('f'))) + + current_state, entry_points, states, fallbacks = None, None, None, None + group = Chat(0, Chat.GROUP) + second_group = Chat(1, Chat.GROUP) + + raise_dp_handler_stop = False test_flag = False + # Test related + @pytest.fixture(autouse=True) + def reset(self): + self.raise_dp_handler_stop = False + self.test_flag = False + self.current_state = {} + self.entry_points = [CommandHandler('start', self.start)] + self.states = { + self.THIRSTY: [CommandHandler('brew', self.brew), CommandHandler('wait', self.start)], + self.BREWING: [CommandHandler('pourCoffee', self.drink)], + self.DRINKING: [ + CommandHandler('startCoding', self.code), + CommandHandler('drinkMore', self.drink), + CommandHandler('end', self.end), + ], + self.CODING: [ + CommandHandler('keepCoding', self.code), + CommandHandler('gettingThirsty', self.start), + CommandHandler('drinkMore', self.drink), + ], + } + self.fallbacks = [CommandHandler('eat', self.start)] + self.is_timeout = False + + # for nesting tests + self.nested_states = { + self.THIRSTY: [CommandHandler('brew', self.brew), CommandHandler('wait', self.start)], + self.BREWING: [CommandHandler('pourCoffee', self.drink)], + self.CODING: [ + CommandHandler('keepCoding', self.code), + CommandHandler('gettingThirsty', self.start), + CommandHandler('drinkMore', self.drink), + ], + } + self.drinking_entry_points = [CommandHandler('hold', self.hold)] + self.drinking_states = { + self.HOLDING: [CommandHandler('sip', self.sip)], + self.SIPPING: [CommandHandler('swallow', self.swallow)], + self.SWALLOWING: [CommandHandler('hold', self.hold)], + } + self.drinking_fallbacks = [ + CommandHandler('replenish', self.replenish), + CommandHandler('stop', self.stop), + CommandHandler('end', self.end), + CommandHandler('startCoding', self.code), + CommandHandler('drinkMore', self.drink), + ] + self.drinking_entry_points.extend(self.drinking_fallbacks) + + # Map nested states to parent states: + self.drinking_map_to_parent = { + # Option 1 - Map a fictional internal state to an external parent state + self.REPLENISHING: self.BREWING, + # Option 2 - Map a fictional internal state to the END state on the parent + self.STOPPING: self.END, + # Option 3 - Map the internal END state to an external parent state + self.END: self.CODING, + # Option 4 - Map an external state to the same external parent state + self.CODING: self.CODING, + # Option 5 - Map an external state to the internal entry point + self.DRINKING: self.DRINKING, + } + + # State handlers + def _set_state(self, update, state): + self.current_state[update.message.from_user.id] = state + return state + + # Actions + @raise_dphs + async def start(self, update, context): + if isinstance(update, Update): + return self._set_state(update, self.THIRSTY) + return self._set_state(context.bot, self.THIRSTY) + + @raise_dphs + async def end(self, update, context): + return self._set_state(update, self.END) + + @raise_dphs + async def start_end(self, update, context): + return self._set_state(update, self.END) + + @raise_dphs + async def start_none(self, update, context): + return self._set_state(update, None) + + @raise_dphs + async def brew(self, update, context): + if isinstance(update, Update): + return self._set_state(update, self.BREWING) + return self._set_state(context.bot, self.BREWING) + + @raise_dphs + async def drink(self, update, context): + return self._set_state(update, self.DRINKING) + + @raise_dphs + async def code(self, update, context): + return self._set_state(update, self.CODING) + + @raise_dphs + async def passout(self, update, context): + assert update.message.text == '/brew' + assert isinstance(update, Update) + self.is_timeout = True + + @raise_dphs + async def passout2(self, update, context): + assert isinstance(update, Update) + self.is_timeout = True + + @raise_dphs + async def passout_context(self, update, context): + assert update.message.text == '/brew' + assert isinstance(context, CallbackContext) + self.is_timeout = True + + @raise_dphs + async def passout2_context(self, update, context): + assert isinstance(context, CallbackContext) + self.is_timeout = True + + # Drinking actions (nested) + + @raise_dphs + async def hold(self, update, context): + return self._set_state(update, self.HOLDING) + + @raise_dphs + async def sip(self, update, context): + return self._set_state(update, self.SIPPING) + + @raise_dphs + async def swallow(self, update, context): + return self._set_state(update, self.SWALLOWING) + + @raise_dphs + async def replenish(self, update, context): + return self._set_state(update, self.REPLENISHING) + + @raise_dphs + async def stop(self, update, context): + return self._set_state(update, self.STOPPING) + def test_slot_behaviour(self, mro_slots): handler = ConversationHandler(entry_points=[], states={}, fallbacks=[]) for attr in handler.__slots__: @@ -63,6 +276,169 @@ def test_init(self): assert ch.name == 'name' assert ch.allow_reentry == 'allow_reentry' + @pytest.mark.asyncio + async def test_handlers_generate_warning(self, recwarn): + """this function tests all handler + per_* setting combinations.""" + + # the warning message action needs to be set to always, + # otherwise only the first occurrence will be issued + filterwarnings(action="always", category=PTBUserWarning) + + # this class doesn't do anything, its just not the Update class + class NotUpdate: + pass + + # this conversation handler has the string, string_regex, Pollhandler and TypeHandler + # which should all generate a warning no matter the per_* setting. TypeHandler should + # not when the class is Update + ConversationHandler( + entry_points=[StringCommandHandler("code", self.code)], + states={ + self.BREWING: [ + StringRegexHandler("code", self.code), + PollHandler(self.code), + TypeHandler(NotUpdate, self.code), + ], + }, + fallbacks=[TypeHandler(Update, self.code)], + ) + + # these handlers should all raise a warning when per_chat is True + ConversationHandler( + entry_points=[ShippingQueryHandler(self.code)], + states={ + self.BREWING: [ + InlineQueryHandler(self.code), + PreCheckoutQueryHandler(self.code), + PollAnswerHandler(self.code), + ], + }, + fallbacks=[ChosenInlineResultHandler(self.code)], + per_chat=True, + ) + + # the CallbackQueryHandler should *not* raise when per_message is True, + # but any other one should + ConversationHandler( + entry_points=[CallbackQueryHandler(self.code)], + states={ + self.BREWING: [CommandHandler("code", self.code)], + }, + fallbacks=[CallbackQueryHandler(self.code)], + per_message=True, + ) + + # the CallbackQueryHandler should raise when per_message is False + ConversationHandler( + entry_points=[CommandHandler("code", self.code)], + states={ + self.BREWING: [CommandHandler("code", self.code)], + }, + fallbacks=[CallbackQueryHandler(self.code)], + per_message=False, + ) + + # adding a nested conv to a conversation with timeout should warn + child = ConversationHandler( + entry_points=[CommandHandler("code", self.code)], + states={ + self.BREWING: [CommandHandler("code", self.code)], + }, + fallbacks=[CommandHandler("code", self.code)], + ) + + ConversationHandler( + entry_points=[CommandHandler("code", self.code)], + states={ + self.BREWING: [child], + }, + fallbacks=[CommandHandler("code", self.code)], + conversation_timeout=42, + ) + + # If per_message is True, per_chat should also be True, since msg ids are not unique + ConversationHandler( + entry_points=[CallbackQueryHandler(self.code, "code")], + states={ + self.BREWING: [CallbackQueryHandler(self.code, "code")], + }, + fallbacks=[CallbackQueryHandler(self.code, "code")], + per_message=True, + per_chat=False, + ) + + # the overall number of handlers throwing a warning is 13 + assert len(recwarn) == 13 + # now we test the messages, they are raised in the order they are inserted + # into the conversation handler + assert str(recwarn[0].message) == ( + "The `ConversationHandler` only handles updates of type `telegram.Update`. " + "StringCommandHandler handles updates of type `str`." + ) + assert str(recwarn[1].message) == ( + "The `ConversationHandler` only handles updates of type `telegram.Update`. " + "StringRegexHandler handles updates of type `str`." + ) + assert str(recwarn[2].message) == ( + "PollHandler will never trigger in a conversation since it has no information " + "about the chat or the user who voted in it. Do you mean the " + "`PollAnswerHandler`?" + ) + assert str(recwarn[3].message) == ( + "The `ConversationHandler` only handles updates of type `telegram.Update`. " + "The TypeHandler is set to handle NotUpdate." + ) + + per_faq_link = ( + " Read this FAQ entry to learn more about the per_* settings: " + "https://github.com/python-telegram-bot/python-telegram-bot/wiki" + "/Frequently-Asked-Questions#what-do-the-per_-settings-in-conversationhandler-do." + ) + + assert str(recwarn[4].message) == ( + "Updates handled by ShippingQueryHandler only have information about the user," + " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link + ) + assert str(recwarn[5].message) == ( + "Updates handled by ChosenInlineResultHandler only have information about the user," + " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link + ) + assert str(recwarn[6].message) == ( + "Updates handled by InlineQueryHandler only have information about the user," + " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link + ) + assert str(recwarn[7].message) == ( + "Updates handled by PreCheckoutQueryHandler only have information about the user," + " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link + ) + assert str(recwarn[8].message) == ( + "Updates handled by PollAnswerHandler only have information about the user," + " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link + ) + assert str(recwarn[9].message) == ( + "If 'per_message=True', all entry points, state handlers, and fallbacks must be " + "'CallbackQueryHandler', since no other handlers have a message context." + + per_faq_link + ) + assert str(recwarn[10].message) == ( + "If 'per_message=False', 'CallbackQueryHandler' will not be tracked for " + "every message." + per_faq_link + ) + assert str(recwarn[11].message) == ( + "Using `conversation_timeout` with nested conversations is currently not " + "supported. You can still try to use it, but it will likely behave differently" + " from what you expect." + ) + + assert str(recwarn[12].message) == ( + "If 'per_message=True' is used, 'per_chat=True' should also be used, " + "since message IDs are not globally unique." + ) + + # this for loop checks if the correct stacklevel is used when generating the warning + for warning in recwarn: + assert warning.filename == __file__, "incorrect stacklevel!" + @pytest.mark.parametrize( 'attr', [ @@ -96,231 +472,273 @@ def test_per_all_false(self): per_message=False, ) - # def test_conversation_handler(self, dp, bot, user1, user2): - # handler = ConversationHandler( - # entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks - # ) - # dp.add_handler(handler) - # - # # User one, starts the state machine. - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # dp.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.THIRSTY - # - # # The user is thirsty and wants to brew coffee. - # message.text = '/brew' - # message.entities[0].length = len('/brew') - # dp.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.BREWING - # - # # Lets see if an invalid command makes sure, no state is changed. - # message.text = '/nothing' - # message.entities[0].length = len('/nothing') - # dp.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.BREWING - # - # # Lets see if the state machine still works by pouring coffee. - # message.text = '/pourCoffee' - # message.entities[0].length = len('/pourCoffee') - # dp.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.DRINKING - # - # # Let's now verify that for another user, who did not start yet, - # # the state has not been changed. - # message.from_user = user2 - # dp.process_update(Update(update_id=0, message=message)) - # with pytest.raises(KeyError): - # self.current_state[user2.id] - # - # def test_conversation_handler_end(self, caplog, dp, bot, user1): - # handler = ConversationHandler( - # entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks - # ) - # dp.add_handler(handler) - # - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # dp.process_update(Update(update_id=0, message=message)) - # message.text = '/brew' - # message.entities[0].length = len('/brew') - # dp.process_update(Update(update_id=0, message=message)) - # message.text = '/pourCoffee' - # message.entities[0].length = len('/pourCoffee') - # dp.process_update(Update(update_id=0, message=message)) - # message.text = '/end' - # message.entities[0].length = len('/end') - # caplog.clear() - # with caplog.at_level(logging.ERROR): - # dp.process_update(Update(update_id=0, message=message)) - # assert len(caplog.records) == 0 - # assert self.current_state[user1.id] == self.END - # with pytest.raises(KeyError): - # print(handler.conversations[(self.group.id, user1.id)]) - # - # def test_conversation_handler_fallback(self, dp, bot, user1, user2): - # handler = ConversationHandler( - # entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks - # ) - # dp.add_handler(handler) - # - # # first check if fallback will not trigger start when not started - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/eat', - # entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, - # length=len('/eat'))], - # bot=bot, - # ) - # dp.process_update(Update(update_id=0, message=message)) - # with pytest.raises(KeyError): - # self.current_state[user1.id] - # - # # User starts the state machine. - # message.text = '/start' - # message.entities[0].length = len('/start') - # dp.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.THIRSTY - # - # # The user is thirsty and wants to brew coffee. - # message.text = '/brew' - # message.entities[0].length = len('/brew') - # dp.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.BREWING - # - # # Now a fallback command is issued - # message.text = '/eat' - # message.entities[0].length = len('/eat') - # dp.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.THIRSTY - # - # def test_unknown_state_warning(self, dp, bot, user1, recwarn): - # handler = ConversationHandler( - # entry_points=[CommandHandler("start", lambda u, c: 1)], - # states={ - # 1: [TypeHandler(Update, lambda u, c: 69)], - # 2: [TypeHandler(Update, lambda u, c: -1)], - # }, - # fallbacks=self.fallbacks, - # name="xyz", - # ) - # dp.add_handler(handler) - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # dp.process_update(Update(update_id=0, message=message)) - # sleep(0.5) - # dp.process_update(Update(update_id=1, message=message)) - # sleep(0.5) - # assert len(recwarn) == 1 - # assert str(recwarn[0].message) == ( - # "Handler returned state 69 which is unknown to the ConversationHandler xyz." - # ) - # - # def test_conversation_handler_per_chat(self, dp, bot, user1, user2): - # handler = ConversationHandler( - # entry_points=self.entry_points, - # states=self.states, - # fallbacks=self.fallbacks, - # per_user=False, - # ) - # dp.add_handler(handler) - # - # # User one, starts the state machine. - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # dp.process_update(Update(update_id=0, message=message)) - # - # # The user is thirsty and wants to brew coffee. - # message.text = '/brew' - # message.entities[0].length = len('/brew') - # dp.process_update(Update(update_id=0, message=message)) - # - # # Let's now verify that for another user, who did not start yet, - # # the state will be changed because they are in the same group. - # message.from_user = user2 - # message.text = '/pourCoffee' - # message.entities[0].length = len('/pourCoffee') - # dp.process_update(Update(update_id=0, message=message)) - # - # assert handler.conversations[(self.group.id,)] == self.DRINKING - # - # def test_conversation_handler_per_user(self, dp, bot, user1): - # handler = ConversationHandler( - # entry_points=self.entry_points, - # states=self.states, - # fallbacks=self.fallbacks, - # per_chat=False, - # ) - # dp.add_handler(handler) - # - # # User one, starts the state machine. - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # dp.process_update(Update(update_id=0, message=message)) - # - # # The user is thirsty and wants to brew coffee. - # message.text = '/brew' - # message.entities[0].length = len('/brew') - # dp.process_update(Update(update_id=0, message=message)) - # - # # Let's now verify that for the same user in a different group, the state will still be - # # updated - # message.chat = self.second_group - # message.text = '/pourCoffee' - # message.entities[0].length = len('/pourCoffee') - # dp.process_update(Update(update_id=0, message=message)) - # - # assert handler.conversations[(user1.id,)] == self.DRINKING - # - # def test_conversation_handler_per_message(self, dp, bot, user1, user2): + @pytest.mark.asyncio + async def test_conversation_handler(self, app, bot, user1, user2): + handler = ConversationHandler( + entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks + ) + app.add_handler(handler) + + # User one, starts the state machine. + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + async with app: + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.THIRSTY + + # The user is thirsty and wants to brew coffee. + message.text = '/brew' + message.entities[0].length = len('/brew') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.BREWING + + # Lets see if an invalid command makes sure, no state is changed. + message.text = '/nothing' + message.entities[0].length = len('/nothing') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.BREWING + + # Lets see if the state machine still works by pouring coffee. + message.text = '/pourCoffee' + message.entities[0].length = len('/pourCoffee') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.DRINKING + + # Let's now verify that for another user, who did not start yet, + # the state has not been changed. + message.from_user = user2 + await app.process_update(Update(update_id=0, message=message)) + with pytest.raises(KeyError): + self.current_state[user2.id] + + @pytest.mark.asyncio + async def test_conversation_handler_end(self, caplog, app, bot, user1): + handler = ConversationHandler( + entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks + ) + app.add_handler(handler) + + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + + async with app: + await app.process_update(Update(update_id=0, message=message)) + message.text = '/brew' + message.entities[0].length = len('/brew') + await app.process_update(Update(update_id=0, message=message)) + message.text = '/pourCoffee' + message.entities[0].length = len('/pourCoffee') + await app.process_update(Update(update_id=0, message=message)) + message.text = '/end' + message.entities[0].length = len('/end') + caplog.clear() + with caplog.at_level(logging.ERROR): + await app.process_update(Update(update_id=0, message=message)) + assert len(caplog.records) == 0 + assert self.current_state[user1.id] == self.END + + # make sure that the conversation has ended by checking that the start command is + # accepted again + message.text = '/start' + message.entities[0].length = len('/start') + assert handler.check_update(Update(update_id=0, message=message)) + + @pytest.mark.asyncio + async def test_conversation_handler_fallback(self, app, bot, user1, user2): + handler = ConversationHandler( + entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks + ) + app.add_handler(handler) + + # first check if fallback will not trigger start when not started + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/eat', + entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/eat'))], + bot=bot, + ) + + async with app: + await app.process_update(Update(update_id=0, message=message)) + with pytest.raises(KeyError): + self.current_state[user1.id] + + # User starts the state machine. + message.text = '/start' + message.entities[0].length = len('/start') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.THIRSTY + + # The user is thirsty and wants to brew coffee. + message.text = '/brew' + message.entities[0].length = len('/brew') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.BREWING + + # Now a fallback command is issued + message.text = '/eat' + message.entities[0].length = len('/eat') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.THIRSTY + + @pytest.mark.asyncio + async def test_unknown_state_warning(self, app, bot, user1, recwarn): + def build_callback(state): + async def callback(_, __): + return state + + return callback + + handler = ConversationHandler( + entry_points=[CommandHandler("start", build_callback(1))], + states={ + 1: [TypeHandler(Update, build_callback(69))], + 2: [TypeHandler(Update, build_callback(42))], + }, + fallbacks=self.fallbacks, + name="xyz", + ) + app.add_handler(handler) + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + async with app: + await app.process_update(Update(update_id=0, message=message)) + try: + await app.process_update(Update(update_id=1, message=message)) + except Exception as exc: + print(exc) + raise exc + assert len(recwarn) == 1 + assert str(recwarn[0].message) == ( + "Handler returned state 69 which is unknown to the ConversationHandler xyz." + ) + + @pytest.mark.asyncio + async def test_conversation_handler_per_chat(self, app, bot, user1, user2): + handler = ConversationHandler( + entry_points=self.entry_points, + states=self.states, + fallbacks=self.fallbacks, + per_user=False, + ) + app.add_handler(handler) + + # User one, starts the state machine. + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + + async with app: + await app.process_update(Update(update_id=0, message=message)) + + # The user is thirsty and wants to brew coffee. + message.text = '/brew' + message.entities[0].length = len('/brew') + await app.process_update(Update(update_id=0, message=message)) + + # Let's now verify that for another user, who did not start yet, + # the state will be changed because they are in the same group. + message.from_user = user2 + message.text = '/pourCoffee' + message.entities[0].length = len('/pourCoffee') + await app.process_update(Update(update_id=0, message=message)) + + # Check that we're in the DRINKING state by checking that the corresponding command + # is accepted + message.from_user = user1 + message.text = '/startCoding' + message.entities[0].length = len('/startCoding') + assert handler.check_update(Update(update_id=0, message=message)) + message.from_user = user2 + assert handler.check_update(Update(update_id=0, message=message)) + + @pytest.mark.asyncio + async def test_conversation_handler_per_user(self, app, bot, user1): + handler = ConversationHandler( + entry_points=self.entry_points, + states=self.states, + fallbacks=self.fallbacks, + per_chat=False, + ) + app.add_handler(handler) + + # User one, starts the state machine. + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + async with app: + await app.process_update(Update(update_id=0, message=message)) + + # The user is thirsty and wants to brew coffee. + message.text = '/brew' + message.entities[0].length = len('/brew') + await app.process_update(Update(update_id=0, message=message)) + + # Let's now verify that for the same user in a different group, the state will still be + # updated + message.chat = self.second_group + message.text = '/pourCoffee' + message.entities[0].length = len('/pourCoffee') + await app.process_update(Update(update_id=0, message=message)) + + # Check that we're in the DRINKING state by checking that the corresponding command + # is accepted + message.chat = self.group + message.text = '/startCoding' + message.entities[0].length = len('/startCoding') + assert handler.check_update(Update(update_id=0, message=message)) + message.chat = self.second_group + assert handler.check_update(Update(update_id=0, message=message)) + + # TODO + # @pytest.mark.asyncio + # async def test_conversation_handler_per_message(self, app, bot, user1, user2): # def entry(update, context): # return 1 # @@ -336,62 +754,67 @@ def test_per_all_false(self): # fallbacks=[], # per_message=True, # ) - # dp.add_handler(handler) + # app.add_handler(handler) # # # User one, starts the state machine. # message = Message( # 0, None, self.group, from_user=user1, text='msg w/ inlinekeyboard', bot=bot # ) # - # cbq = CallbackQuery(0, user1, None, message=message, data='data', bot=bot) - # dp.process_update(Update(update_id=0, callback_query=cbq)) - # - # assert handler.conversations[(self.group.id, user1.id, message.message_id)] == 1 + # async with app: + # cbq = CallbackQuery(0, user1, None, message=message, data='data', bot=bot) + # await app.process_update(Update(update_id=0, callback_query=cbq)) # - # dp.process_update(Update(update_id=0, callback_query=cbq)) + # assert handler.conversations[(self.group.id, user1.id, message.message_id)] == 1 # - # assert handler.conversations[(self.group.id, user1.id, message.message_id)] == 2 + # await app.process_update(Update(update_id=0, callback_query=cbq)) # - # # Let's now verify that for a different user in the same group, the state will not be - # # updated - # cbq.from_user = user2 - # dp.process_update(Update(update_id=0, callback_query=cbq)) + # assert handler.conversations[(self.group.id, user1.id, message.message_id)] == 2 # - # assert handler.conversations[(self.group.id, user1.id, message.message_id)] == 2 - # - # def test_end_on_first_message(self, dp, bot, user1): - # handler = ConversationHandler( - # entry_points=[CommandHandler('start', self.start_end)], states={}, fallbacks=[] - # ) - # dp.add_handler(handler) - # - # # User starts the state machine and immediately ends it. - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # dp.process_update(Update(update_id=0, message=message)) - # assert len(handler.conversations) == 0 + # # Let's now verify that for a different user in the same group, the state will not be + # # updated + # cbq.from_user = user2 + # await app.process_update(Update(update_id=0, callback_query=cbq)) # - # def test_end_on_first_message_async(self, dp, bot, user1): + # assert handler.conversations[(self.group.id, user1.id, message.message_id)] == 2 + + @pytest.mark.asyncio + async def test_end_on_first_message(self, app, bot, user1): + handler = ConversationHandler( + entry_points=[CommandHandler('start', self.start_end)], states={}, fallbacks=[] + ) + app.add_handler(handler) + + # User starts the state machine and immediately ends it. + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + async with app: + await app.process_update(Update(update_id=0, message=message)) + assert handler.check_update(Update(update_id=0, message=message)) + + # TODO + # @pytest.mark.asyncio + # async def test_end_on_first_message_async(self, app, bot, user1): # handler = ConversationHandler( # entry_points=[ # CommandHandler( - # 'start', lambda update, context: dp.run_async(self.start_end, update, + # 'start', lambda update, context: app.run_async(self.start_end, update, # context) # ) # ], # states={}, # fallbacks=[], # ) - # dp.add_handler(handler) + # app.add_handler(handler) # # # User starts the state machine with an async function that immediately ends the # # conversation. Async results are resolved when the users state is queried next time. @@ -406,25 +829,26 @@ def test_per_all_false(self): # ], # bot=bot, # ) - # dp.update_queue.put(Update(update_id=0, message=message)) - # sleep(0.1) + # app.update_queue.put(Update(update_id=0, message=message)) + # await asyncio.sleep(0.1) # # Assert that the Promise has been accepted as the new state # assert len(handler.conversations) == 1 # # message.text = 'resolve promise pls' # message.entities[0].length = len('resolve promise pls') - # dp.update_queue.put(Update(update_id=0, message=message)) - # sleep(0.1) + # app.update_queue.put(Update(update_id=0, message=message)) + # await asyncio.sleep(0.1) # # Assert that the Promise has been resolved and the conversation ended. # assert len(handler.conversations) == 0 - # - # def test_end_on_first_message_async_handler(self, dp, bot, user1): + + # @pytest.mark.asyncio + # async def test_end_on_first_message_async_handler(self, app, bot, user1): # handler = ConversationHandler( # entry_points=[CommandHandler('start', self.start_end, run_async=True)], # states={}, # fallbacks=[], # ) - # dp.add_handler(handler) + # app.add_handler(handler) # # # User starts the state machine with an async function that immediately ends the # # conversation. Async results are resolved when the users state is queried next time. @@ -439,41 +863,43 @@ def test_per_all_false(self): # ], # bot=bot, # ) - # dp.update_queue.put(Update(update_id=0, message=message)) - # sleep(0.1) + # app.update_queue.put(Update(update_id=0, message=message)) + # await asyncio.sleep(0.1) # # Assert that the Promise has been accepted as the new state # assert len(handler.conversations) == 1 # # message.text = 'resolve promise pls' # message.entities[0].length = len('resolve promise pls') - # dp.update_queue.put(Update(update_id=0, message=message)) - # sleep(0.1) + # app.update_queue.put(Update(update_id=0, message=message)) + # await asyncio.sleep(0.1) # # Assert that the Promise has been resolved and the conversation ended. # assert len(handler.conversations) == 0 # - # def test_none_on_first_message(self, dp, bot, user1): + # @pytest.mark.asyncio + # async def test_none_on_first_message(self, app, bot, user1): # handler = ConversationHandler( # entry_points=[CommandHandler('start', self.start_none)], states={}, fallbacks=[] # ) - # dp.add_handler(handler) + # app.add_handler(handler) # # # User starts the state machine and a callback function returns None # message = Message(0, None, self.group, from_user=user1, text='/start', bot=bot) - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert len(handler.conversations) == 0 # - # def test_none_on_first_message_async(self, dp, bot, user1): + # @pytest.mark.asyncio + # async def test_none_on_first_message_async(self, app, bot, user1): # handler = ConversationHandler( # entry_points=[ # CommandHandler( - # 'start', lambda update, context: dp.run_async(self.start_none, update, + # 'start', lambda update, context: app.run_async(self.start_none, update, # context) # ) # ], # states={}, # fallbacks=[], # ) - # dp.add_handler(handler) + # app.add_handler(handler) # # # User starts the state machine with an async function that returns None # # Async results are resolved when the users state is queried next time. @@ -488,24 +914,25 @@ def test_per_all_false(self): # ], # bot=bot, # ) - # dp.update_queue.put(Update(update_id=0, message=message)) - # sleep(0.1) + # app.update_queue.put(Update(update_id=0, message=message)) + # await asyncio.sleep(0.1) # # Assert that the Promise has been accepted as the new state # assert len(handler.conversations) == 1 # # message.text = 'resolve promise pls' - # dp.update_queue.put(Update(update_id=0, message=message)) - # sleep(0.1) + # app.update_queue.put(Update(update_id=0, message=message)) + # await asyncio.sleep(0.1) # # Assert that the Promise has been resolved and the conversation ended. # assert len(handler.conversations) == 0 # - # def test_none_on_first_message_async_handler(self, dp, bot, user1): + # @pytest.mark.asyncio + # async def test_none_on_first_message_async_handler(self, app, bot, user1): # handler = ConversationHandler( # entry_points=[CommandHandler('start', self.start_none, run_async=True)], # states={}, # fallbacks=[], # ) - # dp.add_handler(handler) + # app.add_handler(handler) # # # User starts the state machine with an async function that returns None # # Async results are resolved when the users state is queried next time. @@ -520,129 +947,135 @@ def test_per_all_false(self): # ], # bot=bot, # ) - # dp.update_queue.put(Update(update_id=0, message=message)) - # sleep(0.1) + # app.update_queue.put(Update(update_id=0, message=message)) + # await asyncio.sleep(0.1) # # Assert that the Promise has been accepted as the new state # assert len(handler.conversations) == 1 # # message.text = 'resolve promise pls' - # dp.update_queue.put(Update(update_id=0, message=message)) - # sleep(0.1) + # app.update_queue.put(Update(update_id=0, message=message)) + # await asyncio.sleep(0.1) # # Assert that the Promise has been resolved and the conversation ended. # assert len(handler.conversations) == 0 - # - # def test_per_chat_message_without_chat(self, bot, user1): - # handler = ConversationHandler( - # entry_points=[CommandHandler('start', self.start_end)], states={}, fallbacks=[] - # ) - # cbq = CallbackQuery(0, user1, None, None, bot=bot) - # update = Update(0, callback_query=cbq) - # assert not handler.check_update(update) - # - # def test_channel_message_without_chat(self, bot): - # handler = ConversationHandler( - # entry_points=[MessageHandler(filters.ALL, self.start_end)], states={}, fallbacks=[] - # ) - # message = Message(0, date=None, chat=Chat(0, Chat.CHANNEL, 'Misses Test'), bot=bot) - # - # update = Update(0, channel_post=message) - # assert not handler.check_update(update) - # - # update = Update(0, edited_channel_post=message) - # assert not handler.check_update(update) - # - # def test_all_update_types(self, dp, bot, user1): - # handler = ConversationHandler( - # entry_points=[CommandHandler('start', self.start_end)], states={}, fallbacks=[] - # ) - # message = Message(0, None, self.group, from_user=user1, text='ignore', bot=bot) - # callback_query = CallbackQuery(0, user1, None, message=message, data='data', bot=bot) - # chosen_inline_result = ChosenInlineResult(0, user1, 'query', bot=bot) - # inline_query = InlineQuery(0, user1, 'query', 0, bot=bot) - # pre_checkout_query = PreCheckoutQuery(0, user1, 'USD', 100, [], bot=bot) - # shipping_query = ShippingQuery(0, user1, [], None, bot=bot) - # assert not handler.check_update(Update(0, callback_query=callback_query)) - # assert not handler.check_update(Update(0, chosen_inline_result=chosen_inline_result)) - # assert not handler.check_update(Update(0, inline_query=inline_query)) - # assert not handler.check_update(Update(0, message=message)) - # assert not handler.check_update(Update(0, pre_checkout_query=pre_checkout_query)) - # assert not handler.check_update(Update(0, shipping_query=shipping_query)) - # - # def test_no_jobqueue_warning(self, dp, bot, user1, recwarn): - # handler = ConversationHandler( - # entry_points=self.entry_points, - # states=self.states, - # fallbacks=self.fallbacks, - # conversation_timeout=0.5, - # ) - # # save dp.job_queue in temp variable jqueue - # # and then set dp.job_queue to None. - # jqueue = dp.job_queue - # dp.job_queue = None - # dp.add_handler(handler) - # - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # - # dp.process_update(Update(update_id=0, message=message)) - # sleep(0.5) - # assert len(recwarn) == 1 - # assert ( - # str(recwarn[0].message) - # == "Ignoring `conversation_timeout` because the Dispatcher has no JobQueue." - # ) - # # now set dp.job_queue back to it's original value - # dp.job_queue = jqueue - # - # def test_schedule_job_exception(self, dp, bot, user1, monkeypatch, caplog): - # def mocked_run_once(*a, **kw): - # raise Exception("job error") - # - # class DictJB(JobQueue): - # pass - # - # dp.job_queue = DictJB() - # monkeypatch.setattr(dp.job_queue, "run_once", mocked_run_once) - # handler = ConversationHandler( - # entry_points=self.entry_points, - # states=self.states, - # fallbacks=self.fallbacks, - # conversation_timeout=100, - # ) - # dp.add_handler(handler) - # - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # - # with caplog.at_level(logging.ERROR): - # dp.process_update(Update(update_id=0, message=message)) - # sleep(0.5) - # assert len(caplog.records) == 2 - # assert ( - # caplog.records[0].message - # == "Failed to schedule timeout job due to the following exception:" - # ) - # assert caplog.records[1].message == "job error" - # - # def test_promise_exception(self, dp, bot, user1, caplog): + + @pytest.mark.asyncio + async def test_per_chat_message_without_chat(self, bot, user1): + handler = ConversationHandler( + entry_points=[CommandHandler('start', self.start_end)], states={}, fallbacks=[] + ) + cbq = CallbackQuery(0, user1, None, None, bot=bot) + update = Update(0, callback_query=cbq) + assert not handler.check_update(update) + + @pytest.mark.asyncio + async def test_channel_message_without_chat(self, bot): + handler = ConversationHandler( + entry_points=[MessageHandler(filters.ALL, self.start_end)], states={}, fallbacks=[] + ) + message = Message(0, date=None, chat=Chat(0, Chat.CHANNEL, 'Misses Test'), bot=bot) + + update = Update(0, channel_post=message) + assert not handler.check_update(update) + + update = Update(0, edited_channel_post=message) + assert not handler.check_update(update) + + @pytest.mark.asyncio + async def test_all_update_types(self, app, bot, user1): + handler = ConversationHandler( + entry_points=[CommandHandler('start', self.start_end)], states={}, fallbacks=[] + ) + message = Message(0, None, self.group, from_user=user1, text='ignore', bot=bot) + callback_query = CallbackQuery(0, user1, None, message=message, data='data', bot=bot) + chosen_inline_result = ChosenInlineResult(0, user1, 'query', bot=bot) + inline_query = InlineQuery(0, user1, 'query', 0, bot=bot) + pre_checkout_query = PreCheckoutQuery(0, user1, 'USD', 100, [], bot=bot) + shipping_query = ShippingQuery(0, user1, [], None, bot=bot) + assert not handler.check_update(Update(0, callback_query=callback_query)) + assert not handler.check_update(Update(0, chosen_inline_result=chosen_inline_result)) + assert not handler.check_update(Update(0, inline_query=inline_query)) + assert not handler.check_update(Update(0, message=message)) + assert not handler.check_update(Update(0, pre_checkout_query=pre_checkout_query)) + assert not handler.check_update(Update(0, shipping_query=shipping_query)) + + @pytest.mark.asyncio + async def test_no_job_queue_warning(self, app, bot, user1, recwarn): + handler = ConversationHandler( + entry_points=self.entry_points, + states=self.states, + fallbacks=self.fallbacks, + conversation_timeout=0.5, + ) + # save app.job_queue in temp variable jqueue + # and then set app.job_queue to None. + jqueue = app.job_queue + app.job_queue = None + app.add_handler(handler) + + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + + async with app: + await app.process_update(Update(update_id=0, message=message)) + await asyncio.sleep(0.5) + assert len(recwarn) == 1 + assert ( + str(recwarn[0].message) + == "Ignoring `conversation_timeout` because the Application has no JobQueue." + ) + # now set app.job_queue back to it's original value + app.job_queue = jqueue + + @pytest.mark.asyncio + async def test_schedule_job_exception(self, app, bot, user1, monkeypatch, caplog): + def mocked_run_once(*a, **kw): + raise Exception("job error") + + class DictJB(JobQueue): + pass + + app.job_queue = DictJB() + monkeypatch.setattr(app.job_queue, "run_once", mocked_run_once) + handler = ConversationHandler( + entry_points=self.entry_points, + states=self.states, + fallbacks=self.fallbacks, + conversation_timeout=100, + ) + app.add_handler(handler) + + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + + async with app: + with caplog.at_level(logging.ERROR): + await app.process_update(Update(update_id=0, message=message)) + await asyncio.sleep(0.5) + + assert len(caplog.records) == 1 + assert caplog.records[0].message == "Failed to schedule timeout." + assert str(caplog.records[0].exc_info[1]) == "job error" + + # @pytest.mark.asyncio + # async def test_promise_exception(self, app, bot, user1, caplog): # """ # Here we make sure that when a run_async handle raises an # exception, the state isn't changed. @@ -660,7 +1093,7 @@ def test_per_all_false(self): # fallbacks=self.fallbacks, # run_async=True, # ) - # dp.add_handler(handler) + # app.add_handler(handler) # # message = Message( # 0, @@ -674,30 +1107,31 @@ def test_per_all_false(self): # bot=bot, # ) # # start the conversation - # dp.process_update(Update(update_id=0, message=message)) - # sleep(0.1) + # await app.process_update(Update(update_id=0, message=message)) + # await asyncio.sleep(0.1) # message.text = "error" - # dp.process_update(Update(update_id=0, message=message)) - # sleep(0.1) + # await app.process_update(Update(update_id=0, message=message)) + # await asyncio.sleep(0.1) # message.text = "resolve promise pls" # caplog.clear() # with caplog.at_level(logging.ERROR): - # dp.process_update(Update(update_id=0, message=message)) - # sleep(0.5) + # await app.process_update(Update(update_id=0, message=message)) + # await asyncio.sleep(0.5) # assert len(caplog.records) == 3 # assert caplog.records[0].message == "Promise function raised exception" # assert caplog.records[1].message == "promise exception" # # assert res is old state # assert handler.conversations.get((self.group.id, user1.id))[0] == 1 # - # def test_conversation_timeout(self, dp, bot, user1): + # @pytest.mark.asyncio + # async def test_conversation_timeout(self, app, bot, user1): # handler = ConversationHandler( # entry_points=self.entry_points, # states=self.states, # fallbacks=self.fallbacks, # conversation_timeout=0.5, # ) - # dp.add_handler(handler) + # app.add_handler(handler) # # # Start state machine, then reach timeout # message = Message( @@ -711,22 +1145,23 @@ def test_per_all_false(self): # ], # bot=bot, # ) - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY - # sleep(0.75) + # await asyncio.sleep(0.75) # assert handler.conversations.get((self.group.id, user1.id)) is None # # # Start state machine, do something, then reach timeout - # dp.process_update(Update(update_id=1, message=message)) + # await app.process_update(Update(update_id=1, message=message)) # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY # message.text = '/brew' # message.entities[0].length = len('/brew') - # dp.process_update(Update(update_id=2, message=message)) + # await app.process_update(Update(update_id=2, message=message)) # assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING - # sleep(0.7) + # await asyncio.sleep(0.7) # assert handler.conversations.get((self.group.id, user1.id)) is None # - # def test_timeout_not_triggered_on_conv_end_async(self, bot, dp, user1): + # @pytest.mark.asyncio + # async def test_timeout_not_triggered_on_conv_end_async(self, bot, app, user1): # def timeout(*a, **kw): # self.test_flag = True # @@ -738,7 +1173,7 @@ def test_per_all_false(self): # conversation_timeout=0.5, # run_async=True, # ) - # dp.add_handler(handler) + # app.add_handler(handler) # # message = Message( # 0, @@ -752,24 +1187,25 @@ def test_per_all_false(self): # bot=bot, # ) # # start the conversation - # dp.process_update(Update(update_id=0, message=message)) - # sleep(0.1) + # await app.process_update(Update(update_id=0, message=message)) + # await asyncio.sleep(0.1) # message.text = '/brew' # message.entities[0].length = len('/brew') - # dp.process_update(Update(update_id=1, message=message)) - # sleep(0.1) + # await app.process_update(Update(update_id=1, message=message)) + # await asyncio.sleep(0.1) # message.text = '/pourCoffee' # message.entities[0].length = len('/pourCoffee') - # dp.process_update(Update(update_id=2, message=message)) - # sleep(0.1) + # await app.process_update(Update(update_id=2, message=message)) + # await asyncio.sleep(0.1) # message.text = '/end' # message.entities[0].length = len('/end') - # dp.process_update(Update(update_id=3, message=message)) - # sleep(1) + # await app.process_update(Update(update_id=3, message=message)) + # await asyncio.sleep(1) # # assert timeout handler didn't got called # assert self.test_flag is False # - # def test_conversation_timeout_dispatcher_handler_stop(self, dp, bot, user1, recwarn): + # @pytest.mark.asyncio + # async def test_conversation_timeout_dispatcher_handler_stop(self, app, bot, user1, recwarn): # handler = ConversationHandler( # entry_points=self.entry_points, # states=self.states, @@ -781,7 +1217,7 @@ def test_per_all_false(self): # raise DispatcherHandlerStop() # # self.states.update({ConversationHandler.TIMEOUT: [TypeHandler(Update, timeout)]}) - # dp.add_handler(handler) + # app.add_handler(handler) # # # Start state machine, then reach timeout # message = Message( @@ -796,14 +1232,15 @@ def test_per_all_false(self): # bot=bot, # ) # - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY - # sleep(0.9) + # await asyncio.sleep(0.9) # assert handler.conversations.get((self.group.id, user1.id)) is None # assert len(recwarn) == 1 # assert str(recwarn[0].message).startswith('DispatcherHandlerStop in TIMEOUT') # - # def test_conversation_handler_timeout_update_and_context(self, dp, bot, user1): + # @pytest.mark.asyncio + # async def test_conversation_handler_timeout_update_and_context(self, app, bot, user1): # context = None # # def start_callback(u, c): @@ -820,7 +1257,7 @@ def test_per_all_false(self): # fallbacks=self.fallbacks, # conversation_timeout=0.5, # ) - # dp.add_handler(handler) + # app.add_handler(handler) # # # Start state machine, then reach timeout # message = Message( @@ -844,20 +1281,21 @@ def test_per_all_false(self): # # timeout_handler.callback = timeout_callback # - # dp.process_update(update) - # sleep(0.7) + # await app.process_update(update) + # await asyncio.sleep(0.7) # assert handler.conversations.get((self.group.id, user1.id)) is None # assert self.is_timeout # # @flaky(3, 1) - # def test_conversation_timeout_keeps_extending(self, dp, bot, user1): + # @pytest.mark.asyncio + # async def test_conversation_timeout_keeps_extending(self, app, bot, user1): # handler = ConversationHandler( # entry_points=self.entry_points, # states=self.states, # fallbacks=self.fallbacks, # conversation_timeout=0.5, # ) - # dp.add_handler(handler) + # app.add_handler(handler) # # # Start state machine, wait, do something, verify the timeout is extended. # # t=0 /start (timeout=.5) @@ -877,33 +1315,34 @@ def test_per_all_false(self): # ], # bot=bot, # ) - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY - # sleep(0.35) # t=.35 + # await asyncio.sleep(0.35) # t=.35 # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY # message.text = '/brew' # message.entities[0].length = len('/brew') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING - # sleep(0.25) # t=.6 + # await asyncio.sleep(0.25) # t=.6 # assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING # message.text = '/pourCoffee' # message.entities[0].length = len('/pourCoffee') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert handler.conversations.get((self.group.id, user1.id)) == self.DRINKING - # sleep(0.4) # t=1.0 + # await asyncio.sleep(0.4) # t=1.0 # assert handler.conversations.get((self.group.id, user1.id)) == self.DRINKING - # sleep(0.3) # t=1.3 + # await asyncio.sleep(0.3) # t=1.3 # assert handler.conversations.get((self.group.id, user1.id)) is None # - # def test_conversation_timeout_two_users(self, dp, bot, user1, user2): + # @pytest.mark.asyncio + # async def test_conversation_timeout_two_users(self, app, bot, user1, user2): # handler = ConversationHandler( # entry_points=self.entry_points, # states=self.states, # fallbacks=self.fallbacks, # conversation_timeout=0.5, # ) - # dp.add_handler(handler) + # app.add_handler(handler) # # # Start state machine, do something as second user, then reach timeout # message = Message( @@ -917,23 +1356,24 @@ def test_per_all_false(self): # ], # bot=bot, # ) - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY # message.text = '/brew' # message.entities[0].length = len('/brew') # message.entities[0].length = len('/brew') # message.from_user = user2 - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert handler.conversations.get((self.group.id, user2.id)) is None # message.text = '/start' # message.entities[0].length = len('/start') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert handler.conversations.get((self.group.id, user2.id)) == self.THIRSTY - # sleep(0.7) + # await asyncio.sleep(0.7) # assert handler.conversations.get((self.group.id, user1.id)) is None # assert handler.conversations.get((self.group.id, user2.id)) is None # - # def test_conversation_handler_timeout_state(self, dp, bot, user1): + # @pytest.mark.asyncio + # async def test_conversation_handler_timeout_state(self, app, bot, user1): # states = self.states # states.update( # { @@ -949,7 +1389,7 @@ def test_per_all_false(self): # fallbacks=self.fallbacks, # conversation_timeout=0.5, # ) - # dp.add_handler(handler) + # app.add_handler(handler) # # # CommandHandler timeout # message = Message( @@ -963,11 +1403,11 @@ def test_per_all_false(self): # ], # bot=bot, # ) - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # message.text = '/brew' # message.entities[0].length = len('/brew') - # dp.process_update(Update(update_id=0, message=message)) - # sleep(0.7) + # await app.process_update(Update(update_id=0, message=message)) + # await asyncio.sleep(0.7) # assert handler.conversations.get((self.group.id, user1.id)) is None # assert self.is_timeout # @@ -975,25 +1415,26 @@ def test_per_all_false(self): # self.is_timeout = False # message.text = '/start' # message.entities[0].length = len('/start') - # dp.process_update(Update(update_id=1, message=message)) - # sleep(0.7) + # await app.process_update(Update(update_id=1, message=message)) + # await asyncio.sleep(0.7) # assert handler.conversations.get((self.group.id, user1.id)) is None # assert self.is_timeout # # # Timeout but no valid handler # self.is_timeout = False - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # message.text = '/brew' # message.entities[0].length = len('/brew') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # message.text = '/startCoding' # message.entities[0].length = len('/startCoding') - # dp.process_update(Update(update_id=0, message=message)) - # sleep(0.7) + # await app.process_update(Update(update_id=0, message=message)) + # await asyncio.sleep(0.7) # assert handler.conversations.get((self.group.id, user1.id)) is None # assert not self.is_timeout # - # def test_conversation_handler_timeout_state_context(self, dp, bot, user1): + # @pytest.mark.asyncio + # async def test_conversation_handler_timeout_state_context(self, app, bot, user1): # states = self.states # states.update( # { @@ -1009,7 +1450,7 @@ def test_per_all_false(self): # fallbacks=self.fallbacks, # conversation_timeout=0.5, # ) - # dp.add_handler(handler) + # app.add_handler(handler) # # # CommandHandler timeout # message = Message( @@ -1023,11 +1464,11 @@ def test_per_all_false(self): # ], # bot=bot, # ) - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # message.text = '/brew' # message.entities[0].length = len('/brew') - # dp.process_update(Update(update_id=0, message=message)) - # sleep(0.7) + # await app.process_update(Update(update_id=0, message=message)) + # await asyncio.sleep(0.7) # assert handler.conversations.get((self.group.id, user1.id)) is None # assert self.is_timeout # @@ -1035,25 +1476,26 @@ def test_per_all_false(self): # self.is_timeout = False # message.text = '/start' # message.entities[0].length = len('/start') - # dp.process_update(Update(update_id=1, message=message)) - # sleep(0.7) + # await app.process_update(Update(update_id=1, message=message)) + # await asyncio.sleep(0.7) # assert handler.conversations.get((self.group.id, user1.id)) is None # assert self.is_timeout # # # Timeout but no valid handler # self.is_timeout = False - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # message.text = '/brew' # message.entities[0].length = len('/brew') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # message.text = '/startCoding' # message.entities[0].length = len('/startCoding') - # dp.process_update(Update(update_id=0, message=message)) - # sleep(0.7) + # await app.process_update(Update(update_id=0, message=message)) + # await asyncio.sleep(0.7) # assert handler.conversations.get((self.group.id, user1.id)) is None # assert not self.is_timeout # - # def test_conversation_timeout_cancel_conflict(self, dp, bot, user1): + # @pytest.mark.asyncio + # async def test_conversation_timeout_cancel_conflict(self, app, bot, user1): # # Start state machine, wait half the timeout, # # then call a callback that takes more than the timeout # # t=0 /start (timeout=.5) @@ -1063,9 +1505,9 @@ def test_per_all_false(self): # # t=1.25 timeout # # def slowbrew(_update, context): - # sleep(0.25) + # await asyncio.sleep(0.25) # # Let's give to the original timeout a chance to execute - # sleep(0.25) + # await asyncio.sleep(0.25) # # By returning None we do not override the conversation state so # # we can see if the timeout has been executed # @@ -1079,7 +1521,7 @@ def test_per_all_false(self): # fallbacks=self.fallbacks, # conversation_timeout=0.5, # ) - # dp.add_handler(handler) + # app.add_handler(handler) # # # CommandHandler timeout # message = Message( @@ -1093,183 +1535,20 @@ def test_per_all_false(self): # ], # bot=bot, # ) - # dp.process_update(Update(update_id=0, message=message)) - # sleep(0.25) + # await app.process_update(Update(update_id=0, message=message)) + # await asyncio.sleep(0.25) # message.text = '/slowbrew' # message.entities[0].length = len('/slowbrew') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert handler.conversations.get((self.group.id, user1.id)) is not None # assert not self.is_timeout # - # sleep(0.7) + # await asyncio.sleep(0.7) # assert handler.conversations.get((self.group.id, user1.id)) is None # assert self.is_timeout # - # def test_handlers_generate_warning(self, recwarn): - # """ - # this function tests all handler + per_* setting combinations. - # """ - # - # # the warning message action needs to be set to always, - # # otherwise only the first occurrence will be issued - # filterwarnings(action="always", category=PTBUserWarning) - # - # # this class doesn't do anything, its just not the Update class - # class NotUpdate: - # pass - # - # # this conversation handler has the string, string_regex, Pollhandler and TypeHandler - # # which should all generate a warning no matter the per_* setting. TypeHandler should - # # not when the class is Update - # ConversationHandler( - # entry_points=[StringCommandHandler("code", self.code)], - # states={ - # self.BREWING: [ - # StringRegexHandler("code", self.code), - # PollHandler(self.code), - # TypeHandler(NotUpdate, self.code), - # ], - # }, - # fallbacks=[TypeHandler(Update, self.code)], - # ) - # - # # these handlers should all raise a warning when per_chat is True - # ConversationHandler( - # entry_points=[ShippingQueryHandler(self.code)], - # states={ - # self.BREWING: [ - # InlineQueryHandler(self.code), - # PreCheckoutQueryHandler(self.code), - # PollAnswerHandler(self.code), - # ], - # }, - # fallbacks=[ChosenInlineResultHandler(self.code)], - # per_chat=True, - # ) - # - # # the CallbackQueryHandler should *not* raise when per_message is True, - # # but any other one should - # ConversationHandler( - # entry_points=[CallbackQueryHandler(self.code)], - # states={ - # self.BREWING: [CommandHandler("code", self.code)], - # }, - # fallbacks=[CallbackQueryHandler(self.code)], - # per_message=True, - # ) - # - # # the CallbackQueryHandler should raise when per_message is False - # ConversationHandler( - # entry_points=[CommandHandler("code", self.code)], - # states={ - # self.BREWING: [CommandHandler("code", self.code)], - # }, - # fallbacks=[CallbackQueryHandler(self.code)], - # per_message=False, - # ) - # - # # adding a nested conv to a conversation with timeout should warn - # child = ConversationHandler( - # entry_points=[CommandHandler("code", self.code)], - # states={ - # self.BREWING: [CommandHandler("code", self.code)], - # }, - # fallbacks=[CommandHandler("code", self.code)], - # ) - # - # ConversationHandler( - # entry_points=[CommandHandler("code", self.code)], - # states={ - # self.BREWING: [child], - # }, - # fallbacks=[CommandHandler("code", self.code)], - # conversation_timeout=42, - # ) - # - # # If per_message is True, per_chat should also be True, since msg ids are not unique - # ConversationHandler( - # entry_points=[CallbackQueryHandler(self.code, "code")], - # states={ - # self.BREWING: [CallbackQueryHandler(self.code, "code")], - # }, - # fallbacks=[CallbackQueryHandler(self.code, "code")], - # per_message=True, - # per_chat=False, - # ) - # - # # the overall number of handlers throwing a warning is 13 - # assert len(recwarn) == 13 - # # now we test the messages, they are raised in the order they are inserted - # # into the conversation handler - # assert str(recwarn[0].message) == ( - # "The `ConversationHandler` only handles updates of type `telegram.Update`. " - # "StringCommandHandler handles updates of type `str`." - # ) - # assert str(recwarn[1].message) == ( - # "The `ConversationHandler` only handles updates of type `telegram.Update`. " - # "StringRegexHandler handles updates of type `str`." - # ) - # assert str(recwarn[2].message) == ( - # "PollHandler will never trigger in a conversation since it has no information " - # "about the chat or the user who voted in it. Do you mean the " - # "`PollAnswerHandler`?" - # ) - # assert str(recwarn[3].message) == ( - # "The `ConversationHandler` only handles updates of type `telegram.Update`. " - # "The TypeHandler is set to handle NotUpdate." - # ) - # - # per_faq_link = ( - # " Read this FAQ entry to learn more about the per_* settings: " - # "https://github.com/python-telegram-bot/python-telegram-bot/wiki" - # "/Frequently-Asked-Questions#what-do-the-per_-settings-in-conversationhandler-do." - # ) - # - # assert str(recwarn[4].message) == ( - # "Updates handled by ShippingQueryHandler only have information about the user," - # " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link - # ) - # assert str(recwarn[5].message) == ( - # "Updates handled by ChosenInlineResultHandler only have information about the user," - # " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link - # ) - # assert str(recwarn[6].message) == ( - # "Updates handled by InlineQueryHandler only have information about the user," - # " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link - # ) - # assert str(recwarn[7].message) == ( - # "Updates handled by PreCheckoutQueryHandler only have information about the user," - # " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link - # ) - # assert str(recwarn[8].message) == ( - # "Updates handled by PollAnswerHandler only have information about the user," - # " so this handler won't ever be triggered if `per_chat=True`." + per_faq_link - # ) - # assert str(recwarn[9].message) == ( - # "If 'per_message=True', all entry points, state handlers, and fallbacks must be " - # "'CallbackQueryHandler', since no other handlers have a message context." - # + per_faq_link - # ) - # assert str(recwarn[10].message) == ( - # "If 'per_message=False', 'CallbackQueryHandler' will not be tracked for " - # "every message." + per_faq_link - # ) - # assert str(recwarn[11].message) == ( - # "Using `conversation_timeout` with nested conversations is currently not " - # "supported. You can still try to use it, but it will likely behave differently" - # " from what you expect." - # ) - # - # assert str(recwarn[12].message) == ( - # "If 'per_message=True' is used, 'per_chat=True' should also be used, " - # "since message IDs are not globally unique." - # ) - # - # # this for loop checks if the correct stacklevel is used when generating the warning - # for warning in recwarn: - # assert warning.filename == __file__, "incorrect stacklevel!" - # - # def test_nested_conversation_handler(self, dp, bot, user1, user2): + # @pytest.mark.asyncio + # async def test_nested_conversation_handler(self, app, bot, user1, user2): # self.nested_states[self.DRINKING] = [ # ConversationHandler( # entry_points=self.drinking_entry_points, @@ -1281,7 +1560,7 @@ def test_per_all_false(self): # handler = ConversationHandler( # entry_points=self.entry_points, states=self.nested_states, fallbacks=self.fallbacks # ) - # dp.add_handler(handler) + # app.add_handler(handler) # # # User one, starts the state machine. # message = Message( @@ -1295,97 +1574,98 @@ def test_per_all_false(self): # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) # ], # ) - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.THIRSTY # # # The user is thirsty and wants to brew coffee. # message.text = '/brew' # message.entities[0].length = len('/brew') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.BREWING # # # Lets pour some coffee. # message.text = '/pourCoffee' # message.entities[0].length = len('/pourCoffee') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.DRINKING # # # The user is holding the cup # message.text = '/hold' # message.entities[0].length = len('/hold') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.HOLDING # # # The user is sipping coffee # message.text = '/sip' # message.entities[0].length = len('/sip') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.SIPPING # # # The user is swallowing # message.text = '/swallow' # message.entities[0].length = len('/swallow') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.SWALLOWING # # # The user is holding the cup again # message.text = '/hold' # message.entities[0].length = len('/hold') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.HOLDING # # # The user wants to replenish the coffee supply # message.text = '/replenish' # message.entities[0].length = len('/replenish') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.REPLENISHING # assert handler.conversations[(0, user1.id)] == self.BREWING # # # The user wants to drink their coffee again # message.text = '/pourCoffee' # message.entities[0].length = len('/pourCoffee') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.DRINKING # # # The user is now ready to start coding # message.text = '/startCoding' # message.entities[0].length = len('/startCoding') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.CODING # # # The user decides it's time to drink again # message.text = '/drinkMore' # message.entities[0].length = len('/drinkMore') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.DRINKING # # # The user is holding their cup # message.text = '/hold' # message.entities[0].length = len('/hold') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.HOLDING # # # The user wants to end with the drinking and go back to coding # message.text = '/end' # message.entities[0].length = len('/end') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.END # assert handler.conversations[(0, user1.id)] == self.CODING # # # The user wants to drink once more # message.text = '/drinkMore' # message.entities[0].length = len('/drinkMore') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.DRINKING # # # The user wants to stop altogether # message.text = '/stop' # message.entities[0].length = len('/stop') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.STOPPING # assert handler.conversations.get((0, user1.id)) is None # - # def test_conversation_dispatcher_handler_stop(self, dp, bot, user1, user2): + # @pytest.mark.asyncio + # async def test_conversation_dispatcher_handler_stop(self, app, bot, user1, user2): # self.nested_states[self.DRINKING] = [ # ConversationHandler( # entry_points=self.drinking_entry_points, @@ -1401,8 +1681,8 @@ def test_per_all_false(self): # def test_callback(u, c): # self.test_flag = True # - # dp.add_handler(handler) - # dp.add_handler(TypeHandler(Update, test_callback), group=1) + # app.add_handler(handler) + # app.add_handler(TypeHandler(Update, test_callback), group=1) # self.raise_dp_handler_stop = True # # # User one, starts the state machine. @@ -1417,56 +1697,56 @@ def test_per_all_false(self): # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) # ], # ) - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.THIRSTY # assert not self.test_flag # # # The user is thirsty and wants to brew coffee. # message.text = '/brew' # message.entities[0].length = len('/brew') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.BREWING # assert not self.test_flag # # # Lets pour some coffee. # message.text = '/pourCoffee' # message.entities[0].length = len('/pourCoffee') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.DRINKING # assert not self.test_flag # # # The user is holding the cup # message.text = '/hold' # message.entities[0].length = len('/hold') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.HOLDING # assert not self.test_flag # # # The user is sipping coffee # message.text = '/sip' # message.entities[0].length = len('/sip') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.SIPPING # assert not self.test_flag # # # The user is swallowing # message.text = '/swallow' # message.entities[0].length = len('/swallow') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.SWALLOWING # assert not self.test_flag # # # The user is holding the cup again # message.text = '/hold' # message.entities[0].length = len('/hold') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.HOLDING # assert not self.test_flag # # # The user wants to replenish the coffee supply # message.text = '/replenish' # message.entities[0].length = len('/replenish') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.REPLENISHING # assert handler.conversations[(0, user1.id)] == self.BREWING # assert not self.test_flag @@ -1474,35 +1754,35 @@ def test_per_all_false(self): # # The user wants to drink their coffee again # message.text = '/pourCoffee' # message.entities[0].length = len('/pourCoffee') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.DRINKING # assert not self.test_flag # # # The user is now ready to start coding # message.text = '/startCoding' # message.entities[0].length = len('/startCoding') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.CODING # assert not self.test_flag # # # The user decides it's time to drink again # message.text = '/drinkMore' # message.entities[0].length = len('/drinkMore') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.DRINKING # assert not self.test_flag # # # The user is holding their cup # message.text = '/hold' # message.entities[0].length = len('/hold') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.HOLDING # assert not self.test_flag # # # The user wants to end with the drinking and go back to coding # message.text = '/end' # message.entities[0].length = len('/end') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.END # assert handler.conversations[(0, user1.id)] == self.CODING # assert not self.test_flag @@ -1510,19 +1790,20 @@ def test_per_all_false(self): # # The user wants to drink once more # message.text = '/drinkMore' # message.entities[0].length = len('/drinkMore') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.DRINKING # assert not self.test_flag # # # The user wants to stop altogether # message.text = '/stop' # message.entities[0].length = len('/stop') - # dp.process_update(Update(update_id=0, message=message)) + # await app.process_update(Update(update_id=0, message=message)) # assert self.current_state[user1.id] == self.STOPPING # assert handler.conversations.get((0, user1.id)) is None # assert not self.test_flag # - # def test_conversation_handler_run_async_true(self, dp): + # @pytest.mark.asyncio + # async def test_conversation_handler_run_async_true(self, app): # conv_handler = ConversationHandler( # entry_points=self.entry_points, # states=self.states, @@ -1537,7 +1818,8 @@ def test_per_all_false(self): # for handler in all_handlers: # assert handler.run_async # - # def test_conversation_handler_run_async_false(self, dp): + # @pytest.mark.asyncio + # async def test_conversation_handler_run_async_false(self, app): # conv_handler = ConversationHandler( # entry_points=[CommandHandler('start', self.start_end, run_async=True)], # states=self.states, From 2c149da77ace7053da27f0ffe7bbd9895e86c0f9 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Sun, 10 Apr 2022 02:18:25 +0530 Subject: [PATCH 12/25] re-add test_slot_behaviours --- telegram/ext/_utils/trackingdict.py | 2 +- tests/test_applicationbuilder.py | 5 +++++ tests/test_basepersistence.py | 1 - tests/test_bot.py | 10 ---------- tests/test_callbackcontext.py | 2 -- tests/test_chat.py | 5 +++++ tests/test_chatjoinrequest.py | 2 +- tests/test_chatjoinrequesthandler.py | 2 +- tests/test_chatphoto.py | 5 +++++ tests/test_document.py | 5 +++++ tests/test_error.py | 4 ++-- tests/test_inlinequery.py | 5 +++++ tests/test_message.py | 5 +++++ tests/test_photo.py | 5 +++++ tests/test_precheckoutquery.py | 6 ++++++ tests/test_replykeyboardmarkup.py | 6 ++++++ tests/test_requestdata.py | 5 +++++ tests/test_requestparameter.py | 6 ++++++ tests/test_shippingquery.py | 6 ++++++ tests/test_trackingdict.py | 5 +++++ tests/test_user.py | 5 +++++ tests/test_video.py | 5 +++++ tests/test_videonote.py | 5 +++++ tests/test_voice.py | 5 +++++ 24 files changed, 94 insertions(+), 18 deletions(-) diff --git a/telegram/ext/_utils/trackingdict.py b/telegram/ext/_utils/trackingdict.py index 314ab219d06..34580ab7010 100644 --- a/telegram/ext/_utils/trackingdict.py +++ b/telegram/ext/_utils/trackingdict.py @@ -57,7 +57,7 @@ class TrackingDict(UserDict, Generic[_KT, _VT]): DELETED: ClassVar = object() """Special marker indicating that an entry was deleted.""" - __slots__ = ('_data', '_write_access_keys') + __slots__ = ('_write_access_keys',) def __init__(self) -> None: super().__init__() diff --git a/tests/test_applicationbuilder.py b/tests/test_applicationbuilder.py index 6ee13d1605d..3b0c3a09248 100644 --- a/tests/test_applicationbuilder.py +++ b/tests/test_applicationbuilder.py @@ -44,6 +44,11 @@ def builder(): class TestApplicationBuilder: + def test_slot_behaviour(self, builder, mro_slots): + for attr in builder.__slots__: + assert getattr(builder, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(builder)) == len(set(mro_slots(builder))), "duplicate slot" + def test_build_without_token(self, builder): with pytest.raises(RuntimeError, match='No bot token was set.'): builder.build() diff --git a/tests/test_basepersistence.py b/tests/test_basepersistence.py index 176eda7f2d4..201d45dd34a 100644 --- a/tests/test_basepersistence.py +++ b/tests/test_basepersistence.py @@ -348,7 +348,6 @@ def test_slot_behaviour(self, mro_slots): assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" # We're interested in BasePersistence, not in the implementation slots = mro_slots(inst, only_parents=True) - print(slots) assert len(slots) == len(set(slots)), "duplicate slot" @pytest.mark.parametrize('bot_data', (True, False)) diff --git a/tests/test_bot.py b/tests/test_bot.py index 23bbdfe1f58..9711432d181 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -142,16 +142,6 @@ def inline_results(): ) -@pytest.fixture(scope='function') -@pytest.mark.asyncio -async def inst(request, bot_info, default_bot): - if request.param == 'bot': - async with Bot(bot_info['token']) as _bot: - yield _bot - else: - yield default_bot - - class TestBot: """ Most are executed on tg.ext.ExtBot, as that class only extends the functionality of tg.bot diff --git a/tests/test_callbackcontext.py b/tests/test_callbackcontext.py index 70b2f0a0492..735b694c181 100644 --- a/tests/test_callbackcontext.py +++ b/tests/test_callbackcontext.py @@ -44,8 +44,6 @@ def test_slot_behaviour(self, app, mro_slots, recwarn): assert getattr(c, attr, 'err') != 'err', f"got extra slot '{attr}'" assert not c.__dict__, f"got missing slot(s): {c.__dict__}" assert len(mro_slots(c)) == len(set(mro_slots(c))), "duplicate slot" - c.args = c.args - assert len(recwarn) == 0, recwarn.list def test_from_job(self, app): job = app.job_queue.run_once(lambda x: x, 10) diff --git a/tests/test_chat.py b/tests/test_chat.py index e3eef6f5dee..eaa5cf4de53 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -65,6 +65,11 @@ class TestChat: has_protected_content = True has_private_forwards = True + def test_slot_behaviour(self, chat, mro_slots): + for attr in chat.__slots__: + assert getattr(chat, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(chat)) == len(set(mro_slots(chat))), "duplicate slot" + def test_de_json(self, bot): json_dict = { 'id': self.id_, diff --git a/tests/test_chatjoinrequest.py b/tests/test_chatjoinrequest.py index c5a53398d49..16e7adcbb9a 100644 --- a/tests/test_chatjoinrequest.py +++ b/tests/test_chatjoinrequest.py @@ -56,7 +56,7 @@ class TestChatJoinRequest: is_primary=False, ) - def test_slot_behaviour(self, chat_join_request, recwarn, mro_slots): + def test_slot_behaviour(self, chat_join_request, mro_slots): inst = chat_join_request for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" diff --git a/tests/test_chatjoinrequesthandler.py b/tests/test_chatjoinrequesthandler.py index ccdee344bb2..9bfb8432eb2 100644 --- a/tests/test_chatjoinrequesthandler.py +++ b/tests/test_chatjoinrequesthandler.py @@ -102,7 +102,7 @@ def chat_join_request_update(bot, chat_join_request): class TestChatJoinRequestHandler: test_flag = False - def test_slot_behaviour(self, recwarn, mro_slots): + def test_slot_behaviour(self, mro_slots): action = ChatJoinRequestHandler(self.callback) for attr in action.__slots__: assert getattr(action, attr, 'err') != 'err', f"got extra slot '{attr}'" diff --git a/tests/test_chatphoto.py b/tests/test_chatphoto.py index 9fd3ce954b8..2c774057594 100644 --- a/tests/test_chatphoto.py +++ b/tests/test_chatphoto.py @@ -60,6 +60,11 @@ class TestChatPhoto: chatphoto_big_file_unique_id = 'bigadc3145fd2e84d95b64d68eaa22aa33e' chatphoto_file_url = 'https://python-telegram-bot.org/static/testfiles/telegram.jpg' + def test_slot_behaviour(self, chat_photo, mro_slots): + for attr in chat_photo.__slots__: + assert getattr(chat_photo, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(chat_photo)) == len(set(mro_slots(chat_photo))), "duplicate slot" + @flaky(3, 1) @pytest.mark.asyncio async def test_send_all_args( diff --git a/tests/test_document.py b/tests/test_document.py index c77ab345bbb..ca9dfb5f540 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -60,6 +60,11 @@ class TestDocument: document_file_id = '5a3128a4d2a04750b5b58397f3b5e812' document_file_unique_id = 'adc3145fd2e84d95b64d68eaa22aa33e' + def test_slot_behaviour(self, document, mro_slots): + for attr in document.__slots__: + assert getattr(document, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(document)) == len(set(mro_slots(document))), "duplicate slot" + def test_creation(self, document): assert isinstance(document, Document) assert isinstance(document.file_id, str) diff --git a/tests/test_error.py b/tests/test_error.py index 25c51f665d3..70a6426c480 100644 --- a/tests/test_error.py +++ b/tests/test_error.py @@ -142,12 +142,12 @@ def test_errors_pickling(self, exception, attributes): (InvalidCallbackData('test data')), ], ) - def test_slots_behavior(self, inst, mro_slots): + def test_slot_behaviour(self, inst, mro_slots): for attr in inst.__slots__: assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" - def test_test_coverage(self): + def test_coverage(self): """ This test is only here to make sure that new errors will override __reduce__ and set __slots__ properly. diff --git a/tests/test_inlinequery.py b/tests/test_inlinequery.py index fdd15a1fdf7..52487704a6f 100644 --- a/tests/test_inlinequery.py +++ b/tests/test_inlinequery.py @@ -42,6 +42,11 @@ class TestInlineQuery: offset = 'offset' location = Location(8.8, 53.1) + def test_slot_behaviour(self, inline_query, mro_slots): + for attr in inline_query.__slots__: + assert getattr(inline_query, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(inline_query)) == len(set(mro_slots(inline_query))), "duplicate slot" + def test_de_json(self, bot): json_dict = { 'id': self.id_, diff --git a/tests/test_message.py b/tests/test_message.py index dc42291c22b..c526f2daae7 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -325,6 +325,11 @@ def test_all_possibilities_de_json_and_to_dict(self, bot, message_params): assert new.to_dict() == message_params.to_dict() + def test_slot_behaviour(self, message, mro_slots): + for attr in message.__slots__: + assert getattr(message, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(message)) == len(set(mro_slots(message))), "duplicate slot" + @pytest.mark.asyncio async def test_parse_entity(self): text = ( diff --git a/tests/test_photo.py b/tests/test_photo.py index 6c44f8b29f7..69fb34ced4c 100644 --- a/tests/test_photo.py +++ b/tests/test_photo.py @@ -74,6 +74,11 @@ class TestPhoto: # so we accept three different sizes here. Shouldn't be too much file_size = [29176, 27662] + def test_slot_behaviour(self, photo, mro_slots): + for attr in photo.__slots__: + assert getattr(photo, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(photo)) == len(set(mro_slots(photo))), "duplicate slot" + def test_creation(self, thumb, photo): # Make sure file has been uploaded. assert isinstance(photo, PhotoSize) diff --git a/tests/test_precheckoutquery.py b/tests/test_precheckoutquery.py index c782e066729..88a81ae5643 100644 --- a/tests/test_precheckoutquery.py +++ b/tests/test_precheckoutquery.py @@ -46,6 +46,12 @@ class TestPreCheckoutQuery: from_user = User(0, '', False) order_info = OrderInfo() + def test_slot_behaviour(self, pre_checkout_query, mro_slots): + inst = pre_checkout_query + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + def test_de_json(self, bot): json_dict = { 'id': self.id_, diff --git a/tests/test_replykeyboardmarkup.py b/tests/test_replykeyboardmarkup.py index 0a69e279d1d..6480d1d3167 100644 --- a/tests/test_replykeyboardmarkup.py +++ b/tests/test_replykeyboardmarkup.py @@ -39,6 +39,12 @@ class TestReplyKeyboardMarkup: one_time_keyboard = True selective = True + def test_slot_behaviour(self, reply_keyboard_markup, mro_slots): + inst = reply_keyboard_markup + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + @flaky(3, 1) @pytest.mark.asyncio async def test_send_message_with_reply_keyboard_markup( diff --git a/tests/test_requestdata.py b/tests/test_requestdata.py index 3a38a8e82b8..2f254b6b26d 100644 --- a/tests/test_requestdata.py +++ b/tests/test_requestdata.py @@ -133,6 +133,11 @@ def mixed_rqs(mixed_params) -> RequestData: class TestRequestData: + def test_slot_behaviour(self, simple_rqs, mro_slots): + for attr in simple_rqs.__slots__: + assert getattr(simple_rqs, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(simple_rqs)) == len(set(mro_slots(simple_rqs))), "duplicate slot" + def test_contains_files(self, simple_rqs, file_rqs, mixed_rqs): assert not simple_rqs.contains_files assert file_rqs.contains_files diff --git a/tests/test_requestparameter.py b/tests/test_requestparameter.py index aaf9ea75027..52c404a1e5c 100644 --- a/tests/test_requestparameter.py +++ b/tests/test_requestparameter.py @@ -38,6 +38,12 @@ def test_init(self): assert request_parameter.value == 'value' assert request_parameter.input_files is None + def test_slot_behaviour(self, mro_slots): + inst = RequestParameter('name', 'value', [1, 2]) + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + @pytest.mark.parametrize( 'value, expected', [ diff --git a/tests/test_shippingquery.py b/tests/test_shippingquery.py index d9415436a6d..ce6cdb745a3 100644 --- a/tests/test_shippingquery.py +++ b/tests/test_shippingquery.py @@ -40,6 +40,12 @@ class TestShippingQuery: from_user = User(0, '', False) shipping_address = ShippingAddress('GB', '', 'London', '12 Grimmauld Place', '', 'WC1') + def test_slot_behaviour(self, shipping_query, mro_slots): + inst = shipping_query + for attr in inst.__slots__: + assert getattr(inst, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + def test_de_json(self, bot): json_dict = { 'id': TestShippingQuery.id_, diff --git a/tests/test_trackingdict.py b/tests/test_trackingdict.py index 07da2dae5f0..f6e5e91cd15 100644 --- a/tests/test_trackingdict.py +++ b/tests/test_trackingdict.py @@ -35,6 +35,11 @@ def data() -> dict: class TestTrackingDict: + def test_slot_behaviour(self, td, mro_slots): + for attr in td.__slots__: + assert getattr(td, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(td)) == len(set(mro_slots(td))), "duplicate slot" + def test_representations(self, td, data): assert repr(td) == repr(data) assert str(td) == str(data) diff --git a/tests/test_user.py b/tests/test_user.py index d4f621d3ec2..ad9195c9d91 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -65,6 +65,11 @@ class TestUser: can_read_all_group_messages = True supports_inline_queries = False + def test_slot_behaviour(self, user, mro_slots): + for attr in user.__slots__: + assert getattr(user, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(user)) == len(set(mro_slots(user))), "duplicate slot" + def test_de_json(self, json_dict, bot): user = User.de_json(json_dict, bot) diff --git a/tests/test_video.py b/tests/test_video.py index 141f20a2668..b12806d278c 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -67,6 +67,11 @@ class TestVideo: video_file_id = '5a3128a4d2a04750b5b58397f3b5e812' video_file_unique_id = 'adc3145fd2e84d95b64d68eaa22aa33e' + def test_slot_behaviour(self, video, mro_slots): + for attr in video.__slots__: + assert getattr(video, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(video)) == len(set(mro_slots(video))), "duplicate slot" + def test_creation(self, video): # Make sure file has been uploaded. assert isinstance(video, Video) diff --git a/tests/test_videonote.py b/tests/test_videonote.py index 915a0e88615..7f11bee7b69 100644 --- a/tests/test_videonote.py +++ b/tests/test_videonote.py @@ -60,6 +60,11 @@ class TestVideoNote: videonote_file_id = '5a3128a4d2a04750b5b58397f3b5e812' videonote_file_unique_id = 'adc3145fd2e84d95b64d68eaa22aa33e' + def test_slot_behaviour(self, video_note, mro_slots): + for attr in video_note.__slots__: + assert getattr(video_note, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(video_note)) == len(set(mro_slots(video_note))), "duplicate slot" + def test_creation(self, video_note): # Make sure file has been uploaded. assert isinstance(video_note, VideoNote) diff --git a/tests/test_voice.py b/tests/test_voice.py index 4190e95cdcf..08d492c7798 100644 --- a/tests/test_voice.py +++ b/tests/test_voice.py @@ -58,6 +58,11 @@ class TestVoice: voice_file_id = '5a3128a4d2a04750b5b58397f3b5e812' voice_file_unique_id = 'adc3145fd2e84d95b64d68eaa22aa33e' + def test_slot_behaviour(self, voice, mro_slots): + for attr in voice.__slots__: + assert getattr(voice, attr, 'err') != 'err', f"got extra slot '{attr}'" + assert len(mro_slots(voice)) == len(set(mro_slots(voice))), "duplicate slot" + @pytest.mark.asyncio async def test_creation(self, voice): # Make sure file has been uploaded. From 55d84419df7a92ddeb615709649d484cb9eda6e4 Mon Sep 17 00:00:00 2001 From: Harshil <37377066+harshil21@users.noreply.github.com> Date: Mon, 11 Apr 2022 00:42:06 +0530 Subject: [PATCH 13/25] remove leftovers from test_bot --- tests/conftest.py | 2 ++ tests/test_bot.py | 12 +----------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 120444a05d3..e1903b45876 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -155,6 +155,7 @@ class DictApplication(Application): @pytest.fixture(scope='session') @pytest.mark.asyncio async def bot(bot_info): + """Makes an ExtBot instance with the given bot_info""" async with make_bot(bot_info) as _bot: yield _bot @@ -162,6 +163,7 @@ async def bot(bot_info): @pytest.fixture(scope='session') @pytest.mark.asyncio async def raw_bot(bot_info): + """Makes an regular Bot instance with the given bot_info""" async with DictBot( bot_info['token'], private_key=PRIVATE_KEY, diff --git a/tests/test_bot.py b/tests/test_bot.py index 9711432d181..69fceabd164 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -80,16 +80,6 @@ def to_camel_case(snake_str): return components[0] + ''.join(x.title() for x in components[1:]) -class ExtBotSubClass(ExtBot): - # used for test_defaults_warning below - pass - - -class BotSubClass(Bot): - # used for test_defaults_warning below - pass - - @pytest.fixture(scope='class') @pytest.mark.asyncio async def message(bot, chat_id): @@ -3049,7 +3039,7 @@ def test_camel_case_bot(self): if ( function_name.startswith("_") or not callable(function) - or function_name in ["to_dict", "do_init", "do_teardown"] + or function_name in ["to_dict"] ): continue camel_case_function = getattr(Bot, to_camel_case(function_name), False) From eb48c0fec67a110fe626d50ee1bd9592e47ca098 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 11 Apr 2022 10:04:09 +0200 Subject: [PATCH 14/25] A few more CH tests --- tests/test_conversationhandler.py | 224 +++++++++++++----------------- 1 file changed, 98 insertions(+), 126 deletions(-) diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py index 1b8961a3510..1022678eb94 100644 --- a/tests/test_conversationhandler.py +++ b/tests/test_conversationhandler.py @@ -736,47 +736,56 @@ async def test_conversation_handler_per_user(self, app, bot, user1): message.chat = self.second_group assert handler.check_update(Update(update_id=0, message=message)) - # TODO - # @pytest.mark.asyncio - # async def test_conversation_handler_per_message(self, app, bot, user1, user2): - # def entry(update, context): - # return 1 - # - # def one(update, context): - # return 2 - # - # def two(update, context): - # return ConversationHandler.END - # - # handler = ConversationHandler( - # entry_points=[CallbackQueryHandler(entry)], - # states={1: [CallbackQueryHandler(one)], 2: [CallbackQueryHandler(two)]}, - # fallbacks=[], - # per_message=True, - # ) - # app.add_handler(handler) - # - # # User one, starts the state machine. - # message = Message( - # 0, None, self.group, from_user=user1, text='msg w/ inlinekeyboard', bot=bot - # ) - # - # async with app: - # cbq = CallbackQuery(0, user1, None, message=message, data='data', bot=bot) - # await app.process_update(Update(update_id=0, callback_query=cbq)) - # - # assert handler.conversations[(self.group.id, user1.id, message.message_id)] == 1 - # - # await app.process_update(Update(update_id=0, callback_query=cbq)) - # - # assert handler.conversations[(self.group.id, user1.id, message.message_id)] == 2 - # - # # Let's now verify that for a different user in the same group, the state will not be - # # updated - # cbq.from_user = user2 - # await app.process_update(Update(update_id=0, callback_query=cbq)) - # - # assert handler.conversations[(self.group.id, user1.id, message.message_id)] == 2 + @pytest.mark.asyncio + async def test_conversation_handler_per_message(self, app, bot, user1, user2): + async def entry(update, context): + return 1 + + async def one(update, context): + return 2 + + async def two(update, context): + return ConversationHandler.END + + handler = ConversationHandler( + entry_points=[CallbackQueryHandler(entry)], + states={ + 1: [CallbackQueryHandler(one, pattern='^1$')], + 2: [CallbackQueryHandler(two, pattern='^2$')], + }, + fallbacks=[], + per_message=True, + ) + app.add_handler(handler) + + # User one, starts the state machine. + message = Message( + 0, None, self.group, from_user=user1, text='msg w/ inlinekeyboard', bot=bot + ) + + async with app: + cbq_1 = CallbackQuery(0, user1, None, message=message, data='1', bot=bot) + cbq_2 = CallbackQuery(0, user1, None, message=message, data='2', bot=bot) + await app.process_update(Update(update_id=0, callback_query=cbq_1)) + + # Make sure that we're in the correct state + assert handler.check_update(Update(0, callback_query=cbq_1)) + assert not handler.check_update(Update(0, callback_query=cbq_2)) + + await app.process_update(Update(update_id=0, callback_query=cbq_1)) + + # Make sure that we're in the correct state + assert not handler.check_update(Update(0, callback_query=cbq_1)) + assert handler.check_update(Update(0, callback_query=cbq_2)) + + # Let's now verify that for a different user in the same group, the state will not be + # updated + cbq_2.from_user = user2 + await app.process_update(Update(update_id=0, callback_query=cbq_2)) + + cbq_2.from_user = user1 + assert not handler.check_update(Update(0, callback_query=cbq_1)) + assert handler.check_update(Update(0, callback_query=cbq_2)) @pytest.mark.asyncio async def test_end_on_first_message(self, app, bot, user1): @@ -801,92 +810,55 @@ async def test_end_on_first_message(self, app, bot, user1): await app.process_update(Update(update_id=0, message=message)) assert handler.check_update(Update(update_id=0, message=message)) - # TODO - # @pytest.mark.asyncio - # async def test_end_on_first_message_async(self, app, bot, user1): - # handler = ConversationHandler( - # entry_points=[ - # CommandHandler( - # 'start', lambda update, context: app.run_async(self.start_end, update, - # context) - # ) - # ], - # states={}, - # fallbacks=[], - # ) - # app.add_handler(handler) - # - # # User starts the state machine with an async function that immediately ends the - # # conversation. Async results are resolved when the users state is queried next time. - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # app.update_queue.put(Update(update_id=0, message=message)) - # await asyncio.sleep(0.1) - # # Assert that the Promise has been accepted as the new state - # assert len(handler.conversations) == 1 - # - # message.text = 'resolve promise pls' - # message.entities[0].length = len('resolve promise pls') - # app.update_queue.put(Update(update_id=0, message=message)) - # await asyncio.sleep(0.1) - # # Assert that the Promise has been resolved and the conversation ended. - # assert len(handler.conversations) == 0 + @pytest.mark.asyncio + async def test_end_on_first_message_non_blocking_handler(self, app, bot, user1): + handler = ConversationHandler( + entry_points=[CommandHandler('start', callback=self.start_end, block=False)], + states={}, + fallbacks=[], + ) + app.add_handler(handler) - # @pytest.mark.asyncio - # async def test_end_on_first_message_async_handler(self, app, bot, user1): - # handler = ConversationHandler( - # entry_points=[CommandHandler('start', self.start_end, run_async=True)], - # states={}, - # fallbacks=[], - # ) - # app.add_handler(handler) - # - # # User starts the state machine with an async function that immediately ends the - # # conversation. Async results are resolved when the users state is queried next time. - # message = Message( - # 0, - # None, - # self.group, - # text='/start', - # from_user=user1, - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # app.update_queue.put(Update(update_id=0, message=message)) - # await asyncio.sleep(0.1) - # # Assert that the Promise has been accepted as the new state - # assert len(handler.conversations) == 1 - # - # message.text = 'resolve promise pls' - # message.entities[0].length = len('resolve promise pls') - # app.update_queue.put(Update(update_id=0, message=message)) - # await asyncio.sleep(0.1) - # # Assert that the Promise has been resolved and the conversation ended. - # assert len(handler.conversations) == 0 - # - # @pytest.mark.asyncio - # async def test_none_on_first_message(self, app, bot, user1): - # handler = ConversationHandler( - # entry_points=[CommandHandler('start', self.start_none)], states={}, fallbacks=[] - # ) - # app.add_handler(handler) - # - # # User starts the state machine and a callback function returns None - # message = Message(0, None, self.group, from_user=user1, text='/start', bot=bot) - # await app.process_update(Update(update_id=0, message=message)) - # assert len(handler.conversations) == 0 - # + # User starts the state machine with a non-blocking function that immediately ends the + # conversation. non-blocking results are resolved when the users state is queried next + # time. + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + async with app: + await app.process_update(Update(update_id=0, message=message)) + # give the task a chance to finish + await asyncio.sleep(0.1) + + # Let's check that processing the same update again is accepted. this confirms that + # a) the pending state is correctly resolved + # b) the conversation has ended + assert handler.check_update(Update(0, message=message)) + + @pytest.mark.asyncio + async def test_none_on_first_message(self, app, bot, user1): + handler = ConversationHandler( + entry_points=[MessageHandler(filters.ALL, self.start_none)], states={}, fallbacks=[] + ) + app.add_handler(handler) + + # User starts the state machine and a callback function returns None + message = Message(0, None, self.group, from_user=user1, text='/start', bot=bot) + async with app: + await app.process_update(Update(update_id=0, message=message)) + # Check that the same message is accepted again, i.e. the conversation immediately + # ended + assert handler.check_update(Update(0, message=message)) + + # TODO # @pytest.mark.asyncio # async def test_none_on_first_message_async(self, app, bot, user1): # handler = ConversationHandler( From 3e5c8cb0c38de7cb023dd5bbedafaee8cebc63ad Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 11 Apr 2022 11:23:04 +0200 Subject: [PATCH 15/25] More CH tests --- tests/test_conversationhandler.py | 312 +++++++++++++++--------------- 1 file changed, 152 insertions(+), 160 deletions(-) diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py index 1022678eb94..f89699ab40c 100644 --- a/tests/test_conversationhandler.py +++ b/tests/test_conversationhandler.py @@ -858,77 +858,37 @@ async def test_none_on_first_message(self, app, bot, user1): # ended assert handler.check_update(Update(0, message=message)) - # TODO - # @pytest.mark.asyncio - # async def test_none_on_first_message_async(self, app, bot, user1): - # handler = ConversationHandler( - # entry_points=[ - # CommandHandler( - # 'start', lambda update, context: app.run_async(self.start_none, update, - # context) - # ) - # ], - # states={}, - # fallbacks=[], - # ) - # app.add_handler(handler) - # - # # User starts the state machine with an async function that returns None - # # Async results are resolved when the users state is queried next time. - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # app.update_queue.put(Update(update_id=0, message=message)) - # await asyncio.sleep(0.1) - # # Assert that the Promise has been accepted as the new state - # assert len(handler.conversations) == 1 - # - # message.text = 'resolve promise pls' - # app.update_queue.put(Update(update_id=0, message=message)) - # await asyncio.sleep(0.1) - # # Assert that the Promise has been resolved and the conversation ended. - # assert len(handler.conversations) == 0 - # - # @pytest.mark.asyncio - # async def test_none_on_first_message_async_handler(self, app, bot, user1): - # handler = ConversationHandler( - # entry_points=[CommandHandler('start', self.start_none, run_async=True)], - # states={}, - # fallbacks=[], - # ) - # app.add_handler(handler) - # - # # User starts the state machine with an async function that returns None - # # Async results are resolved when the users state is queried next time. - # message = Message( - # 0, - # None, - # self.group, - # text='/start', - # from_user=user1, - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # app.update_queue.put(Update(update_id=0, message=message)) - # await asyncio.sleep(0.1) - # # Assert that the Promise has been accepted as the new state - # assert len(handler.conversations) == 1 - # - # message.text = 'resolve promise pls' - # app.update_queue.put(Update(update_id=0, message=message)) - # await asyncio.sleep(0.1) - # # Assert that the Promise has been resolved and the conversation ended. - # assert len(handler.conversations) == 0 + @pytest.mark.asyncio + async def test_none_on_first_message_non_blocking_handler(self, app, bot, user1): + handler = ConversationHandler( + entry_points=[CommandHandler('start', self.start_none, block=False)], + states={}, + fallbacks=[], + ) + app.add_handler(handler) + + # User starts the state machine with a non-blocking handler that returns None + # non-blocking results are resolved when the users state is queried next time. + message = Message( + 0, + None, + self.group, + text='/start', + from_user=user1, + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + async with app: + await app.process_update(Update(update_id=0, message=message)) + # Give the task a chance to finish + await asyncio.sleep(0.1) + + # Let's check that processing the same update again is accepted. this confirms that + # a) the pending state is correctly resolved + # b) the conversation has ended + assert handler.check_update(Update(0, message=message)) @pytest.mark.asyncio async def test_per_chat_message_without_chat(self, bot, user1): @@ -1046,92 +1006,124 @@ class DictJB(JobQueue): assert caplog.records[0].message == "Failed to schedule timeout." assert str(caplog.records[0].exc_info[1]) == "job error" - # @pytest.mark.asyncio - # async def test_promise_exception(self, app, bot, user1, caplog): - # """ - # Here we make sure that when a run_async handle raises an - # exception, the state isn't changed. - # """ - # - # def conv_entry(*a, **kw): - # return 1 - # - # def raise_error(*a, **kw): - # raise Exception("promise exception") - # - # handler = ConversationHandler( - # entry_points=[CommandHandler("start", conv_entry)], - # states={1: [MessageHandler(filters.ALL, raise_error)]}, - # fallbacks=self.fallbacks, - # run_async=True, - # ) - # app.add_handler(handler) - # - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # # start the conversation - # await app.process_update(Update(update_id=0, message=message)) - # await asyncio.sleep(0.1) - # message.text = "error" - # await app.process_update(Update(update_id=0, message=message)) - # await asyncio.sleep(0.1) - # message.text = "resolve promise pls" - # caplog.clear() - # with caplog.at_level(logging.ERROR): - # await app.process_update(Update(update_id=0, message=message)) - # await asyncio.sleep(0.5) - # assert len(caplog.records) == 3 - # assert caplog.records[0].message == "Promise function raised exception" - # assert caplog.records[1].message == "promise exception" - # # assert res is old state - # assert handler.conversations.get((self.group.id, user1.id))[0] == 1 - # - # @pytest.mark.asyncio - # async def test_conversation_timeout(self, app, bot, user1): - # handler = ConversationHandler( - # entry_points=self.entry_points, - # states=self.states, - # fallbacks=self.fallbacks, - # conversation_timeout=0.5, - # ) - # app.add_handler(handler) - # - # # Start state machine, then reach timeout - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # await app.process_update(Update(update_id=0, message=message)) - # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY - # await asyncio.sleep(0.75) - # assert handler.conversations.get((self.group.id, user1.id)) is None - # - # # Start state machine, do something, then reach timeout - # await app.process_update(Update(update_id=1, message=message)) - # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY - # message.text = '/brew' - # message.entities[0].length = len('/brew') - # await app.process_update(Update(update_id=2, message=message)) - # assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING - # await asyncio.sleep(0.7) - # assert handler.conversations.get((self.group.id, user1.id)) is None - # + @pytest.mark.asyncio + async def test_non_blocking_exception(self, app, bot, user1, caplog): + """Here we make sure that when a non-blocking handler raises an + exception, the state isn't changed. + """ + error = Exception('task exception') + + async def conv_entry(*a, **kw): + return 1 + + async def raise_error(*a, **kw): + raise error + + handler = ConversationHandler( + entry_points=[CommandHandler("start", conv_entry)], + states={1: [MessageHandler(filters.Text(['error']), raise_error)]}, + fallbacks=self.fallbacks, + block=False, + ) + app.add_handler(handler) + + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + # start the conversation + async with app: + await app.process_update(Update(update_id=0, message=message)) + await asyncio.sleep(0.1) + message.text = "error" + await app.process_update(Update(update_id=0, message=message)) + await asyncio.sleep(0.1) + caplog.clear() + with caplog.at_level(logging.ERROR): + # This also makes sure that we're still in the same state + assert handler.check_update(Update(0, message=message)) + assert len(caplog.records) == 1 + assert ( + caplog.records[0].message + == "Task function raised exception. Falling back to old state 1" + ) + assert caplog.records[0].exc_info[1] is error + + @pytest.mark.asyncio + async def test_conversation_timeout(self, app, bot, user1): + handler = ConversationHandler( + entry_points=self.entry_points, + states=self.states, + fallbacks=self.fallbacks, + conversation_timeout=0.5, + ) + app.add_handler(handler) + + # Start state machine, then reach timeout + start_message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + brew_message = Message( + 0, + None, + self.group, + from_user=user1, + text='/brew', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/brew')) + ], + bot=bot, + ) + pour_coffee_message = Message( + 0, + None, + self.group, + from_user=user1, + text='/pourCoffee', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/pourCoffee')) + ], + bot=bot, + ) + async with app: + await app.start() + + await app.process_update(Update(update_id=0, message=start_message)) + assert handler.check_update(Update(0, message=brew_message)) + await asyncio.sleep(0.75) + assert handler.check_update(Update(0, message=start_message)) + + # Start state machine, do something, then reach timeout + await app.process_update(Update(update_id=1, message=start_message)) + assert handler.check_update(Update(0, message=brew_message)) + # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY + # start_message.text = '/brew' + # start_message.entities[0].length = len('/brew') + await app.process_update(Update(update_id=2, message=brew_message)) + assert handler.check_update(Update(0, message=pour_coffee_message)) + # assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING + await asyncio.sleep(0.7) + assert handler.check_update(Update(0, message=start_message)) + # assert handler.conversations.get((self.group.id, user1.id)) is None + + await app.stop() + + # TODO # @pytest.mark.asyncio # async def test_timeout_not_triggered_on_conv_end_async(self, bot, app, user1): # def timeout(*a, **kw): @@ -1143,7 +1135,7 @@ class DictJB(JobQueue): # states=self.states, # fallbacks=self.fallbacks, # conversation_timeout=0.5, - # run_async=True, + # block=False, # ) # app.add_handler(handler) # @@ -1780,7 +1772,7 @@ class DictJB(JobQueue): # entry_points=self.entry_points, # states=self.states, # fallbacks=self.fallbacks, - # run_async=True, + # block=False, # ) # # all_handlers = conv_handler.entry_points + conv_handler.fallbacks @@ -1793,7 +1785,7 @@ class DictJB(JobQueue): # @pytest.mark.asyncio # async def test_conversation_handler_run_async_false(self, app): # conv_handler = ConversationHandler( - # entry_points=[CommandHandler('start', self.start_end, run_async=True)], + # entry_points=[CommandHandler('start', self.start_end, block=False)], # states=self.states, # fallbacks=self.fallbacks, # run_async=False, From aa5f29bb908ca06085f8da6ac95a7e3992764c08 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 11 Apr 2022 20:10:08 +0200 Subject: [PATCH 16/25] remove duplicate in test --- tests/test_bot.py | 43 ------------------------------------------- 1 file changed, 43 deletions(-) diff --git a/tests/test_bot.py b/tests/test_bot.py index 69fceabd164..81abddce1ef 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -471,49 +471,6 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): finally: await bot.get_me() # because running the mock-get_me messages with bot.bot & friends - # check that tg.Bot does the right thing - # make_assertion basically checks everything that happens in - # Bot._insert_defaults and Bot._insert_defaults_for_ilq_results - async def make_assertion(url, request_data: RequestData, *args, **kwargs): - data = request_data.parameters - - # Check regular kwargs - for k, v in data.items(): - if isinstance(v, DefaultValue): - pytest.fail(f'Parameter {k} was passed as DefaultValue to request') - elif isinstance(v, InputMedia) and isinstance(v.parse_mode, DefaultValue): - pytest.fail(f'Parameter {k} has a DefaultValue parse_mode') - - # Check InputMedia - elif k == 'media' and isinstance(v, list): - for med in v: - if isinstance(med.get('parse_mode', None), DefaultValue): - pytest.fail('One of the media items has a DefaultValue parse_mode') - - # Check inline query results - if bot_method_name.lower().replace('_', '') == 'answerinlinequery': - for result_dict in data['results']: - if isinstance(result_dict.get('parse_mode'), DefaultValue): - pytest.fail('InlineQueryResult has DefaultValue parse_mode') - imc = result_dict.get('input_message_content') - if imc and isinstance(imc.get('parse_mode'), DefaultValue): - pytest.fail( - 'InlineQueryResult is InputMessageContext with DefaultValue parse_mode' - ) - if imc and isinstance(imc.get('disable_web_page_preview'), DefaultValue): - pytest.fail( - 'InlineQueryResult is InputMessageContext with DefaultValue ' - 'disable_web_page_preview ' - ) - # Check datetime conversion - until_date = data.pop('until_date', None) - if until_date and until_date != 946684800: - pytest.fail('Naive until_date was not interpreted as UTC') - - if bot_method_name in ['get_file', 'getFile']: - # The get_file methods try to check if the result is a local file - return File(file_id='result', file_unique_id='result').to_dict() - method = getattr(raw_bot, bot_method_name) signature = inspect.signature(method) kwargs_need_default = [ From 88a69a8ea351833d572f7941e1d3b4ba8a0dc97c Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 11 Apr 2022 21:41:27 +0200 Subject: [PATCH 17/25] more ch tests --- tests/test_conversationhandler.py | 521 +++++++++++++++++------------- 1 file changed, 288 insertions(+), 233 deletions(-) diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py index f89699ab40c..99708583f59 100644 --- a/tests/test_conversationhandler.py +++ b/tests/test_conversationhandler.py @@ -22,6 +22,7 @@ from warnings import filterwarnings import pytest +from flaky import flaky from telegram import ( Chat, @@ -67,10 +68,10 @@ def user2(): return User(first_name='Mister Test', id=124, is_bot=False) -def raise_dphs(func): +def raise_ahs(func): async def decorator(self, *args, **kwargs): result = await func(self, *args, **kwargs) - if self.raise_dp_handler_stop: + if self.raise_app_handler_stop: raise ApplicationHandlerStop(result) return result @@ -80,6 +81,9 @@ async def decorator(self, *args, **kwargs): class TestConversationHandler: """Persistence of conversations is tested in test_basepersistence.py""" + # TODO + # * Test that we have a warning when conversation timeout is scheduled with non-running JQ + # State definitions # At first we're thirsty. Then we brew coffee, we drink it # and then we can start coding! @@ -93,13 +97,13 @@ class TestConversationHandler: group = Chat(0, Chat.GROUP) second_group = Chat(1, Chat.GROUP) - raise_dp_handler_stop = False + raise_app_handler_stop = False test_flag = False # Test related @pytest.fixture(autouse=True) def reset(self): - self.raise_dp_handler_stop = False + self.raise_app_handler_stop = False self.test_flag = False self.current_state = {} self.entry_points = [CommandHandler('start', self.start)] @@ -165,79 +169,79 @@ def _set_state(self, update, state): return state # Actions - @raise_dphs + @raise_ahs async def start(self, update, context): if isinstance(update, Update): return self._set_state(update, self.THIRSTY) return self._set_state(context.bot, self.THIRSTY) - @raise_dphs + @raise_ahs async def end(self, update, context): return self._set_state(update, self.END) - @raise_dphs + @raise_ahs async def start_end(self, update, context): return self._set_state(update, self.END) - @raise_dphs + @raise_ahs async def start_none(self, update, context): return self._set_state(update, None) - @raise_dphs + @raise_ahs async def brew(self, update, context): if isinstance(update, Update): return self._set_state(update, self.BREWING) return self._set_state(context.bot, self.BREWING) - @raise_dphs + @raise_ahs async def drink(self, update, context): return self._set_state(update, self.DRINKING) - @raise_dphs + @raise_ahs async def code(self, update, context): return self._set_state(update, self.CODING) - @raise_dphs + @raise_ahs async def passout(self, update, context): assert update.message.text == '/brew' assert isinstance(update, Update) self.is_timeout = True - @raise_dphs + @raise_ahs async def passout2(self, update, context): assert isinstance(update, Update) self.is_timeout = True - @raise_dphs + @raise_ahs async def passout_context(self, update, context): assert update.message.text == '/brew' assert isinstance(context, CallbackContext) self.is_timeout = True - @raise_dphs + @raise_ahs async def passout2_context(self, update, context): assert isinstance(context, CallbackContext) self.is_timeout = True # Drinking actions (nested) - @raise_dphs + @raise_ahs async def hold(self, update, context): return self._set_state(update, self.HOLDING) - @raise_dphs + @raise_ahs async def sip(self, update, context): return self._set_state(update, self.SIPPING) - @raise_dphs + @raise_ahs async def swallow(self, update, context): return self._set_state(update, self.SWALLOWING) - @raise_dphs + @raise_ahs async def replenish(self, update, context): return self._set_state(update, self.REPLENISHING) - @raise_dphs + @raise_ahs async def stop(self, update, context): return self._set_state(update, self.STOPPING) @@ -1123,220 +1127,271 @@ async def test_conversation_timeout(self, app, bot, user1): await app.stop() + @pytest.mark.asyncio + async def test_timeout_not_triggered_on_conv_end_non_blocking(self, bot, app, user1): + def timeout(*a, **kw): + self.test_flag = True + + self.states.update({ConversationHandler.TIMEOUT: [TypeHandler(Update, timeout)]}) + handler = ConversationHandler( + entry_points=self.entry_points, + states=self.states, + fallbacks=self.fallbacks, + conversation_timeout=0.5, + block=False, + ) + app.add_handler(handler) + + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + async with app: + # start the conversation + await app.process_update(Update(update_id=0, message=message)) + await asyncio.sleep(0.1) + message.text = '/brew' + message.entities[0].length = len('/brew') + await app.process_update(Update(update_id=1, message=message)) + await asyncio.sleep(0.1) + message.text = '/pourCoffee' + message.entities[0].length = len('/pourCoffee') + await app.process_update(Update(update_id=2, message=message)) + await asyncio.sleep(0.1) + message.text = '/end' + message.entities[0].length = len('/end') + await app.process_update(Update(update_id=3, message=message)) + await asyncio.sleep(1) + # assert timeout handler didn't get called + assert self.test_flag is False + + @pytest.mark.asyncio + async def test_conversation_timeout_application_handler_stop(self, app, bot, user1, recwarn): + handler = ConversationHandler( + entry_points=self.entry_points, + states=self.states, + fallbacks=self.fallbacks, + conversation_timeout=0.5, + ) + + def timeout(*args, **kwargs): + raise ApplicationHandlerStop() + + self.states.update({ConversationHandler.TIMEOUT: [TypeHandler(Update, timeout)]}) + app.add_handler(handler) + + # Start state machine, then reach timeout + message = Message( + 0, + None, + self.group, + text='/start', + from_user=user1, + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + brew_message = Message( + 0, + None, + self.group, + from_user=user1, + text='/brew', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/brew')) + ], + bot=bot, + ) + + async with app: + await app.start() + + await app.process_update(Update(update_id=0, message=message)) + # Make sure that we're in the next state + assert handler.check_update(Update(0, message=brew_message)) + await app.process_update(Update(0, message=brew_message)) + await asyncio.sleep(0.9) + # Check that conversation has ended by checking that the start messages is accepted + # again + assert handler.check_update(Update(0, message=message)) + assert len(recwarn) == 1 + assert str(recwarn[0].message).startswith('ApplicationHandlerStop in TIMEOUT') + + await app.stop() + + @pytest.mark.asyncio + async def test_conversation_handler_timeout_update_and_context(self, app, bot, user1): + context = None + + async def start_callback(u, c): + nonlocal context, self + context = c + return await self.start(u, c) + + # Start state machine, then reach timeout + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + update = Update(update_id=0, message=message) + + async def timeout_callback(u, c): + nonlocal update, context + assert u is update + assert c is context + + self.is_timeout = (u is update) and (c is context) + + states = self.states + timeout_handler = CommandHandler('start', timeout_callback) + states.update({ConversationHandler.TIMEOUT: [timeout_handler]}) + handler = ConversationHandler( + entry_points=[CommandHandler('start', start_callback)], + states=states, + fallbacks=self.fallbacks, + conversation_timeout=0.5, + ) + app.add_handler(handler) + + async with app: + await app.start() + + await app.process_update(update) + await asyncio.sleep(0.9) + # check that the conversation has ended by checking that the start message is accepted + assert handler.check_update(Update(0, message=message)) + assert self.is_timeout + + await app.stop() + + @flaky(3, 1) + @pytest.mark.asyncio + async def test_conversation_timeout_keeps_extending(self, app, bot, user1): + handler = ConversationHandler( + entry_points=self.entry_points, + states=self.states, + fallbacks=self.fallbacks, + conversation_timeout=0.5, + ) + app.add_handler(handler) + + # Start state machine, wait, do something, verify the timeout is extended. + # t=0 /start (timeout=.5) + # t=.35 /brew (timeout=.85) + # t=.5 original timeout + # t=.6 /pourCoffee (timeout=1.1) + # t=.85 second timeout + # t=1.1 actual timeout + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + + async with app: + await app.start() + + await app.process_update(Update(update_id=0, message=message)) + message.text = '/brew' + message.entities[0].length = len('/brew') + assert handler.check_update(Update(0, message=message)) + await asyncio.sleep(0.35) # t=.35 + assert handler.check_update(Update(0, message=message)) + await app.process_update(Update(update_id=0, message=message)) + message.text = '/pourCoffee' + message.entities[0].length = len('/pourCoffee') + assert handler.check_update(Update(0, message=message)) + await asyncio.sleep(0.25) # t=.6 + assert handler.check_update(Update(0, message=message)) + await app.process_update(Update(update_id=0, message=message)) + message.text = '/startCoding' + message.entities[0].length = len('/startCoding') + assert handler.check_update(Update(0, message=message)) + await asyncio.sleep(0.4) # t=1.0 + assert handler.check_update(Update(0, message=message)) + await asyncio.sleep(0.3) # t=1.3 + assert not handler.check_update(Update(0, message=message)) + message.text = '/start' + message.entities[0].length = len('/start') + assert handler.check_update(Update(0, message=message)) + + await app.stop() + + @pytest.mark.asyncio + async def test_conversation_timeout_two_users(self, app, bot, user1, user2): + handler = ConversationHandler( + entry_points=self.entry_points, + states=self.states, + fallbacks=self.fallbacks, + conversation_timeout=0.5, + ) + app.add_handler(handler) + + # Start state machine, do something as second user, then reach timeout + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + + async with app: + await app.start() + + await app.process_update(Update(update_id=0, message=message)) + message.text = '/brew' + message.entities[0].length = len('/brew') + assert handler.check_update(Update(0, message=message)) + message.from_user = user2 + await app.process_update(Update(update_id=0, message=message)) + message.text = '/start' + message.entities[0].length = len('/start') + # Make sure that user2s conversation has not yet started + assert handler.check_update(Update(0, message=message)) + await app.process_update(Update(update_id=0, message=message)) + message.text = '/brew' + message.entities[0].length = len('/brew') + assert handler.check_update(Update(0, message=message)) + await asyncio.sleep(0.7) + # check that both conversations have ended by checking that the start message is + # accepted again + message.text = '/start' + message.entities[0].length = len('/start') + message.from_user = user1 + assert handler.check_update(Update(0, message=message)) + message.from_user = user2 + assert handler.check_update(Update(0, message=message)) + + await app.stop() + # TODO # @pytest.mark.asyncio - # async def test_timeout_not_triggered_on_conv_end_async(self, bot, app, user1): - # def timeout(*a, **kw): - # self.test_flag = True - # - # self.states.update({ConversationHandler.TIMEOUT: [TypeHandler(Update, timeout)]}) - # handler = ConversationHandler( - # entry_points=self.entry_points, - # states=self.states, - # fallbacks=self.fallbacks, - # conversation_timeout=0.5, - # block=False, - # ) - # app.add_handler(handler) - # - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # # start the conversation - # await app.process_update(Update(update_id=0, message=message)) - # await asyncio.sleep(0.1) - # message.text = '/brew' - # message.entities[0].length = len('/brew') - # await app.process_update(Update(update_id=1, message=message)) - # await asyncio.sleep(0.1) - # message.text = '/pourCoffee' - # message.entities[0].length = len('/pourCoffee') - # await app.process_update(Update(update_id=2, message=message)) - # await asyncio.sleep(0.1) - # message.text = '/end' - # message.entities[0].length = len('/end') - # await app.process_update(Update(update_id=3, message=message)) - # await asyncio.sleep(1) - # # assert timeout handler didn't got called - # assert self.test_flag is False - # - # @pytest.mark.asyncio - # async def test_conversation_timeout_dispatcher_handler_stop(self, app, bot, user1, recwarn): - # handler = ConversationHandler( - # entry_points=self.entry_points, - # states=self.states, - # fallbacks=self.fallbacks, - # conversation_timeout=0.5, - # ) - # - # def timeout(*args, **kwargs): - # raise DispatcherHandlerStop() - # - # self.states.update({ConversationHandler.TIMEOUT: [TypeHandler(Update, timeout)]}) - # app.add_handler(handler) - # - # # Start state machine, then reach timeout - # message = Message( - # 0, - # None, - # self.group, - # text='/start', - # from_user=user1, - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # - # await app.process_update(Update(update_id=0, message=message)) - # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY - # await asyncio.sleep(0.9) - # assert handler.conversations.get((self.group.id, user1.id)) is None - # assert len(recwarn) == 1 - # assert str(recwarn[0].message).startswith('DispatcherHandlerStop in TIMEOUT') - # - # @pytest.mark.asyncio - # async def test_conversation_handler_timeout_update_and_context(self, app, bot, user1): - # context = None - # - # def start_callback(u, c): - # nonlocal context, self - # context = c - # return self.start(u, c) - # - # states = self.states - # timeout_handler = CommandHandler('start', None) - # states.update({ConversationHandler.TIMEOUT: [timeout_handler]}) - # handler = ConversationHandler( - # entry_points=[CommandHandler('start', start_callback)], - # states=states, - # fallbacks=self.fallbacks, - # conversation_timeout=0.5, - # ) - # app.add_handler(handler) - # - # # Start state machine, then reach timeout - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # update = Update(update_id=0, message=message) - # - # def timeout_callback(u, c): - # nonlocal update, context, self - # self.is_timeout = True - # assert u is update - # assert c is context - # - # timeout_handler.callback = timeout_callback - # - # await app.process_update(update) - # await asyncio.sleep(0.7) - # assert handler.conversations.get((self.group.id, user1.id)) is None - # assert self.is_timeout - # - # @flaky(3, 1) - # @pytest.mark.asyncio - # async def test_conversation_timeout_keeps_extending(self, app, bot, user1): - # handler = ConversationHandler( - # entry_points=self.entry_points, - # states=self.states, - # fallbacks=self.fallbacks, - # conversation_timeout=0.5, - # ) - # app.add_handler(handler) - # - # # Start state machine, wait, do something, verify the timeout is extended. - # # t=0 /start (timeout=.5) - # # t=.35 /brew (timeout=.85) - # # t=.5 original timeout - # # t=.6 /pourCoffee (timeout=1.1) - # # t=.85 second timeout - # # t=1.1 actual timeout - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # await app.process_update(Update(update_id=0, message=message)) - # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY - # await asyncio.sleep(0.35) # t=.35 - # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY - # message.text = '/brew' - # message.entities[0].length = len('/brew') - # await app.process_update(Update(update_id=0, message=message)) - # assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING - # await asyncio.sleep(0.25) # t=.6 - # assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING - # message.text = '/pourCoffee' - # message.entities[0].length = len('/pourCoffee') - # await app.process_update(Update(update_id=0, message=message)) - # assert handler.conversations.get((self.group.id, user1.id)) == self.DRINKING - # await asyncio.sleep(0.4) # t=1.0 - # assert handler.conversations.get((self.group.id, user1.id)) == self.DRINKING - # await asyncio.sleep(0.3) # t=1.3 - # assert handler.conversations.get((self.group.id, user1.id)) is None - # - # @pytest.mark.asyncio - # async def test_conversation_timeout_two_users(self, app, bot, user1, user2): - # handler = ConversationHandler( - # entry_points=self.entry_points, - # states=self.states, - # fallbacks=self.fallbacks, - # conversation_timeout=0.5, - # ) - # app.add_handler(handler) - # - # # Start state machine, do something as second user, then reach timeout - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # await app.process_update(Update(update_id=0, message=message)) - # assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY - # message.text = '/brew' - # message.entities[0].length = len('/brew') - # message.entities[0].length = len('/brew') - # message.from_user = user2 - # await app.process_update(Update(update_id=0, message=message)) - # assert handler.conversations.get((self.group.id, user2.id)) is None - # message.text = '/start' - # message.entities[0].length = len('/start') - # await app.process_update(Update(update_id=0, message=message)) - # assert handler.conversations.get((self.group.id, user2.id)) == self.THIRSTY - # await asyncio.sleep(0.7) - # assert handler.conversations.get((self.group.id, user1.id)) is None - # assert handler.conversations.get((self.group.id, user2.id)) is None - # - # @pytest.mark.asyncio # async def test_conversation_handler_timeout_state(self, app, bot, user1): # states = self.states # states.update( @@ -1647,7 +1702,7 @@ async def test_conversation_timeout(self, app, bot, user1): # # app.add_handler(handler) # app.add_handler(TypeHandler(Update, test_callback), group=1) - # self.raise_dp_handler_stop = True + # self.raise_app_handler_stop = True # # # User one, starts the state machine. # message = Message( From 3294bd5693d7bf9c96cb3bcdd351bd765466901f Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Tue, 12 Apr 2022 20:41:00 +0200 Subject: [PATCH 18/25] new tests for app.run --- tests/test_application.py | 45 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/test_application.py b/tests/test_application.py index 4d333f93254..cd76034becc 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1609,3 +1609,48 @@ def test_run_without_updater(self, bot): with pytest.raises(RuntimeError, match='only available if the application has an Updater'): app.run_polling() + + @pytest.mark.parametrize('method', ['start', 'initialize']) + def test_run_error_in_application(self, bot, monkeypatch, method): + shutdowns = [] + + async def raise_method(*args, **kwargs): + raise RuntimeError('Test Exception') + + async def shutdown(*args, **kwargs): + shutdowns.append(True) + + monkeypatch.setattr(Application, method, raise_method) + monkeypatch.setattr(Application, 'shutdown', shutdown) + monkeypatch.setattr(Updater, 'shutdown', shutdown) + app = ApplicationBuilder().token(bot.token).build() + with pytest.raises(RuntimeError, match='Test Exception'): + app.run_polling(close_loop=False) + + assert not app.running + assert not app.updater.running + assert shutdowns == [True, True] + + @pytest.mark.parametrize('method', ['start_polling', 'start_webhook']) + def test_run_error_in_updater(self, bot, monkeypatch, method): + shutdowns = [] + + async def raise_method(*args, **kwargs): + raise RuntimeError('Test Exception') + + async def shutdown(*args, **kwargs): + shutdowns.append(True) + + monkeypatch.setattr(Updater, method, raise_method) + monkeypatch.setattr(Application, 'shutdown', shutdown) + monkeypatch.setattr(Updater, 'shutdown', shutdown) + app = ApplicationBuilder().token(bot.token).build() + with pytest.raises(RuntimeError, match='Test Exception'): + if 'polling' in method: + app.run_polling(close_loop=False) + else: + app.run_webhook(close_loop=False) + + assert not app.running + assert not app.updater.running + assert shutdowns == [True, True] From 7f649c36ef09032c2fde384da48d3c23869e18f0 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Tue, 12 Apr 2022 21:09:45 +0200 Subject: [PATCH 19/25] adjust remaining ch timeout tests --- tests/test_conversationhandler.py | 387 ++++++++++++++++-------------- 1 file changed, 212 insertions(+), 175 deletions(-) diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py index 99708583f59..47c0057c24c 100644 --- a/tests/test_conversationhandler.py +++ b/tests/test_conversationhandler.py @@ -83,6 +83,7 @@ class TestConversationHandler: # TODO # * Test that we have a warning when conversation timeout is scheduled with non-running JQ + # * Test the blocking/non-blocking behavior including the different resolution orders # State definitions # At first we're thirsty. Then we brew coffee, we drink it @@ -1390,183 +1391,219 @@ async def test_conversation_timeout_two_users(self, app, bot, user1, user2): await app.stop() + @pytest.mark.asyncio + async def test_conversation_handler_timeout_state(self, app, bot, user1): + states = self.states + states.update( + { + ConversationHandler.TIMEOUT: [ + CommandHandler('brew', self.passout), + MessageHandler(~filters.Regex('oding'), self.passout2), + ] + } + ) + handler = ConversationHandler( + entry_points=self.entry_points, + states=states, + fallbacks=self.fallbacks, + conversation_timeout=0.5, + ) + app.add_handler(handler) + + # CommandHandler timeout + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + + async with app: + await app.start() + + await app.process_update(Update(update_id=0, message=message)) + message.text = '/brew' + message.entities[0].length = len('/brew') + await app.process_update(Update(update_id=0, message=message)) + await asyncio.sleep(0.7) + # check that conversation has ended by checking that start cmd is accepted again + message.text = '/start' + message.entities[0].length = len('/start') + assert handler.check_update(Update(0, message=message)) + assert self.is_timeout + + # MessageHandler timeout + self.is_timeout = False + message.text = '/start' + message.entities[0].length = len('/start') + await app.process_update(Update(update_id=1, message=message)) + await asyncio.sleep(0.7) + # check that conversation has ended by checking that start cmd is accepted again + assert handler.check_update(Update(0, message=message)) + assert self.is_timeout + + # Timeout but no valid handler + self.is_timeout = False + await app.process_update(Update(update_id=0, message=message)) + message.text = '/brew' + message.entities[0].length = len('/brew') + await app.process_update(Update(update_id=0, message=message)) + message.text = '/startCoding' + message.entities[0].length = len('/startCoding') + await app.process_update(Update(update_id=0, message=message)) + await asyncio.sleep(0.7) + # check that conversation has ended by checking that start cmd is accepted again + message.text = '/start' + message.entities[0].length = len('/start') + assert handler.check_update(Update(0, message=message)) + assert not self.is_timeout + + await app.stop() + + @pytest.mark.asyncio + async def test_conversation_handler_timeout_state_context(self, app, bot, user1): + states = self.states + states.update( + { + ConversationHandler.TIMEOUT: [ + CommandHandler('brew', self.passout_context), + MessageHandler(~filters.Regex('oding'), self.passout2_context), + ] + } + ) + handler = ConversationHandler( + entry_points=self.entry_points, + states=states, + fallbacks=self.fallbacks, + conversation_timeout=0.5, + ) + app.add_handler(handler) + + # CommandHandler timeout + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + async with app: + await app.start() + + await app.process_update(Update(update_id=0, message=message)) + message.text = '/brew' + message.entities[0].length = len('/brew') + await app.process_update(Update(update_id=0, message=message)) + await asyncio.sleep(0.7) + # check that conversation has ended by checking that start cmd is accepted again + message.text = '/start' + message.entities[0].length = len('/start') + assert handler.check_update(Update(0, message=message)) + assert self.is_timeout + + # MessageHandler timeout + self.is_timeout = False + message.text = '/start' + message.entities[0].length = len('/start') + await app.process_update(Update(update_id=1, message=message)) + await asyncio.sleep(0.7) + # check that conversation has ended by checking that start cmd is accepted again + assert handler.check_update(Update(0, message=message)) + assert self.is_timeout + + # Timeout but no valid handler + self.is_timeout = False + await app.process_update(Update(update_id=0, message=message)) + message.text = '/brew' + message.entities[0].length = len('/brew') + await app.process_update(Update(update_id=0, message=message)) + message.text = '/startCoding' + message.entities[0].length = len('/startCoding') + await app.process_update(Update(update_id=0, message=message)) + await asyncio.sleep(0.7) + # check that conversation has ended by checking that start cmd is accepted again + message.text = '/start' + message.entities[0].length = len('/start') + assert handler.check_update(Update(0, message=message)) + assert not self.is_timeout + + await app.stop() + + @pytest.mark.asyncio + async def test_conversation_timeout_cancel_conflict(self, app, bot, user1): + # Start state machine, wait half the timeout, + # then call a callback that takes more than the timeout + # t=0 /start (timeout=.5) + # t=.25 /slowbrew (sleep .5) + # | t=.5 original timeout (should not execute) + # | t=.75 /slowbrew returns (timeout=1.25) + # t=1.25 timeout + + async def slowbrew(_update, context): + await asyncio.sleep(0.25) + # Let's give to the original timeout a chance to execute + await asyncio.sleep(0.25) + # By returning None we do not override the conversation state so + # we can see if the timeout has been executed + + states = self.states + states[self.THIRSTY].append(CommandHandler('slowbrew', slowbrew)) + states.update({ConversationHandler.TIMEOUT: [MessageHandler(None, self.passout2)]}) + + handler = ConversationHandler( + entry_points=self.entry_points, + states=states, + fallbacks=self.fallbacks, + conversation_timeout=0.5, + ) + app.add_handler(handler) + + # CommandHandler timeout + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + bot=bot, + ) + + async with app: + await app.start() + await app.process_update(Update(update_id=0, message=message)) + await asyncio.sleep(0.25) + message.text = '/slowbrew' + message.entities[0].length = len('/slowbrew') + await app.process_update(Update(update_id=0, message=message)) + # Check that conversation has not ended by checking that start cmd is not accepted + message.text = '/start' + message.entities[0].length = len('/start') + assert not handler.check_update(Update(0, message=message)) + assert not self.is_timeout + + await asyncio.sleep(0.7) + # Check that conversation has ended by checking that start cmd is accepted again + message.text = '/start' + message.entities[0].length = len('/start') + assert handler.check_update(Update(0, message=message)) + assert self.is_timeout + + await app.stop() + # TODO # @pytest.mark.asyncio - # async def test_conversation_handler_timeout_state(self, app, bot, user1): - # states = self.states - # states.update( - # { - # ConversationHandler.TIMEOUT: [ - # CommandHandler('brew', self.passout), - # MessageHandler(~filters.Regex('oding'), self.passout2), - # ] - # } - # ) - # handler = ConversationHandler( - # entry_points=self.entry_points, - # states=states, - # fallbacks=self.fallbacks, - # conversation_timeout=0.5, - # ) - # app.add_handler(handler) - # - # # CommandHandler timeout - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # await app.process_update(Update(update_id=0, message=message)) - # message.text = '/brew' - # message.entities[0].length = len('/brew') - # await app.process_update(Update(update_id=0, message=message)) - # await asyncio.sleep(0.7) - # assert handler.conversations.get((self.group.id, user1.id)) is None - # assert self.is_timeout - # - # # MessageHandler timeout - # self.is_timeout = False - # message.text = '/start' - # message.entities[0].length = len('/start') - # await app.process_update(Update(update_id=1, message=message)) - # await asyncio.sleep(0.7) - # assert handler.conversations.get((self.group.id, user1.id)) is None - # assert self.is_timeout - # - # # Timeout but no valid handler - # self.is_timeout = False - # await app.process_update(Update(update_id=0, message=message)) - # message.text = '/brew' - # message.entities[0].length = len('/brew') - # await app.process_update(Update(update_id=0, message=message)) - # message.text = '/startCoding' - # message.entities[0].length = len('/startCoding') - # await app.process_update(Update(update_id=0, message=message)) - # await asyncio.sleep(0.7) - # assert handler.conversations.get((self.group.id, user1.id)) is None - # assert not self.is_timeout - # - # @pytest.mark.asyncio - # async def test_conversation_handler_timeout_state_context(self, app, bot, user1): - # states = self.states - # states.update( - # { - # ConversationHandler.TIMEOUT: [ - # CommandHandler('brew', self.passout_context), - # MessageHandler(~filters.Regex('oding'), self.passout2_context), - # ] - # } - # ) - # handler = ConversationHandler( - # entry_points=self.entry_points, - # states=states, - # fallbacks=self.fallbacks, - # conversation_timeout=0.5, - # ) - # app.add_handler(handler) - # - # # CommandHandler timeout - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # await app.process_update(Update(update_id=0, message=message)) - # message.text = '/brew' - # message.entities[0].length = len('/brew') - # await app.process_update(Update(update_id=0, message=message)) - # await asyncio.sleep(0.7) - # assert handler.conversations.get((self.group.id, user1.id)) is None - # assert self.is_timeout - # - # # MessageHandler timeout - # self.is_timeout = False - # message.text = '/start' - # message.entities[0].length = len('/start') - # await app.process_update(Update(update_id=1, message=message)) - # await asyncio.sleep(0.7) - # assert handler.conversations.get((self.group.id, user1.id)) is None - # assert self.is_timeout - # - # # Timeout but no valid handler - # self.is_timeout = False - # await app.process_update(Update(update_id=0, message=message)) - # message.text = '/brew' - # message.entities[0].length = len('/brew') - # await app.process_update(Update(update_id=0, message=message)) - # message.text = '/startCoding' - # message.entities[0].length = len('/startCoding') - # await app.process_update(Update(update_id=0, message=message)) - # await asyncio.sleep(0.7) - # assert handler.conversations.get((self.group.id, user1.id)) is None - # assert not self.is_timeout - # - # @pytest.mark.asyncio - # async def test_conversation_timeout_cancel_conflict(self, app, bot, user1): - # # Start state machine, wait half the timeout, - # # then call a callback that takes more than the timeout - # # t=0 /start (timeout=.5) - # # t=.25 /slowbrew (sleep .5) - # # | t=.5 original timeout (should not execute) - # # | t=.75 /slowbrew returns (timeout=1.25) - # # t=1.25 timeout - # - # def slowbrew(_update, context): - # await asyncio.sleep(0.25) - # # Let's give to the original timeout a chance to execute - # await asyncio.sleep(0.25) - # # By returning None we do not override the conversation state so - # # we can see if the timeout has been executed - # - # states = self.states - # states[self.THIRSTY].append(CommandHandler('slowbrew', slowbrew)) - # states.update({ConversationHandler.TIMEOUT: [MessageHandler(None, self.passout2)]}) - # - # handler = ConversationHandler( - # entry_points=self.entry_points, - # states=states, - # fallbacks=self.fallbacks, - # conversation_timeout=0.5, - # ) - # app.add_handler(handler) - # - # # CommandHandler timeout - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # bot=bot, - # ) - # await app.process_update(Update(update_id=0, message=message)) - # await asyncio.sleep(0.25) - # message.text = '/slowbrew' - # message.entities[0].length = len('/slowbrew') - # await app.process_update(Update(update_id=0, message=message)) - # assert handler.conversations.get((self.group.id, user1.id)) is not None - # assert not self.is_timeout - # - # await asyncio.sleep(0.7) - # assert handler.conversations.get((self.group.id, user1.id)) is None - # assert self.is_timeout - # - # @pytest.mark.asyncio # async def test_nested_conversation_handler(self, app, bot, user1, user2): # self.nested_states[self.DRINKING] = [ # ConversationHandler( From e98a78794a7d9170558a7eee25b60721faed796a Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Tue, 12 Apr 2022 21:40:40 +0200 Subject: [PATCH 20/25] adjust tests for nested conversations --- tests/test_conversationhandler.py | 525 +++++++++++++++--------------- 1 file changed, 270 insertions(+), 255 deletions(-) diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py index 47c0057c24c..9941d9c4caa 100644 --- a/tests/test_conversationhandler.py +++ b/tests/test_conversationhandler.py @@ -84,6 +84,7 @@ class TestConversationHandler: # TODO # * Test that we have a warning when conversation timeout is scheduled with non-running JQ # * Test the blocking/non-blocking behavior including the different resolution orders + # * test AppHandlerStop with non-nested conversations # State definitions # At first we're thirsty. Then we brew coffee, we drink it @@ -1602,263 +1603,277 @@ async def slowbrew(_update, context): await app.stop() + @pytest.mark.asyncio + async def test_nested_conversation_handler(self, app, bot, user1, user2): + self.nested_states[self.DRINKING] = [ + ConversationHandler( + entry_points=self.drinking_entry_points, + states=self.drinking_states, + fallbacks=self.drinking_fallbacks, + map_to_parent=self.drinking_map_to_parent, + ) + ] + handler = ConversationHandler( + entry_points=self.entry_points, states=self.nested_states, fallbacks=self.fallbacks + ) + app.add_handler(handler) + + # User one, starts the state machine. + message = Message( + 0, + None, + self.group, + from_user=user1, + text='/start', + bot=bot, + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + ) + async with app: + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.THIRSTY + + # The user is thirsty and wants to brew coffee. + message.text = '/brew' + message.entities[0].length = len('/brew') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.BREWING + + # Lets pour some coffee. + message.text = '/pourCoffee' + message.entities[0].length = len('/pourCoffee') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.DRINKING + + # The user is holding the cup + message.text = '/hold' + message.entities[0].length = len('/hold') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.HOLDING + + # The user is sipping coffee + message.text = '/sip' + message.entities[0].length = len('/sip') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.SIPPING + + # The user is swallowing + message.text = '/swallow' + message.entities[0].length = len('/swallow') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.SWALLOWING + + # The user is holding the cup again + message.text = '/hold' + message.entities[0].length = len('/hold') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.HOLDING + + # The user wants to replenish the coffee supply + message.text = '/replenish' + message.entities[0].length = len('/replenish') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.REPLENISHING + # check that we're in the right state now by checking that the update is accepted + message.text = '/pourCoffee' + message.entities[0].length = len('/pourCoffee') + assert handler.check_update(Update(0, message=message)) + + # The user wants to drink their coffee again) + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.DRINKING + + # The user is now ready to start coding + message.text = '/startCoding' + message.entities[0].length = len('/startCoding') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.CODING + + # The user decides it's time to drink again + message.text = '/drinkMore' + message.entities[0].length = len('/drinkMore') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.DRINKING + + # The user is holding their cup + message.text = '/hold' + message.entities[0].length = len('/hold') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.HOLDING + + # The user wants to end with the drinking and go back to coding + message.text = '/end' + message.entities[0].length = len('/end') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.END + # check that we're in the right state now by checking that the update is accepted + message.text = '/drinkMore' + message.entities[0].length = len('/drinkMore') + assert handler.check_update(Update(0, message=message)) + + # The user wants to drink once more + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.DRINKING + + # The user wants to stop altogether + message.text = '/stop' + message.entities[0].length = len('/stop') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.STOPPING + # check that the conversation has ended by checking that the start cmd is accepted + message.text = '/start' + message.entities[0].length = len('/start') + assert handler.check_update(Update(0, message=message)) + + @pytest.mark.asyncio + async def test_nested_conversation_application_handler_stop(self, app, bot, user1, user2): + self.nested_states[self.DRINKING] = [ + ConversationHandler( + entry_points=self.drinking_entry_points, + states=self.drinking_states, + fallbacks=self.drinking_fallbacks, + map_to_parent=self.drinking_map_to_parent, + ) + ] + handler = ConversationHandler( + entry_points=self.entry_points, states=self.nested_states, fallbacks=self.fallbacks + ) + + def test_callback(u, c): + self.test_flag = True + + app.add_handler(handler) + app.add_handler(TypeHandler(Update, test_callback), group=1) + self.raise_app_handler_stop = True + + # User one, starts the state machine. + message = Message( + 0, + None, + self.group, + text='/start', + bot=bot, + from_user=user1, + entities=[ + MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) + ], + ) + async with app: + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.THIRSTY + assert not self.test_flag + + # The user is thirsty and wants to brew coffee. + message.text = '/brew' + message.entities[0].length = len('/brew') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.BREWING + assert not self.test_flag + + # Lets pour some coffee. + message.text = '/pourCoffee' + message.entities[0].length = len('/pourCoffee') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.DRINKING + assert not self.test_flag + + # The user is holding the cup + message.text = '/hold' + message.entities[0].length = len('/hold') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.HOLDING + assert not self.test_flag + + # The user is sipping coffee + message.text = '/sip' + message.entities[0].length = len('/sip') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.SIPPING + assert not self.test_flag + + # The user is swallowing + message.text = '/swallow' + message.entities[0].length = len('/swallow') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.SWALLOWING + assert not self.test_flag + + # The user is holding the cup again + message.text = '/hold' + message.entities[0].length = len('/hold') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.HOLDING + assert not self.test_flag + + # The user wants to replenish the coffee supply + message.text = '/replenish' + message.entities[0].length = len('/replenish') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.REPLENISHING + # check that we're in the right state now by checking that the update is accepted + message.text = '/pourCoffee' + message.entities[0].length = len('/pourCoffee') + assert handler.check_update(Update(0, message=message)) + assert not self.test_flag + + # The user wants to drink their coffee again + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.DRINKING + assert not self.test_flag + + # The user is now ready to start coding + message.text = '/startCoding' + message.entities[0].length = len('/startCoding') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.CODING + assert not self.test_flag + + # The user decides it's time to drink again + message.text = '/drinkMore' + message.entities[0].length = len('/drinkMore') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.DRINKING + assert not self.test_flag + + # The user is holding their cup + message.text = '/hold' + message.entities[0].length = len('/hold') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.HOLDING + assert not self.test_flag + + # The user wants to end with the drinking and go back to coding + message.text = '/end' + message.entities[0].length = len('/end') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.END + # check that we're in the right state now by checking that the update is accepted + message.text = '/drinkMore' + message.entities[0].length = len('/drinkMore') + assert handler.check_update(Update(0, message=message)) + assert not self.test_flag + + # The user wants to drink once more + message.text = '/drinkMore' + message.entities[0].length = len('/drinkMore') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.DRINKING + assert not self.test_flag + + # The user wants to stop altogether + message.text = '/stop' + message.entities[0].length = len('/stop') + await app.process_update(Update(update_id=0, message=message)) + assert self.current_state[user1.id] == self.STOPPING + # check that the conv has ended by checking that the start cmd is accepted + message.text = '/start' + message.entities[0].length = len('/start') + assert handler.check_update(Update(0, message=message)) + assert not self.test_flag + # TODO # @pytest.mark.asyncio - # async def test_nested_conversation_handler(self, app, bot, user1, user2): - # self.nested_states[self.DRINKING] = [ - # ConversationHandler( - # entry_points=self.drinking_entry_points, - # states=self.drinking_states, - # fallbacks=self.drinking_fallbacks, - # map_to_parent=self.drinking_map_to_parent, - # ) - # ] - # handler = ConversationHandler( - # entry_points=self.entry_points, states=self.nested_states, fallbacks=self.fallbacks - # ) - # app.add_handler(handler) - # - # # User one, starts the state machine. - # message = Message( - # 0, - # None, - # self.group, - # from_user=user1, - # text='/start', - # bot=bot, - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # ) - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.THIRSTY - # - # # The user is thirsty and wants to brew coffee. - # message.text = '/brew' - # message.entities[0].length = len('/brew') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.BREWING - # - # # Lets pour some coffee. - # message.text = '/pourCoffee' - # message.entities[0].length = len('/pourCoffee') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.DRINKING - # - # # The user is holding the cup - # message.text = '/hold' - # message.entities[0].length = len('/hold') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.HOLDING - # - # # The user is sipping coffee - # message.text = '/sip' - # message.entities[0].length = len('/sip') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.SIPPING - # - # # The user is swallowing - # message.text = '/swallow' - # message.entities[0].length = len('/swallow') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.SWALLOWING - # - # # The user is holding the cup again - # message.text = '/hold' - # message.entities[0].length = len('/hold') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.HOLDING - # - # # The user wants to replenish the coffee supply - # message.text = '/replenish' - # message.entities[0].length = len('/replenish') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.REPLENISHING - # assert handler.conversations[(0, user1.id)] == self.BREWING - # - # # The user wants to drink their coffee again - # message.text = '/pourCoffee' - # message.entities[0].length = len('/pourCoffee') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.DRINKING - # - # # The user is now ready to start coding - # message.text = '/startCoding' - # message.entities[0].length = len('/startCoding') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.CODING - # - # # The user decides it's time to drink again - # message.text = '/drinkMore' - # message.entities[0].length = len('/drinkMore') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.DRINKING - # - # # The user is holding their cup - # message.text = '/hold' - # message.entities[0].length = len('/hold') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.HOLDING - # - # # The user wants to end with the drinking and go back to coding - # message.text = '/end' - # message.entities[0].length = len('/end') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.END - # assert handler.conversations[(0, user1.id)] == self.CODING - # - # # The user wants to drink once more - # message.text = '/drinkMore' - # message.entities[0].length = len('/drinkMore') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.DRINKING - # - # # The user wants to stop altogether - # message.text = '/stop' - # message.entities[0].length = len('/stop') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.STOPPING - # assert handler.conversations.get((0, user1.id)) is None - # - # @pytest.mark.asyncio - # async def test_conversation_dispatcher_handler_stop(self, app, bot, user1, user2): - # self.nested_states[self.DRINKING] = [ - # ConversationHandler( - # entry_points=self.drinking_entry_points, - # states=self.drinking_states, - # fallbacks=self.drinking_fallbacks, - # map_to_parent=self.drinking_map_to_parent, - # ) - # ] - # handler = ConversationHandler( - # entry_points=self.entry_points, states=self.nested_states, fallbacks=self.fallbacks - # ) - # - # def test_callback(u, c): - # self.test_flag = True - # - # app.add_handler(handler) - # app.add_handler(TypeHandler(Update, test_callback), group=1) - # self.raise_app_handler_stop = True - # - # # User one, starts the state machine. - # message = Message( - # 0, - # None, - # self.group, - # text='/start', - # bot=bot, - # from_user=user1, - # entities=[ - # MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len('/start')) - # ], - # ) - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.THIRSTY - # assert not self.test_flag - # - # # The user is thirsty and wants to brew coffee. - # message.text = '/brew' - # message.entities[0].length = len('/brew') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.BREWING - # assert not self.test_flag - # - # # Lets pour some coffee. - # message.text = '/pourCoffee' - # message.entities[0].length = len('/pourCoffee') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.DRINKING - # assert not self.test_flag - # - # # The user is holding the cup - # message.text = '/hold' - # message.entities[0].length = len('/hold') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.HOLDING - # assert not self.test_flag - # - # # The user is sipping coffee - # message.text = '/sip' - # message.entities[0].length = len('/sip') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.SIPPING - # assert not self.test_flag - # - # # The user is swallowing - # message.text = '/swallow' - # message.entities[0].length = len('/swallow') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.SWALLOWING - # assert not self.test_flag - # - # # The user is holding the cup again - # message.text = '/hold' - # message.entities[0].length = len('/hold') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.HOLDING - # assert not self.test_flag - # - # # The user wants to replenish the coffee supply - # message.text = '/replenish' - # message.entities[0].length = len('/replenish') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.REPLENISHING - # assert handler.conversations[(0, user1.id)] == self.BREWING - # assert not self.test_flag - # - # # The user wants to drink their coffee again - # message.text = '/pourCoffee' - # message.entities[0].length = len('/pourCoffee') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.DRINKING - # assert not self.test_flag - # - # # The user is now ready to start coding - # message.text = '/startCoding' - # message.entities[0].length = len('/startCoding') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.CODING - # assert not self.test_flag - # - # # The user decides it's time to drink again - # message.text = '/drinkMore' - # message.entities[0].length = len('/drinkMore') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.DRINKING - # assert not self.test_flag - # - # # The user is holding their cup - # message.text = '/hold' - # message.entities[0].length = len('/hold') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.HOLDING - # assert not self.test_flag - # - # # The user wants to end with the drinking and go back to coding - # message.text = '/end' - # message.entities[0].length = len('/end') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.END - # assert handler.conversations[(0, user1.id)] == self.CODING - # assert not self.test_flag - # - # # The user wants to drink once more - # message.text = '/drinkMore' - # message.entities[0].length = len('/drinkMore') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.DRINKING - # assert not self.test_flag - # - # # The user wants to stop altogether - # message.text = '/stop' - # message.entities[0].length = len('/stop') - # await app.process_update(Update(update_id=0, message=message)) - # assert self.current_state[user1.id] == self.STOPPING - # assert handler.conversations.get((0, user1.id)) is None - # assert not self.test_flag - # - # @pytest.mark.asyncio # async def test_conversation_handler_run_async_true(self, app): # conv_handler = ConversationHandler( # entry_points=self.entry_points, From c82cdf359976fdbacad2042edc693cd1cdd6a432 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Tue, 12 Apr 2022 21:46:28 +0200 Subject: [PATCH 21/25] test new ch-timeout-jq warning --- tests/test_conversationhandler.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py index 9941d9c4caa..11934bf21c8 100644 --- a/tests/test_conversationhandler.py +++ b/tests/test_conversationhandler.py @@ -937,7 +937,8 @@ async def test_all_update_types(self, app, bot, user1): assert not handler.check_update(Update(0, shipping_query=shipping_query)) @pytest.mark.asyncio - async def test_no_job_queue_warning(self, app, bot, user1, recwarn): + @pytest.mark.parametrize('jq', [True, False]) + async def test_no_running_job_queue_warning(self, app, bot, user1, recwarn, jq): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, @@ -947,7 +948,8 @@ async def test_no_job_queue_warning(self, app, bot, user1, recwarn): # save app.job_queue in temp variable jqueue # and then set app.job_queue to None. jqueue = app.job_queue - app.job_queue = None + if not jq: + app.job_queue = None app.add_handler(handler) message = Message( @@ -966,10 +968,8 @@ async def test_no_job_queue_warning(self, app, bot, user1, recwarn): await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.5) assert len(recwarn) == 1 - assert ( - str(recwarn[0].message) - == "Ignoring `conversation_timeout` because the Application has no JobQueue." - ) + assert str(recwarn[0].message).startswith("Ignoring `conversation_timeout`") + assert ("is not running" if jq else "has no JobQueue.") in str(recwarn[0].message) # now set app.job_queue back to it's original value app.job_queue = jqueue From 7234a36f34a1ee3c4e98e17d65c958439753c707 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 13 Apr 2022 10:51:36 +0200 Subject: [PATCH 22/25] fix a test --- tests/test_conversationhandler.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py index 11934bf21c8..33401d89370 100644 --- a/tests/test_conversationhandler.py +++ b/tests/test_conversationhandler.py @@ -1004,6 +1004,8 @@ class DictJB(JobQueue): ) async with app: + await app.start() + with caplog.at_level(logging.ERROR): await app.process_update(Update(update_id=0, message=message)) await asyncio.sleep(0.5) @@ -1012,6 +1014,8 @@ class DictJB(JobQueue): assert caplog.records[0].message == "Failed to schedule timeout." assert str(caplog.records[0].exc_info[1]) == "job error" + await app.stop() + @pytest.mark.asyncio async def test_non_blocking_exception(self, app, bot, user1, caplog): """Here we make sure that when a non-blocking handler raises an From 93a190b0a21ae5ec28e9c1b5f03c66c864b52f3d Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 13 Apr 2022 22:07:45 +0200 Subject: [PATCH 23/25] try stabilizing tests on macOS --- tests/test_basepersistence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_basepersistence.py b/tests/test_basepersistence.py index 201d45dd34a..6d83c396764 100644 --- a/tests/test_basepersistence.py +++ b/tests/test_basepersistence.py @@ -1259,7 +1259,7 @@ async def callback(_, __): stop_task = asyncio.create_task(papp.stop()) assert not stop_task.done() event.set() - await asyncio.sleep(0.05) + await asyncio.sleep(0.5) assert stop_task.done() assert papp.persistence.updated_conversations == {} From d81ef7718ff6bc0664f3dd81a5ed890febcfd1ed Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 16 Apr 2022 22:21:48 +0200 Subject: [PATCH 24/25] more ch tests --- tests/test_conversationhandler.py | 182 +++++++++++++++++++++++------- 1 file changed, 144 insertions(+), 38 deletions(-) diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py index 33401d89370..e0928ad2c48 100644 --- a/tests/test_conversationhandler.py +++ b/tests/test_conversationhandler.py @@ -35,6 +35,7 @@ ChosenInlineResult, ShippingQuery, PreCheckoutQuery, + Bot, ) from telegram.ext import ( ConversationHandler, @@ -54,8 +55,12 @@ InlineQueryHandler, PollAnswerHandler, ChosenInlineResultHandler, + Defaults, + ApplicationBuilder, + ExtBot, ) from telegram.warnings import PTBUserWarning +from tests.conftest import make_command_message @pytest.fixture(scope='class') @@ -82,9 +87,7 @@ class TestConversationHandler: """Persistence of conversations is tested in test_basepersistence.py""" # TODO - # * Test that we have a warning when conversation timeout is scheduled with non-running JQ # * Test the blocking/non-blocking behavior including the different resolution orders - # * test AppHandlerStop with non-nested conversations # State definitions # At first we're thirsty. Then we brew coffee, we drink it @@ -282,6 +285,12 @@ def test_init(self): assert ch.name == 'name' assert ch.allow_reentry == 'allow_reentry' + def test_init_persistent_no_name(self): + with pytest.raises(ValueError, match="can't be persistent when handler is unnamed"): + ConversationHandler( + self.entry_points, states=self.states, fallbacks=[], persistent=True + ) + @pytest.mark.asyncio async def test_handlers_generate_warning(self, recwarn): """this function tests all handler + per_* setting combinations.""" @@ -294,6 +303,8 @@ async def test_handlers_generate_warning(self, recwarn): class NotUpdate: pass + recwarn.clear() + # this conversation handler has the string, string_regex, Pollhandler and TypeHandler # which should all generate a warning no matter the per_* setting. TypeHandler should # not when the class is Update @@ -479,12 +490,19 @@ def test_per_all_false(self): ) @pytest.mark.asyncio - async def test_conversation_handler(self, app, bot, user1, user2): + @pytest.mark.parametrize('raise_ahs', [True, False]) + async def test_basic_and_app_handler_stop(self, app, bot, user1, user2, raise_ahs): handler = ConversationHandler( entry_points=self.entry_points, states=self.states, fallbacks=self.fallbacks ) app.add_handler(handler) + async def callback(_, __): + self.test_flag = True + + app.add_handler(TypeHandler(object, callback), group=100) + self.raise_app_handler_stop = raise_ahs + # User one, starts the state machine. message = Message( 0, @@ -500,24 +518,29 @@ async def test_conversation_handler(self, app, bot, user1, user2): async with app: await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.THIRSTY + assert self.test_flag == (not raise_ahs) # The user is thirsty and wants to brew coffee. message.text = '/brew' message.entities[0].length = len('/brew') await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.BREWING + assert self.test_flag == (not raise_ahs) # Lets see if an invalid command makes sure, no state is changed. message.text = '/nothing' message.entities[0].length = len('/nothing') await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.BREWING + assert self.test_flag is True + self.test_flag = False # Lets see if the state machine still works by pouring coffee. message.text = '/pourCoffee' message.entities[0].length = len('/pourCoffee') await app.process_update(Update(update_id=0, message=message)) assert self.current_state[user1.id] == self.DRINKING + assert self.test_flag == (not raise_ahs) # Let's now verify that for another user, who did not start yet, # the state has not been changed. @@ -1876,38 +1899,121 @@ def test_callback(u, c): assert handler.check_update(Update(0, message=message)) assert not self.test_flag - # TODO - # @pytest.mark.asyncio - # async def test_conversation_handler_run_async_true(self, app): - # conv_handler = ConversationHandler( - # entry_points=self.entry_points, - # states=self.states, - # fallbacks=self.fallbacks, - # block=False, - # ) - # - # all_handlers = conv_handler.entry_points + conv_handler.fallbacks - # for state_handlers in conv_handler.states.values(): - # all_handlers += state_handlers - # - # for handler in all_handlers: - # assert handler.run_async - # - # @pytest.mark.asyncio - # async def test_conversation_handler_run_async_false(self, app): - # conv_handler = ConversationHandler( - # entry_points=[CommandHandler('start', self.start_end, block=False)], - # states=self.states, - # fallbacks=self.fallbacks, - # run_async=False, - # ) - # - # for handler in conv_handler.entry_points: - # assert handler.run_async - # - # all_handlers = conv_handler.fallbacks - # for state_handlers in conv_handler.states.values(): - # all_handlers += state_handlers - # - # for handler in all_handlers: - # assert not handler.run_async.value + @pytest.mark.asyncio + async def test_conversation_handler_block_dont_override(self, app): + """This just makes sure that we don't change any attributes of the handlers of the conv""" + conv_handler = ConversationHandler( + entry_points=self.entry_points, + states=self.states, + fallbacks=self.fallbacks, + block=False, + ) + + all_handlers = conv_handler.entry_points + conv_handler.fallbacks + for state_handlers in conv_handler.states.values(): + all_handlers += state_handlers + + for handler in all_handlers: + assert handler.block + + conv_handler = ConversationHandler( + entry_points=[CommandHandler('start', self.start_end, block=False)], + states={1: [CommandHandler('start', self.start_end, block=False)]}, + fallbacks=[CommandHandler('start', self.start_end, block=False)], + block=True, + ) + + all_handlers = conv_handler.entry_points + conv_handler.fallbacks + for state_handlers in conv_handler.states.values(): + all_handlers += state_handlers + + for handler in all_handlers: + assert handler.block is False + + @pytest.mark.asyncio + @pytest.mark.parametrize('default_block', [True, False, None]) + @pytest.mark.parametrize('ch_block', [True, False, None]) + @pytest.mark.parametrize('handler_block', [True, False, None]) + @pytest.mark.parametrize('ext_bot', [True, False], ids=['ExtBot', 'Bot']) + async def test_blocking_resolution_order( + self, bot, default_block, ch_block, handler_block, ext_bot + ): + + event = asyncio.Event() + + async def callback(_, __): + await event.wait() + event.clear() + self.test_flag = True + return 1 + + if handler_block is not None: + handler = CommandHandler('start', callback=callback, block=handler_block) + fallback = MessageHandler(filters.ALL, callback, block=handler_block) + else: + handler = CommandHandler('start', callback=callback) + fallback = MessageHandler(filters.ALL, callback, block=handler_block) + + if default_block is not None: + defaults = Defaults(block=default_block) + else: + defaults = None + + if ch_block is not None: + conv_handler = ConversationHandler( + entry_points=[handler], + states={1: [handler]}, + fallbacks=[fallback], + block=ch_block, + ) + else: + conv_handler = ConversationHandler( + entry_points=[handler], + states={1: [handler]}, + fallbacks=[fallback], + ) + + bot = ExtBot(bot.token, defaults=defaults) if ext_bot else Bot(bot.token) + app = ApplicationBuilder().bot(bot).build() + app.add_handler(conv_handler) + + async with app: + start_message = make_command_message('/start', bot=bot) + fallback_message = make_command_message('/fallback', bot=bot) + + # This loop makes sure that we test all of entry points, states handler & fallbacks + for message in [start_message, start_message, fallback_message]: + process_update_task = asyncio.create_task( + app.process_update(Update(0, message=message)) + ) + if ( + # resolution order is handler_block -> ch_block -> default_block + # setting block=True/False on a lower priority setting may only have an effect + # if it wasn't set for the higher priority settings + (handler_block is False) + or ((handler_block is None) and (ch_block is False)) + or ( + (handler_block is None) + and (ch_block is None) + and ext_bot + and (default_block is False) + ) + ): + # check that the handler was called non-blocking by checking that + # `process_update` has finished + await asyncio.sleep(0.01) + assert process_update_task.done() + else: + # the opposite + assert not process_update_task.done() + + # In any case, the callback must not have finished + assert not self.test_flag + + # After setting the event, the callback must have finished and in the blocking + # case this leads to `process_update` finishing. + event.set() + await asyncio.sleep(0.01) + assert process_update_task.done() + assert self.test_flag + self.test_flag = False From c00b72b1038628011137f0a165a16f32120adc31 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Sat, 16 Apr 2022 23:35:22 +0200 Subject: [PATCH 25/25] increase coverage a bit --- tests/test_conversationhandler.py | 165 ++++++++++++++++++++++++++++-- 1 file changed, 157 insertions(+), 8 deletions(-) diff --git a/tests/test_conversationhandler.py b/tests/test_conversationhandler.py index e0928ad2c48..2827eac5ced 100644 --- a/tests/test_conversationhandler.py +++ b/tests/test_conversationhandler.py @@ -86,9 +86,6 @@ async def decorator(self, *args, **kwargs): class TestConversationHandler: """Persistence of conversations is tested in test_basepersistence.py""" - # TODO - # * Test the blocking/non-blocking behavior including the different resolution orders - # State definitions # At first we're thirsty. Then we brew coffee, we drink it # and then we can start coding! @@ -291,6 +288,16 @@ def test_init_persistent_no_name(self): self.entry_points, states=self.states, fallbacks=[], persistent=True ) + @pytest.mark.asyncio + async def test_check_update_returns_non(self, app, user1): + """checks some cases where updates should not be handled""" + conv_handler = ConversationHandler([], {}, [], per_message=True, per_chat=True) + assert not conv_handler.check_update('not an Update') + assert not conv_handler.check_update(Update(0)) + assert not conv_handler.check_update( + Update(0, callback_query=CallbackQuery('1', from_user=user1, chat_instance='1')) + ) + @pytest.mark.asyncio async def test_handlers_generate_warning(self, recwarn): """this function tests all handler + per_* setting combinations.""" @@ -766,7 +773,9 @@ async def test_conversation_handler_per_user(self, app, bot, user1): assert handler.check_update(Update(update_id=0, message=message)) @pytest.mark.asyncio - async def test_conversation_handler_per_message(self, app, bot, user1, user2): + @pytest.mark.parametrize('inline', [True, False]) + @pytest.mark.filterwarnings("ignore: If 'per_message=True' is used, 'per_chat=True'") + async def test_conversation_handler_per_message(self, app, bot, user1, user2, inline): async def entry(update, context): return 1 @@ -784,17 +793,37 @@ async def two(update, context): }, fallbacks=[], per_message=True, + per_chat=not inline, ) app.add_handler(handler) # User one, starts the state machine. - message = Message( - 0, None, self.group, from_user=user1, text='msg w/ inlinekeyboard', bot=bot + message = ( + Message(0, None, self.group, from_user=user1, text='msg w/ inlinekeyboard', bot=bot) + if not inline + else None ) + inline_message_id = '42' if inline else None async with app: - cbq_1 = CallbackQuery(0, user1, None, message=message, data='1', bot=bot) - cbq_2 = CallbackQuery(0, user1, None, message=message, data='2', bot=bot) + cbq_1 = CallbackQuery( + 0, + user1, + None, + message=message, + data='1', + bot=bot, + inline_message_id=inline_message_id, + ) + cbq_2 = CallbackQuery( + 0, + user1, + None, + message=message, + data='2', + bot=bot, + inline_message_id=inline_message_id, + ) await app.process_update(Update(update_id=0, callback_query=cbq_1)) # Make sure that we're in the correct state @@ -1899,6 +1928,73 @@ def test_callback(u, c): assert handler.check_update(Update(0, message=message)) assert not self.test_flag + @pytest.mark.asyncio + @pytest.mark.parametrize('callback_raises', [True, False]) + async def test_timeout_non_block(self, app, user1, callback_raises): + event = asyncio.Event() + + async def callback(_, __): + await event.wait() + if callback_raises: + raise RuntimeError + return 1 + + conv_handler = ConversationHandler( + entry_points=[MessageHandler(filters.ALL, callback=callback, block=False)], + states={ConversationHandler.TIMEOUT: [TypeHandler(Update, self.passout2)]}, + fallbacks=[], + conversation_timeout=0.5, + ) + app.add_handler(conv_handler) + + async with app: + await app.start() + + message = Message( + 0, + None, + self.group, + text='/start', + from_user=user1, + ) + assert conv_handler.check_update(Update(0, message=message)) + await app.process_update(Update(0, message=message)) + await asyncio.sleep(0.7) + assert not self.is_timeout + event.set() + await asyncio.sleep(0.7) + assert self.is_timeout == (not callback_raises) + + await app.stop() + + @pytest.mark.asyncio + async def test_no_timeout_on_end(self, app, user1): + + conv_handler = ConversationHandler( + entry_points=[MessageHandler(filters.ALL, callback=self.start_end)], + states={ConversationHandler.TIMEOUT: [TypeHandler(Update, self.passout2)]}, + fallbacks=[], + conversation_timeout=0.5, + ) + app.add_handler(conv_handler) + + async with app: + await app.start() + + message = Message( + 0, + None, + self.group, + text='/start', + from_user=user1, + ) + assert conv_handler.check_update(Update(0, message=message)) + await app.process_update(Update(0, message=message)) + await asyncio.sleep(0.7) + assert not self.is_timeout + + await app.stop() + @pytest.mark.asyncio async def test_conversation_handler_block_dont_override(self, app): """This just makes sure that we don't change any attributes of the handlers of the conv""" @@ -2017,3 +2113,56 @@ async def callback(_, __): assert process_update_task.done() assert self.test_flag self.test_flag = False + + @pytest.mark.asyncio + async def test_waiting_state(self, app, user1): + event = asyncio.Event() + + async def callback_1(_, __): + self.test_flag = 1 + + async def callback_2(_, __): + self.test_flag = 2 + + async def callback_3(_, __): + self.test_flag = 3 + + async def blocking(_, __): + await event.wait() + return 1 + + conv_handler = ConversationHandler( + entry_points=[MessageHandler(filters.ALL, callback=blocking, block=False)], + states={ + ConversationHandler.WAITING: [ + MessageHandler(filters.Regex('1'), callback_1), + MessageHandler(filters.Regex('2'), callback_2), + ], + 1: [MessageHandler(filters.Regex('2'), callback_3)], + }, + fallbacks=[], + ) + app.add_handler(conv_handler) + + message = Message( + 0, + None, + self.group, + text='/start', + from_user=user1, + ) + + async with app: + await app.process_update(Update(0, message=message)) + assert not self.test_flag + message.text = '1' + await app.process_update(Update(0, message=message)) + assert self.test_flag == 1 + message.text = '2' + await app.process_update(Update(0, message=message)) + assert self.test_flag == 2 + event.set() + await asyncio.sleep(0.05) + self.test_flag = None + await app.process_update(Update(0, message=message)) + assert self.test_flag == 3