From 8674d0dbbd54cb84c07e1889de012db414329b03 Mon Sep 17 00:00:00 2001 From: Genesis Date: Thu, 11 Aug 2016 09:23:52 +0200 Subject: [PATCH] UpdateTitleStats -> UpdateLiveStats, new stat, refactoring (#3467) * Renamed UpdateTitleStats to UpdateLiveStats * Cleaned worker documentation * Added documentation for terminal_log and terminal_title * Fixed https://github.com/PokemonGoF/PokemonGo-Bot/pull/3312#issuecomment-238672978 * Made some refactoring * Added captures_per_hour stat that shows estimated pokemon captures per hour * Added a captures_per_hour method in metrics.py * Added unit tests for features added in https://github.com/PokemonGoF/PokemonGo-Bot/pull/3312 * Added unit tests for captures_per_hour * Avoid useless overhead when no output configured * Added default config values in documentation * Fixed issue with title updating on Windows * See https://github.com/PokemonGoF/PokemonGo-Bot/pull/3472 --- pokemongo_bot/cell_workers/__init__.py | 2 +- ...te_title_stats.py => update_live_stats.py} | 109 +++++++++--------- pokemongo_bot/metrics.py | 8 ++ ...tats_test.py => update_live_stats_test.py} | 89 +++++++++----- 4 files changed, 126 insertions(+), 82 deletions(-) rename pokemongo_bot/cell_workers/{update_title_stats.py => update_live_stats.py} (81%) rename tests/{update_title_stats_test.py => update_live_stats_test.py} (58%) diff --git a/pokemongo_bot/cell_workers/__init__.py b/pokemongo_bot/cell_workers/__init__.py index 68d181947a..7933425b63 100644 --- a/pokemongo_bot/cell_workers/__init__.py +++ b/pokemongo_bot/cell_workers/__init__.py @@ -17,4 +17,4 @@ from collect_level_up_reward import CollectLevelUpReward from follow_cluster import FollowCluster from sleep_schedule import SleepSchedule -from update_title_stats import UpdateTitleStats +from update_live_stats import UpdateLiveStats diff --git a/pokemongo_bot/cell_workers/update_title_stats.py b/pokemongo_bot/cell_workers/update_live_stats.py similarity index 81% rename from pokemongo_bot/cell_workers/update_title_stats.py rename to pokemongo_bot/cell_workers/update_live_stats.py index bc40ed82e8..e51253edc5 100644 --- a/pokemongo_bot/cell_workers/update_title_stats.py +++ b/pokemongo_bot/cell_workers/update_live_stats.py @@ -6,27 +6,17 @@ from pokemongo_bot.worker_result import WorkerResult from pokemongo_bot.tree_config_builder import ConfigException -class UpdateTitleStats(BaseTask): + +class UpdateLiveStats(BaseTask): """ - Periodically updates the terminal title to display stats about the bot. + Periodically displays stats about the bot in the terminal and/or in its title. Fetching some stats requires making API calls. If you're concerned about the amount of calls your bot is making, don't enable this worker. Example config : { - "type": "UpdateTitleStats", - "config": { - "min_interval": 10, - "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"], - } - } - - You can set a logging on terminal mode like this: - - Example logging on console (and disabling title change): - { - "type": "UpdateTitleStats", + "type": "UpdateLiveStats", "config": { "min_interval": 10, "stats": ["login", "uptime", "km_walked", "level_stats", "xp_earned", "xp_per_hour"], @@ -34,6 +24,15 @@ class UpdateTitleStats(BaseTask): "terminal_title": false } } + + min_interval : The minimum interval at which the stats are displayed, + in seconds (defaults to 120 seconds). + The update interval cannot be accurate as workers run synchronously. + stats : An array of stats to display and their display order (implicitly), + see available stats below (defaults to []). + terminal_log : Logs the stats into the terminal (defaults to false). + terminal_title : Displays the stats into the terminal title (defaults to true). + Available stats : - login : The account login (from the credentials). - username : The trainer name (asked at first in-game connection). @@ -48,6 +47,7 @@ class UpdateTitleStats(BaseTask): - stops_visited : The number of visited stops. - pokemon_encountered : The number of encountered pokemon. - pokemon_caught : The number of caught pokemon. + - captures_per_hour : The estimated number of pokemon captured per hour. - pokemon_released : The number of released pokemon. - pokemon_evolved : The number of evolved pokemon. - pokemon_unseen : The number of pokemon never seen before. @@ -56,16 +56,9 @@ class UpdateTitleStats(BaseTask): - stardust_earned : The number of earned stardust since the bot started. - highest_cp_pokemon : The caught pokemon with the highest CP since the bot started. - most_perfect_pokemon : The most perfect caught pokemon since the bot started. - - min_interval : The minimum interval at which the title is updated, - in seconds (defaults to 10 seconds). - The update interval cannot be accurate as workers run synchronously. - stats : An array of stats to display and their display order (implicitly), - see available stats above. """ SUPPORTED_TASK_API_VERSION = 1 - def __init__(self, bot, config): """ Initializes the worker. @@ -74,62 +67,78 @@ def __init__(self, bot, config): :param config: The task configuration. :type config: dict """ - super(UpdateTitleStats, self).__init__(bot, config) + super(UpdateLiveStats, self).__init__(bot, config) self.next_update = None self.min_interval = int(self.config.get('min_interval', 120)) self.displayed_stats = self.config.get('stats', []) - self.terminal_log = self.config.get('terminal_log', False) - self.terminal_title = self.config.get('terminal_title', True) + self.terminal_log = bool(self.config.get('terminal_log', False)) + self.terminal_title = bool(self.config.get('terminal_title', True)) - self.bot.event_manager.register_event('update_title', parameters=('title',)) - self.bot.event_manager.register_event('log_stats',parameters=('title',)) + self.bot.event_manager.register_event('log_stats', parameters=('stats',)) def initialize(self): pass def work(self): """ - Updates the title if necessary. + Displays the stats if necessary. :return: Always returns WorkerResult.SUCCESS. :rtype: WorkerResult """ if not self._should_display(): return WorkerResult.SUCCESS - title = self._get_stats_title(self._get_player_stats()) - # If title is empty, it couldn't be generated. - if not title: + line = self._get_stats_line(self._get_player_stats()) + # If line is empty, it couldn't be generated. + if not line: return WorkerResult.SUCCESS if self.terminal_title: - self._update_title(title, _platform) + self._update_title(line, _platform) if self.terminal_log: - self._log_on_terminal(title) + self._log_on_terminal(line) return WorkerResult.SUCCESS def _should_display(self): """ - Returns a value indicating whether the title should be updated. - :return: True if the title should be updated; otherwise, False. + Returns a value indicating whether the stats should be displayed. + :return: True if the stats should be displayed; otherwise, False. :rtype: bool """ + if not self.terminal_title and not self.terminal_log: + return False return self.next_update is None or datetime.now() >= self.next_update - def _log_on_terminal(self, title): + def _compute_next_update(self): + """ + Computes the next update datetime based on the minimum update interval. + :return: Nothing. + :rtype: None + """ + self.next_update = datetime.now() + timedelta(seconds=self.min_interval) + + def _log_on_terminal(self, stats): + """ + Logs the stats into the terminal using an event. + :param stats: The stats to display. + :type stats: string + :return: Nothing. + :rtype: None + """ self.emit_event( 'log_stats', - formatted="{title}", + formatted="{stats}", data={ - 'title': title + 'stats': stats } ) - self.next_update = datetime.now() + timedelta(seconds=self.min_interval) + self._compute_next_update() def _update_title(self, title, platform): """ - Updates the window title using different methods, according to the given platform + Updates the window title using different methods, according to the given platform. :param title: The new window title. :type title: string :param platform: The platform string. @@ -139,14 +148,6 @@ def _update_title(self, title, platform): :raise: RuntimeError: When the given platform isn't supported. """ - self.emit_event( - 'update_title', - formatted="{title}", - data={ - 'title': title - } - ) - if platform == "linux" or platform == "linux2" or platform == "cygwin": stdout.write("\x1b]2;{}\x07".format(title)) stdout.flush() @@ -154,14 +155,12 @@ def _update_title(self, title, platform): stdout.write("\033]0;{}\007".format(title)) stdout.flush() elif platform == "win32": - ctypes.windll.kernel32.SetConsoleTitleA(title) + ctypes.windll.kernel32.SetConsoleTitleA(title.encode()) else: raise RuntimeError("unsupported platform '{}'".format(platform)) + self._compute_next_update() - self.next_update = datetime.now() + timedelta(seconds=self.min_interval) - - - def _get_stats_title(self, player_stats): + def _get_stats_line(self, player_stats): """ Generates a stats string with the given player stats according to the configuration. :return: A string containing human-readable stats, ready to be displayed. @@ -194,6 +193,7 @@ def _get_stats_title(self, player_stats): stops_visited = metrics.visits['latest'] - metrics.visits['start'] pokemon_encountered = metrics.num_encounters() pokemon_caught = metrics.num_captures() + captures_per_hour = int(metrics.captures_per_hour()) pokemon_released = metrics.releases pokemon_evolved = metrics.num_evolutions() pokemon_unseen = metrics.num_new_mons() @@ -223,6 +223,7 @@ def _get_stats_title(self, player_stats): 'stops_visited': 'Visited {:,} stops'.format(stops_visited), 'pokemon_encountered': 'Encountered {:,} pokemon'.format(pokemon_encountered), 'pokemon_caught': 'Caught {:,} pokemon'.format(pokemon_caught), + 'captures_per_hour': '{:,} pokemon/h'.format(captures_per_hour), 'pokemon_released': 'Released {:,} pokemon'.format(pokemon_released), 'pokemon_evolved': 'Evolved {:,} pokemon'.format(pokemon_evolved), 'pokemon_unseen': 'Encountered {} new pokemon'.format(pokemon_unseen), @@ -251,9 +252,9 @@ def get_stat(stat): return available_stats[stat] # Map stats the user wants to see to available stats and join them with pipes. - title = ' | '.join(map(get_stat, self.displayed_stats)) + line = ' | '.join(map(get_stat, self.displayed_stats)) - return title + return line def _get_player_stats(self): """ diff --git a/pokemongo_bot/metrics.py b/pokemongo_bot/metrics.py index 0ffeb39a6c..4a6a70cae3 100644 --- a/pokemongo_bot/metrics.py +++ b/pokemongo_bot/metrics.py @@ -42,6 +42,14 @@ def num_throws(self): def num_captures(self): return self.captures['latest'] - self.captures['start'] + def captures_per_hour(self): + """ + Returns an estimated number of pokemon caught per hour. + :return: An estimated number of pokemon caught per hour. + :rtype: float + """ + return self.num_captures() / (time.time() - self.start_time) * 3600 + def num_visits(self): return self.visits['latest'] - self.visits['start'] diff --git a/tests/update_title_stats_test.py b/tests/update_live_stats_test.py similarity index 58% rename from tests/update_title_stats_test.py rename to tests/update_live_stats_test.py index ba480f0151..dc5b140080 100644 --- a/tests/update_title_stats_test.py +++ b/tests/update_live_stats_test.py @@ -2,18 +2,20 @@ from sys import platform as _platform from datetime import datetime, timedelta from mock import call, patch, MagicMock -from pokemongo_bot.cell_workers.update_title_stats import UpdateTitleStats +from pokemongo_bot.cell_workers.update_live_stats import UpdateLiveStats from tests import FakeBot -class UpdateTitleStatsTestCase(unittest.TestCase): +class UpdateLiveStatsTestCase(unittest.TestCase): config = { 'min_interval': 20, 'stats': ['login', 'username', 'pokemon_evolved', 'pokemon_encountered', 'uptime', 'pokemon_caught', 'stops_visited', 'km_walked', 'level', 'stardust_earned', 'level_completion', 'xp_per_hour', 'pokeballs_thrown', 'highest_cp_pokemon', 'level_stats', 'xp_earned', 'pokemon_unseen', 'most_perfect_pokemon', - 'pokemon_stats', 'pokemon_released'] + 'pokemon_stats', 'pokemon_released', 'captures_per_hour'], + 'terminal_log': True, + 'terminal_title': False } player_stats = { 'level': 25, @@ -26,7 +28,7 @@ def setUp(self): self.bot = FakeBot() self.bot._player = {'username': 'Username'} self.bot.config.username = 'Login' - self.worker = UpdateTitleStats(self.bot, self.config) + self.worker = UpdateLiveStats(self.bot, self.config) def mock_metrics(self): self.bot.metrics = MagicMock() @@ -37,6 +39,7 @@ def mock_metrics(self): self.bot.metrics.visits = {'latest': 250, 'start': 30} self.bot.metrics.num_encounters.return_value = 130 self.bot.metrics.num_captures.return_value = 120 + self.bot.metrics.captures_per_hour.return_value = 75 self.bot.metrics.releases = 30 self.bot.metrics.num_evolutions.return_value = 12 self.bot.metrics.num_new_mons.return_value = 3 @@ -45,16 +48,30 @@ def mock_metrics(self): self.bot.metrics.highest_cp = {'desc': 'highest_cp'} self.bot.metrics.most_perfect = {'desc': 'most_perfect'} - def test_process_config(self): + def test_config(self): self.assertEqual(self.worker.min_interval, self.config['min_interval']) self.assertEqual(self.worker.displayed_stats, self.config['stats']) + self.assertEqual(self.worker.terminal_title, self.config['terminal_title']) + self.assertEqual(self.worker.terminal_log, self.config['terminal_log']) def test_should_display_no_next_update(self): self.worker.next_update = None self.assertTrue(self.worker._should_display()) - @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') + @patch('pokemongo_bot.cell_workers.update_live_stats.datetime') + def test_should_display_no_terminal_log_title(self, mock_datetime): + # _should_display should return False if both terminal_title and terminal_log are false + # in configuration, even if we're past next_update. + now = datetime.now() + mock_datetime.now.return_value = now + timedelta(seconds=20) + self.worker.next_update = now + self.worker.terminal_log = False + self.worker.terminal_title = False + + self.assertFalse(self.worker._should_display()) + + @patch('pokemongo_bot.cell_workers.update_live_stats.datetime') def test_should_display_before_next_update(self, mock_datetime): now = datetime.now() mock_datetime.now.return_value = now - timedelta(seconds=20) @@ -62,7 +79,7 @@ def test_should_display_before_next_update(self, mock_datetime): self.assertFalse(self.worker._should_display()) - @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') + @patch('pokemongo_bot.cell_workers.update_live_stats.datetime') def test_should_display_after_next_update(self, mock_datetime): now = datetime.now() mock_datetime.now.return_value = now + timedelta(seconds=20) @@ -70,7 +87,7 @@ def test_should_display_after_next_update(self, mock_datetime): self.assertTrue(self.worker._should_display()) - @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') + @patch('pokemongo_bot.cell_workers.update_live_stats.datetime') def test_should_display_exactly_next_update(self, mock_datetime): now = datetime.now() mock_datetime.now.return_value = now @@ -78,65 +95,83 @@ def test_should_display_exactly_next_update(self, mock_datetime): self.assertTrue(self.worker._should_display()) - @patch('pokemongo_bot.cell_workers.update_title_stats.datetime') - def test_next_update_after_update_title(self, mock_datetime): + @patch('pokemongo_bot.cell_workers.update_live_stats.datetime') + def test_compute_next_update(self, mock_datetime): now = datetime.now() mock_datetime.now.return_value = now old_next_display_value = self.worker.next_update - self.worker._update_title('', 'linux2') + self.worker._compute_next_update() self.assertNotEqual(self.worker.next_update, old_next_display_value) self.assertEqual(self.worker.next_update, now + timedelta(seconds=self.config['min_interval'])) - @patch('pokemongo_bot.cell_workers.update_title_stats.stdout') - def test_update_title_linux_cygwin(self, mock_stdout): + @patch('pokemongo_bot.cell_workers.update_live_stats.stdout') + @patch('pokemongo_bot.cell_workers.UpdateLiveStats._compute_next_update') + def test_update_title_linux_cygwin(self, mock_compute_next_update, mock_stdout): self.worker._update_title('new title linux', 'linux') self.assertEqual(mock_stdout.write.call_count, 1) self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title linux\x07')) + self.assertEqual(mock_compute_next_update.call_count, 1) self.worker._update_title('new title linux2', 'linux2') self.assertEqual(mock_stdout.write.call_count, 2) self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title linux2\x07')) + self.assertEqual(mock_compute_next_update.call_count, 2) self.worker._update_title('new title cygwin', 'cygwin') self.assertEqual(mock_stdout.write.call_count, 3) self.assertEqual(mock_stdout.write.call_args, call('\x1b]2;new title cygwin\x07')) + self.assertEqual(mock_compute_next_update.call_count, 3) - @patch('pokemongo_bot.cell_workers.update_title_stats.stdout') - def test_update_title_darwin(self, mock_stdout): + @patch('pokemongo_bot.cell_workers.update_live_stats.stdout') + @patch('pokemongo_bot.cell_workers.UpdateLiveStats._compute_next_update') + def test_update_title_darwin(self, mock_compute_next_update, mock_stdout): self.worker._update_title('new title darwin', 'darwin') self.assertEqual(mock_stdout.write.call_count, 1) self.assertEqual(mock_stdout.write.call_args, call('\033]0;new title darwin\007')) + self.assertEqual(mock_compute_next_update.call_count, 1) @unittest.skipUnless(_platform.startswith("win"), "requires Windows") - @patch('pokemongo_bot.cell_workers.update_title_stats.ctypes') - def test_update_title_win32(self, mock_ctypes): + @patch('pokemongo_bot.cell_workers.update_live_stats.ctypes') + @patch('pokemongo_bot.cell_workers.UpdateLiveStats._compute_next_update') + def test_update_title_win32(self, mock_compute_next_update, mock_ctypes): self.worker._update_title('new title win32', 'win32') self.assertEqual(mock_ctypes.windll.kernel32.SetConsoleTitleA.call_count, 1) self.assertEqual(mock_ctypes.windll.kernel32.SetConsoleTitleA.call_args, call('new title win32')) + self.assertEqual(mock_compute_next_update.call_count, 1) + + @patch('pokemongo_bot.cell_workers.update_live_stats.BaseTask.emit_event') + @patch('pokemongo_bot.cell_workers.UpdateLiveStats._compute_next_update') + def test_log_on_terminal(self, mock_compute_next_update, mock_emit_event): + self.worker._log_on_terminal('stats') + + self.assertEqual(mock_emit_event.call_count, 1) + self.assertEqual(mock_emit_event.call_args, + call('log_stats', data={'stats': 'stats'}, formatted='{stats}')) + self.assertEqual(mock_compute_next_update.call_count, 1) - def test_get_stats_title_player_stats_none(self): - title = self.worker._get_stats_title(None) + def test_get_stats_line_player_stats_none(self): + line = self.worker._get_stats_line(None) - self.assertEqual(title, '') + self.assertEqual(line, '') - def test_get_stats_no_displayed_stats(self): + def test_get_stats_line_no_displayed_stats(self): self.worker.displayed_stats = [] - title = self.worker._get_stats_title(self.player_stats) + line = self.worker._get_stats_line(self.player_stats) - self.assertEqual(title, '') + self.assertEqual(line, '') - def test_get_stats(self): + def test_get_stats_line(self): self.mock_metrics() - title = self.worker._get_stats_title(self.player_stats) + line = self.worker._get_stats_line(self.player_stats) expected = 'Login | Username | Evolved 12 pokemon | Encountered 130 pokemon | ' \ 'Uptime : 15:42:13 | Caught 120 pokemon | Visited 220 stops | ' \ '42.05km walked | Level 25 | Earned 24,069 Stardust | ' \ @@ -145,6 +180,6 @@ def test_get_stats(self): '+424,242 XP | Encountered 3 new pokemon | ' \ 'Most perfect pokemon : most_perfect | ' \ 'Encountered 130 pokemon, 120 caught, 30 released, 12 evolved, ' \ - '3 never seen before | Released 30 pokemon' + '3 never seen before | Released 30 pokemon | 75 pokemon/h' - self.assertEqual(title, expected) + self.assertEqual(line, expected)