class PreviousCommandUnitTest(unittest.TestCase): def setUp(self) -> None: super().setUp() self.ev_loop = asyncio.get_event_loop() self.async_run_with_timeout(read_system_configs_from_yml()) self.client_config = ClientConfigMap() self.config_adapter = ClientConfigAdapter(self.client_config) self.app = HummingbotApplication(self.config_adapter) self.cli_mock_assistant = CLIMockingAssistant(self.app.app) self.cli_mock_assistant.start() def tearDown(self) -> None: self.cli_mock_assistant.stop() super().tearDown() def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def mock_user_response(self, config): config.value = "yes" def test_no_previous_strategy_found(self): self.config_adapter.previous_strategy = None self.app.previous_strategy(option="") self.assertTrue( self.cli_mock_assistant.check_log_called_with( "No previous strategy found.")) @patch( "hummingbot.client.command.import_command.ImportCommand.import_command" ) def test_strategy_found_and_user_declines(self, import_command: MagicMock): strategy_name = "conf_1.yml" self.cli_mock_assistant.queue_prompt_reply("No") self.async_run_with_timeout( self.app.prompt_for_previous_strategy(strategy_name)) import_command.assert_not_called() @patch( "hummingbot.client.command.import_command.ImportCommand.import_command" ) def test_strategy_found_and_user_accepts(self, import_command: MagicMock): strategy_name = "conf_1.yml" self.config_adapter.previous_strategy = strategy_name self.cli_mock_assistant.queue_prompt_reply("Yes") self.async_run_with_timeout( self.app.prompt_for_previous_strategy(strategy_name)) import_command.assert_called() self.assertTrue(import_command.call_args[0][1] == strategy_name)
class ImportCommandTest(unittest.TestCase): @patch("hummingbot.core.utils.trading_pair_fetcher.TradingPairFetcher") def setUp(self, _: MagicMock) -> None: super().setUp() self.ev_loop = asyncio.get_event_loop() self.app = HummingbotApplication() self.cli_mock_assistant = CLIMockingAssistant(self.app.app) self.cli_mock_assistant.start() def tearDown(self) -> None: self.cli_mock_assistant.stop() super().tearDown() @staticmethod async def raise_timeout(*args, **kwargs): raise asyncio.TimeoutError def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) return ret def async_run_with_timeout_coroutine_must_raise_timeout(self, coroutine: Awaitable, timeout: float = 1): class DesiredError(Exception): pass async def run_coro_that_raises(coro: Awaitable): try: await coro except asyncio.TimeoutError: raise DesiredError try: self.async_run_with_timeout(run_coro_that_raises(coroutine), timeout) except DesiredError: # the coroutine raised an asyncio.TimeoutError as expected raise asyncio.TimeoutError except asyncio.TimeoutError: # the coroutine did not finish on time raise RuntimeError @patch("hummingbot.client.command.import_command.update_strategy_config_map_from_file") @patch("hummingbot.client.command.status_command.StatusCommand.status_check_all") def test_import_config_file_success( self, status_check_all_mock: AsyncMock, update_strategy_config_map_from_file: AsyncMock ): strategy_name = "some-strategy" strategy_file_name = f"{strategy_name}.yml" status_check_all_mock.return_value = True update_strategy_config_map_from_file.return_value = strategy_name self.async_run_with_timeout(self.app.import_config_file(strategy_file_name)) self.assertEqual(strategy_file_name, self.app.strategy_file_name) self.assertEqual(strategy_name, self.app.strategy_name) self.assertTrue( self.cli_mock_assistant.check_log_called_with("\nEnter \"start\" to start market making.") ) @patch("hummingbot.client.command.import_command.update_strategy_config_map_from_file") @patch("hummingbot.client.command.status_command.StatusCommand.status_check_all") def test_import_config_file_handles_network_timeouts( self, status_check_all_mock: AsyncMock, update_strategy_config_map_from_file: AsyncMock ): strategy_name = "some-strategy" strategy_file_name = f"{strategy_name}.yml" status_check_all_mock.side_effect = self.raise_timeout update_strategy_config_map_from_file.return_value = strategy_name with self.assertRaises(asyncio.TimeoutError): self.async_run_with_timeout_coroutine_must_raise_timeout( self.app.import_config_file(strategy_file_name) ) self.assertEqual(None, self.app.strategy_file_name) self.assertEqual(None, self.app.strategy_name)
class CreateCommandTest(unittest.TestCase): @patch("hummingbot.core.utils.trading_pair_fetcher.TradingPairFetcher") def setUp(self, _: MagicMock) -> None: super().setUp() self.ev_loop = asyncio.get_event_loop() self.async_run_with_timeout(read_system_configs_from_yml()) self.client_config_map = ClientConfigAdapter(ClientConfigMap()) self.app = HummingbotApplication( client_config_map=self.client_config_map) self.cli_mock_assistant = CLIMockingAssistant(self.app.app) self.cli_mock_assistant.start() def tearDown(self) -> None: self.cli_mock_assistant.stop() super().tearDown() @staticmethod def get_async_sleep_fn(delay: float): async def async_sleep(*_, **__): await asyncio.sleep(delay) return async_sleep def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def async_run_with_timeout_coroutine_must_raise_timeout( self, coroutine: Awaitable, timeout: float = 1): class DesiredError(Exception): pass async def run_coro_that_raises(coro: Awaitable): try: await coro except asyncio.TimeoutError: raise DesiredError try: self.async_run_with_timeout(run_coro_that_raises(coroutine), timeout) except DesiredError: # the coroutine raised an asyncio.TimeoutError as expected raise asyncio.TimeoutError except asyncio.TimeoutError: # the coroutine did not finish on time raise RuntimeError @patch("shutil.copy") @patch("hummingbot.client.command.create_command.save_to_yml_legacy") @patch("hummingbot.client.config.security.Security.is_decryption_done") @patch( "hummingbot.client.command.status_command.StatusCommand.validate_required_connections" ) @patch("hummingbot.core.utils.market_price.get_last_price") def test_prompt_for_configuration_re_prompts_on_lower_than_minimum_amount( self, get_last_price_mock: AsyncMock, validate_required_connections_mock: AsyncMock, is_decryption_done_mock: MagicMock, save_to_yml_mock: MagicMock, _: MagicMock, ): get_last_price_mock.return_value = Decimal("11") validate_required_connections_mock.return_value = {} is_decryption_done_mock.return_value = True config_maps = [] save_to_yml_mock.side_effect = lambda _, cm: config_maps.append(cm) self.client_config_map.commands_timeout.create_command_timeout = 10 self.client_config_map.commands_timeout.other_commands_timeout = 30 strategy_name = "some-strategy" strategy_file_name = f"{strategy_name}.yml" base_strategy = "pure_market_making" self.cli_mock_assistant.queue_prompt_reply(base_strategy) # strategy self.cli_mock_assistant.queue_prompt_reply("binance") # spot connector self.cli_mock_assistant.queue_prompt_reply("BTC-USDT") # trading pair self.cli_mock_assistant.queue_prompt_reply("1") # bid spread self.cli_mock_assistant.queue_prompt_reply("1") # ask spread self.cli_mock_assistant.queue_prompt_reply("30") # order refresh time self.cli_mock_assistant.queue_prompt_reply( "0") # unacceptable order amount self.cli_mock_assistant.queue_prompt_reply( "1") # acceptable order amount self.cli_mock_assistant.queue_prompt_reply("No") # ping pong feature self.async_run_with_timeout( self.app.prompt_for_configuration(strategy_file_name)) self.assertEqual(strategy_file_name, self.app.strategy_file_name) self.assertEqual(base_strategy, self.app.strategy_name) self.assertTrue( self.cli_mock_assistant.check_log_called_with( msg="Value must be more than 0.")) @patch("shutil.copy") @patch("hummingbot.client.command.create_command.save_to_yml_legacy") @patch("hummingbot.client.config.security.Security.is_decryption_done") @patch( "hummingbot.client.command.status_command.StatusCommand.validate_required_connections" ) @patch("hummingbot.core.utils.market_price.get_last_price") def test_prompt_for_configuration_accepts_zero_amount_on_get_last_price_network_timeout( self, get_last_price_mock: AsyncMock, validate_required_connections_mock: AsyncMock, is_decryption_done_mock: MagicMock, save_to_yml_mock: MagicMock, _: MagicMock, ): get_last_price_mock.side_effect = self.get_async_sleep_fn(delay=0.02) validate_required_connections_mock.return_value = {} is_decryption_done_mock.return_value = True config_maps = [] save_to_yml_mock.side_effect = lambda _, cm: config_maps.append(cm) self.client_config_map.commands_timeout.create_command_timeout = 0.005 self.client_config_map.commands_timeout.other_commands_timeout = 0.01 strategy_name = "some-strategy" strategy_file_name = f"{strategy_name}.yml" base_strategy = "pure_market_making" self.cli_mock_assistant.queue_prompt_reply(base_strategy) # strategy self.cli_mock_assistant.queue_prompt_reply("binance") # spot connector self.cli_mock_assistant.queue_prompt_reply("BTC-USDT") # trading pair self.cli_mock_assistant.queue_prompt_reply("1") # bid spread self.cli_mock_assistant.queue_prompt_reply("1") # ask spread self.cli_mock_assistant.queue_prompt_reply("30") # order refresh time self.cli_mock_assistant.queue_prompt_reply("1") # order amount self.cli_mock_assistant.queue_prompt_reply("No") # ping pong feature self.async_run_with_timeout( self.app.prompt_for_configuration(strategy_file_name)) self.assertEqual(strategy_file_name, self.app.strategy_file_name) self.assertEqual(base_strategy, self.app.strategy_name) def test_create_command_restores_config_map_after_config_stop(self): base_strategy = "pure_market_making" strategy_config = get_strategy_config_map(base_strategy) original_exchange = "bybit" strategy_config["exchange"].value = original_exchange self.cli_mock_assistant.queue_prompt_reply(base_strategy) # strategy self.cli_mock_assistant.queue_prompt_reply("binance") # spot connector self.cli_mock_assistant.queue_prompt_to_stop_config( ) # cancel on trading pair prompt self.async_run_with_timeout(self.app.prompt_for_configuration(None)) strategy_config = get_strategy_config_map(base_strategy) self.assertEqual(original_exchange, strategy_config["exchange"].value) def test_create_command_restores_config_map_after_config_stop_on_new_file_prompt( self): base_strategy = "pure_market_making" strategy_config = get_strategy_config_map(base_strategy) original_exchange = "bybit" strategy_config["exchange"].value = original_exchange self.cli_mock_assistant.queue_prompt_reply(base_strategy) # strategy self.cli_mock_assistant.queue_prompt_reply("binance") # spot connector self.cli_mock_assistant.queue_prompt_reply("BTC-USDT") # trading pair self.cli_mock_assistant.queue_prompt_reply("1") # bid spread self.cli_mock_assistant.queue_prompt_reply("1") # ask spread self.cli_mock_assistant.queue_prompt_reply("30") # order refresh time self.cli_mock_assistant.queue_prompt_reply("1") # order amount self.cli_mock_assistant.queue_prompt_reply("No") # ping pong feature self.cli_mock_assistant.queue_prompt_to_stop_config( ) # cancel on new file prompt self.async_run_with_timeout(self.app.prompt_for_configuration(None)) strategy_config = get_strategy_config_map(base_strategy) self.assertEqual(original_exchange, strategy_config["exchange"].value) @patch("shutil.copy") @patch("hummingbot.client.command.create_command.save_to_yml_legacy") @patch("hummingbot.client.config.security.Security.is_decryption_done") @patch( "hummingbot.client.command.status_command.StatusCommand.validate_required_connections" ) @patch("hummingbot.core.utils.market_price.get_last_price") def test_prompt_for_configuration_handles_status_network_timeout( self, get_last_price_mock: AsyncMock, validate_required_connections_mock: AsyncMock, is_decryption_done_mock: MagicMock, _: MagicMock, __: MagicMock, ): get_last_price_mock.return_value = None validate_required_connections_mock.side_effect = self.get_async_sleep_fn( delay=0.02) is_decryption_done_mock.return_value = True self.client_config_map.commands_timeout.create_command_timeout = 0.005 self.client_config_map.commands_timeout.other_commands_timeout = 0.01 strategy_file_name = "some-strategy.yml" self.cli_mock_assistant.queue_prompt_reply( "pure_market_making") # strategy self.cli_mock_assistant.queue_prompt_reply("binance") # spot connector self.cli_mock_assistant.queue_prompt_reply("BTC-USDT") # trading pair self.cli_mock_assistant.queue_prompt_reply("1") # bid spread self.cli_mock_assistant.queue_prompt_reply("1") # ask spread self.cli_mock_assistant.queue_prompt_reply("30") # order refresh time self.cli_mock_assistant.queue_prompt_reply("1") # order amount self.cli_mock_assistant.queue_prompt_reply("No") # ping pong feature with self.assertRaises(asyncio.TimeoutError): self.async_run_with_timeout_coroutine_must_raise_timeout( self.app.prompt_for_configuration(strategy_file_name)) self.assertEqual(None, self.app.strategy_file_name) self.assertEqual(None, self.app.strategy_name) self.assertTrue( self.cli_mock_assistant.check_log_called_with( msg= "\nA network error prevented the connection check to complete. See logs for more details." ))
class HistoryCommandTest(unittest.TestCase): @patch("hummingbot.core.utils.trading_pair_fetcher.TradingPairFetcher") def setUp(self, _: MagicMock) -> None: super().setUp() self.ev_loop = asyncio.get_event_loop() self.async_run_with_timeout(read_system_configs_from_yml()) self.app = HummingbotApplication() self.cli_mock_assistant = CLIMockingAssistant(self.app.app) self.cli_mock_assistant.start() self.global_config_backup = deepcopy(global_config_map) self.mock_strategy_name = "test-strategy" def tearDown(self) -> None: self.cli_mock_assistant.stop() self.reset_global_config() db_path = Path( SQLConnectionManager.create_db_path( db_name=self.mock_strategy_name)) db_path.unlink(missing_ok=True) super().tearDown() def reset_global_config(self): for key, value in self.global_config_backup.items(): global_config_map[key] = value @staticmethod def get_async_sleep_fn(delay: float): async def async_sleep(*_, **__): await asyncio.sleep(delay) return async_sleep def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def async_run_with_timeout_coroutine_must_raise_timeout( self, coroutine: Awaitable, timeout: float = 1): class DesiredError(Exception): pass async def run_coro_that_raises(coro: Awaitable): try: await coro except asyncio.TimeoutError: raise DesiredError try: self.async_run_with_timeout(run_coro_that_raises(coroutine), timeout) except DesiredError: # the coroutine raised an asyncio.TimeoutError as expected raise asyncio.TimeoutError except asyncio.TimeoutError: # the coroutine did not finish on time raise RuntimeError def get_trades(self) -> List[TradeFill]: trade_fee = AddedToCostTradeFee(percent=Decimal("5")) trades = [ TradeFill( config_file_path=f"{self.mock_strategy_name}.yml", strategy=self.mock_strategy_name, market="binance", symbol="BTC-USDT", base_asset="BTC", quote_asset="USDT", timestamp=int(time.time()), order_id="someId", trade_type="BUY", order_type="LIMIT", price=1, amount=2, leverage=1, trade_fee=trade_fee.to_json(), exchange_trade_id="someExchangeId", ) ] return trades @patch( "hummingbot.client.command.history_command.HistoryCommand.get_current_balances" ) def test_history_report_raises_on_get_current_balances_network_timeout( self, get_current_balances_mock: AsyncMock): get_current_balances_mock.side_effect = self.get_async_sleep_fn( delay=0.02) global_config_map["other_commands_timeout"].value = 0.01 trades = self.get_trades() with self.assertRaises(asyncio.TimeoutError): self.async_run_with_timeout_coroutine_must_raise_timeout( self.app.history_report(start_time=time.time(), trades=trades), 10000) self.assertTrue( self.cli_mock_assistant.check_log_called_with( msg= "\nA network error prevented the balances retrieval to complete. See logs for more details." )) @patch( "hummingbot.client.hummingbot_application.HummingbotApplication._notify" ) def test_list_trades(self, notify_mock): global_config_map["tables_format"].value = "psql" captures = [] notify_mock.side_effect = lambda s: captures.append(s) self.app.strategy_file_name = f"{self.mock_strategy_name}.yml" trade_fee = AddedToCostTradeFee(percent=Decimal("5")) order_id = PaperTradeExchange.random_order_id(order_side="BUY", trading_pair="BTC-USDT") with self.app.trade_fill_db.get_new_session() as session: o = Order( id=order_id, config_file_path=f"{self.mock_strategy_name}.yml", strategy=self.mock_strategy_name, market="binance", symbol="BTC-USDT", base_asset="BTC", quote_asset="USDT", creation_timestamp=0, order_type="LMT", amount=4, leverage=0, price=3, last_status="PENDING", last_update_timestamp=0, ) session.add(o) for i in [1, 2]: t = TradeFill( config_file_path=f"{self.mock_strategy_name}.yml", strategy=self.mock_strategy_name, market="binance", symbol="BTC-USDT", base_asset="BTC", quote_asset="USDT", timestamp=i, order_id=order_id, trade_type="BUY", order_type="LIMIT", price=i, amount=2, leverage=1, trade_fee=trade_fee.to_json(), exchange_trade_id=f"someExchangeId{i}", ) session.add(t) session.commit() self.app.list_trades(start_time=0) self.assertEqual(1, len(captures)) creation_time_str = str(datetime.datetime.fromtimestamp(0)) df_str_expected = ( f"\n Recent trades:" f"\n +---------------------+------------+----------+--------------+--------+---------+----------+------------+------------+-------+" # noqa: E501 f"\n | Timestamp | Exchange | Market | Order_type | Side | Price | Amount | Leverage | Position | Age |" # noqa: E501 f"\n |---------------------+------------+----------+--------------+--------+---------+----------+------------+------------+-------|" # noqa: E501 f"\n | {creation_time_str} | binance | BTC-USDT | limit | buy | 1 | 2 | 1 | NIL | n/a |" # noqa: E501 f"\n | {creation_time_str} | binance | BTC-USDT | limit | buy | 2 | 2 | 1 | NIL | n/a |" # noqa: E501 f"\n +---------------------+------------+----------+--------------+--------+---------+----------+------------+------------+-------+" # noqa: E501 ) self.assertEqual(df_str_expected, captures[0])
class ConnectCommandTest(unittest.TestCase): @patch("hummingbot.core.utils.trading_pair_fetcher.TradingPairFetcher") def setUp(self, _: MagicMock) -> None: super().setUp() self.ev_loop = asyncio.get_event_loop() self.async_run_with_timeout(read_system_configs_from_yml()) self.app = HummingbotApplication() self.cli_mock_assistant = CLIMockingAssistant(self.app.app) self.cli_mock_assistant.start() self.global_config_backup = deepcopy(global_config_map) def tearDown(self) -> None: self.cli_mock_assistant.stop() self.reset_global_config() Security._decryption_done.clear() super().tearDown() def reset_global_config(self): for key, value in self.global_config_backup.items(): global_config_map[key] = value @staticmethod def get_async_sleep_fn(delay: float): async def async_sleep(*_, **__): await asyncio.sleep(delay) return async_sleep def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def async_run_with_timeout_coroutine_must_raise_timeout( self, coroutine: Awaitable, timeout: float = 1): class DesiredError(Exception): pass async def run_coro_that_raises(coro: Awaitable): try: await coro except asyncio.TimeoutError: raise DesiredError try: self.async_run_with_timeout(run_coro_that_raises(coroutine), timeout) except DesiredError: # the coroutine raised an asyncio.TimeoutError as expected raise asyncio.TimeoutError except asyncio.TimeoutError: # the coroutine did not finish on time raise RuntimeError @patch("hummingbot.client.config.security.Security.update_secure_config") @patch("hummingbot.client.config.security.Security.encrypted_file_exists") @patch("hummingbot.client.config.security.Security.api_keys") @patch("hummingbot.user.user_balances.UserBalances.add_exchange") def test_connect_exchange_success( self, add_exchange_mock: AsyncMock, api_keys_mock: AsyncMock, encrypted_file_exists_mock: MagicMock, _: MagicMock, ): add_exchange_mock.return_value = None exchange = "binance" api_key = "someKey" api_secret = "someSecret" api_keys_mock.return_value = { "binance_api_key": api_key, "binance_api_secret": api_secret } encrypted_file_exists_mock.return_value = False global_config_map["other_commands_timeout"].value = 30 self.cli_mock_assistant.queue_prompt_reply(api_key) # binance API key self.cli_mock_assistant.queue_prompt_reply( api_secret) # binance API secret self.async_run_with_timeout(self.app.connect_exchange(exchange)) self.assertTrue( self.cli_mock_assistant.check_log_called_with( msg=f"\nYou are now connected to {exchange}.")) self.assertFalse(self.app.placeholder_mode) self.assertFalse(self.app.app.hide_input) @patch("hummingbot.client.config.security.Security.update_secure_config") @patch("hummingbot.client.config.security.Security.encrypted_file_exists") @patch("hummingbot.client.config.security.Security.api_keys") @patch("hummingbot.user.user_balances.UserBalances.add_exchange") def test_connect_exchange_handles_network_timeouts( self, add_exchange_mock: AsyncMock, api_keys_mock: AsyncMock, encrypted_file_exists_mock: MagicMock, _: MagicMock, ): add_exchange_mock.side_effect = self.get_async_sleep_fn(delay=0.02) global_config_map["other_commands_timeout"].value = 0.01 api_key = "someKey" api_secret = "someSecret" api_keys_mock.return_value = { "binance_api_key": api_key, "binance_api_secret": api_secret } encrypted_file_exists_mock.return_value = False self.cli_mock_assistant.queue_prompt_reply(api_key) # binance API key self.cli_mock_assistant.queue_prompt_reply( api_secret) # binance API secret with self.assertRaises(asyncio.TimeoutError): self.async_run_with_timeout_coroutine_must_raise_timeout( self.app.connect_exchange("binance")) self.assertTrue( self.cli_mock_assistant.check_log_called_with( msg= "\nA network error prevented the connection to complete. See logs for more details." )) self.assertFalse(self.app.placeholder_mode) self.assertFalse(self.app.app.hide_input) @patch("hummingbot.user.user_balances.UserBalances.update_exchanges") def test_connection_df_handles_network_timeouts( self, update_exchanges_mock: AsyncMock): update_exchanges_mock.side_effect = self.get_async_sleep_fn(delay=0.02) global_config_map["other_commands_timeout"].value = 0.01 with self.assertRaises(asyncio.TimeoutError): self.async_run_with_timeout_coroutine_must_raise_timeout( self.app.connection_df()) self.assertTrue( self.cli_mock_assistant.check_log_called_with( msg= "\nA network error prevented the connection table to populate. See logs for more details." )) @patch("hummingbot.user.user_balances.UserBalances.update_exchanges") def test_connection_df_handles_network_timeouts_logs_hidden( self, update_exchanges_mock: AsyncMock): self.cli_mock_assistant.toggle_logs() update_exchanges_mock.side_effect = self.get_async_sleep_fn(delay=0.02) global_config_map["other_commands_timeout"].value = 0.01 with self.assertRaises(asyncio.TimeoutError): self.async_run_with_timeout_coroutine_must_raise_timeout( self.app.connection_df()) self.assertTrue( self.cli_mock_assistant.check_log_called_with( msg= "\nA network error prevented the connection table to populate. See logs for more details." )) @patch( "hummingbot.client.hummingbot_application.HummingbotApplication._notify" ) @patch( "hummingbot.client.hummingbot_application.HummingbotApplication.connection_df" ) def test_show_connections(self, connection_df_mock, notify_mock): global_config_map["tables_format"].value = "psql" Security._decryption_done.set() captures = [] notify_mock.side_effect = lambda s: captures.append(s) connections_df = pd.DataFrame( columns=pd.Index( ['Exchange', ' Keys Added', ' Keys Confirmed', ' Status'], dtype='object'), data=[["ascend_ex", "Yes", "Yes", "&cYELLOW"], ["beaxy", "Yes", "Yes", "&cGREEN"]]) connection_df_mock.return_value = (connections_df, []) self.async_run_with_timeout(self.app.show_connections()) self.assertEqual(2, len(captures)) self.assertEqual("\nTesting connections, please wait...", captures[0]) df_str_expected = ( " +------------+----------------+--------------------+------------+" "\n | Exchange | Keys Added | Keys Confirmed | Status |" "\n |------------+----------------+--------------------+------------|" "\n | ascend_ex | Yes | Yes | &cYELLOW |" "\n | beaxy | Yes | Yes | &cGREEN |" "\n +------------+----------------+--------------------+------------+" ) self.assertEqual(df_str_expected, captures[1])
class ConnectCommandTest(unittest.TestCase): @patch("hummingbot.core.utils.trading_pair_fetcher.TradingPairFetcher") def setUp(self, _: MagicMock) -> None: super().setUp() self.ev_loop = asyncio.get_event_loop() self.async_run_with_timeout(read_system_configs_from_yml()) self.app = HummingbotApplication() self.cli_mock_assistant = CLIMockingAssistant(self.app.app) self.cli_mock_assistant.start() self.global_config_backup = deepcopy(global_config_map) def tearDown(self) -> None: self.cli_mock_assistant.stop() self.reset_global_config() super().tearDown() def reset_global_config(self): for key, value in self.global_config_backup.items(): global_config_map[key] = value @staticmethod def get_async_sleep_fn(delay: float): async def async_sleep(*_, **__): await asyncio.sleep(delay) return async_sleep def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def async_run_with_timeout_coroutine_must_raise_timeout( self, coroutine: Awaitable, timeout: float = 1): class DesiredError(Exception): pass async def run_coro_that_raises(coro: Awaitable): try: await coro except asyncio.TimeoutError: raise DesiredError try: self.async_run_with_timeout(run_coro_that_raises(coroutine), timeout) except DesiredError: # the coroutine raised an asyncio.TimeoutError as expected raise asyncio.TimeoutError except asyncio.TimeoutError: # the coroutine did not finish on time raise RuntimeError @patch("hummingbot.client.config.security.Security.update_secure_config") @patch("hummingbot.client.config.security.Security.encrypted_file_exists") @patch("hummingbot.client.config.security.Security.api_keys") @patch("hummingbot.user.user_balances.UserBalances.add_exchange") def test_connect_exchange_success( self, add_exchange_mock: AsyncMock, api_keys_mock: AsyncMock, encrypted_file_exists_mock: MagicMock, _: MagicMock, ): add_exchange_mock.return_value = None exchange = "binance" api_key = "someKey" api_secret = "someSecret" api_keys_mock.return_value = { "binance_api_key": api_key, "binance_api_secret": api_secret } encrypted_file_exists_mock.return_value = False global_config_map["other_commands_timeout"].value = 30 self.cli_mock_assistant.queue_prompt_reply(api_key) # binance API key self.cli_mock_assistant.queue_prompt_reply( api_secret) # binance API secret self.async_run_with_timeout(self.app.connect_exchange(exchange)) self.assertTrue( self.cli_mock_assistant.check_log_called_with( msg=f"\nYou are now connected to {exchange}.")) self.assertFalse(self.app.placeholder_mode) self.assertFalse(self.app.app.hide_input) @patch("hummingbot.client.config.security.Security.update_secure_config") @patch("hummingbot.client.config.security.Security.encrypted_file_exists") @patch("hummingbot.client.config.security.Security.api_keys") @patch("hummingbot.user.user_balances.UserBalances.add_exchange") def test_connect_exchange_handles_network_timeouts( self, add_exchange_mock: AsyncMock, api_keys_mock: AsyncMock, encrypted_file_exists_mock: MagicMock, _: MagicMock, ): add_exchange_mock.side_effect = self.get_async_sleep_fn(delay=0.02) global_config_map["other_commands_timeout"].value = 0.01 api_key = "someKey" api_secret = "someSecret" api_keys_mock.return_value = { "binance_api_key": api_key, "binance_api_secret": api_secret } encrypted_file_exists_mock.return_value = False self.cli_mock_assistant.queue_prompt_reply(api_key) # binance API key self.cli_mock_assistant.queue_prompt_reply( api_secret) # binance API secret with self.assertRaises(asyncio.TimeoutError): self.async_run_with_timeout_coroutine_must_raise_timeout( self.app.connect_exchange("binance")) self.assertTrue( self.cli_mock_assistant.check_log_called_with( msg= "\nA network error prevented the connection to complete. See logs for more details." )) self.assertFalse(self.app.placeholder_mode) self.assertFalse(self.app.app.hide_input) @patch("hummingbot.user.user_balances.UserBalances.update_exchanges") def test_connection_df_handles_network_timeouts( self, update_exchanges_mock: AsyncMock): update_exchanges_mock.side_effect = self.get_async_sleep_fn(delay=0.02) global_config_map["other_commands_timeout"].value = 0.01 with self.assertRaises(asyncio.TimeoutError): self.async_run_with_timeout_coroutine_must_raise_timeout( self.app.connection_df()) self.assertTrue( self.cli_mock_assistant.check_log_called_with( msg= "\nA network error prevented the connection table to populate. See logs for more details." )) @patch("hummingbot.user.user_balances.UserBalances.update_exchanges") def test_connection_df_handles_network_timeouts_logs_hidden( self, update_exchanges_mock: AsyncMock): self.cli_mock_assistant.toggle_logs() update_exchanges_mock.side_effect = self.get_async_sleep_fn(delay=0.02) global_config_map["other_commands_timeout"].value = 0.01 with self.assertRaises(asyncio.TimeoutError): self.async_run_with_timeout_coroutine_must_raise_timeout( self.app.connection_df()) self.assertTrue( self.cli_mock_assistant.check_log_called_with( msg= "\nA network error prevented the connection table to populate. See logs for more details." ))
class ImportCommandTest(unittest.TestCase): @patch("hummingbot.core.utils.trading_pair_fetcher.TradingPairFetcher") def setUp(self, _: MagicMock) -> None: super().setUp() self.ev_loop = asyncio.get_event_loop() self.async_run_with_timeout(read_system_configs_from_yml()) self.app = HummingbotApplication() self.cli_mock_assistant = CLIMockingAssistant(self.app.app) self.cli_mock_assistant.start() def tearDown(self) -> None: self.cli_mock_assistant.stop() super().tearDown() @staticmethod async def raise_timeout(*args, **kwargs): raise asyncio.TimeoutError def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def async_run_with_timeout_coroutine_must_raise_timeout( self, coroutine: Awaitable, timeout: float = 1): class DesiredError(Exception): pass async def run_coro_that_raises(coro: Awaitable): try: await coro except asyncio.TimeoutError: raise DesiredError try: self.async_run_with_timeout(run_coro_that_raises(coroutine), timeout) except DesiredError: # the coroutine raised an asyncio.TimeoutError as expected raise asyncio.TimeoutError except asyncio.TimeoutError: # the coroutine did not finish on time raise RuntimeError @staticmethod def build_dummy_strategy_config_cls( strategy_name: str) -> Type[BaseClientModel]: class SomeEnum(ClientConfigEnum): ONE = "one" class DoubleNestedModel(BaseClientModel): double_nested_attr: datetime = Field( default=datetime(2022, 1, 1, 10, 30), description="Double nested attr description") class NestedModel(BaseClientModel): nested_attr: str = Field( default="some value", description="Nested attr\nmultiline description", ) double_nested_model: DoubleNestedModel = Field( default=DoubleNestedModel(), ) class DummyModel(BaseTradingStrategyConfigMap): strategy: str = strategy_name exchange: str = "binance" market: str = "BTC-USDT" some_attr: SomeEnum = Field( default=SomeEnum.ONE, description="Some description", ) nested_model: NestedModel = Field( default=NestedModel(), description="Nested model description", ) another_attr: Decimal = Field( default=Decimal("1.0"), description="Some other\nmultiline description", ) non_nested_no_description: time = Field(default=time(10, 30), ) date_attr: date = Field(default=date(2022, 1, 2)) no_default: str = Field(default=...) class Config: title = "dummy_model" return DummyModel @patch( "hummingbot.client.command.import_command.load_strategy_config_map_from_file" ) @patch( "hummingbot.client.command.status_command.StatusCommand.status_check_all" ) def test_import_config_file_success_legacy( self, status_check_all_mock: AsyncMock, load_strategy_config_map_from_file: AsyncMock): strategy_name = "some_strategy" strategy_file_name = f"{strategy_name}.yml" status_check_all_mock.return_value = True strategy_conf_var = ConfigVar("strategy", None) strategy_conf_var.value = strategy_name load_strategy_config_map_from_file.return_value = { "strategy": strategy_conf_var } self.async_run_with_timeout( self.app.import_config_file(strategy_file_name)) self.assertEqual(strategy_file_name, self.app.strategy_file_name) self.assertEqual(strategy_name, self.app.strategy_name) self.assertTrue( self.cli_mock_assistant.check_log_called_with( "\nEnter \"start\" to start market making.")) @patch( "hummingbot.client.command.import_command.load_strategy_config_map_from_file" ) @patch( "hummingbot.client.command.status_command.StatusCommand.status_check_all" ) def test_import_config_file_handles_network_timeouts_legacy( self, status_check_all_mock: AsyncMock, load_strategy_config_map_from_file: AsyncMock): strategy_name = "some_strategy" strategy_file_name = f"{strategy_name}.yml" status_check_all_mock.side_effect = self.raise_timeout strategy_conf_var = ConfigVar("strategy", None) strategy_conf_var.value = strategy_name load_strategy_config_map_from_file.return_value = { "strategy": strategy_conf_var } with self.assertRaises(asyncio.TimeoutError): self.async_run_with_timeout_coroutine_must_raise_timeout( self.app.import_config_file(strategy_file_name)) self.assertEqual(None, self.app.strategy_file_name) self.assertEqual(None, self.app.strategy_name) @patch( "hummingbot.client.config.config_helpers.get_strategy_pydantic_config_cls" ) @patch( "hummingbot.client.command.status_command.StatusCommand.status_check_all" ) def test_import_config_file_success( self, status_check_all_mock: AsyncMock, get_strategy_pydantic_config_cls: MagicMock): strategy_name = "perpetual_market_making" strategy_file_name = f"{strategy_name}.yml" status_check_all_mock.return_value = True dummy_strategy_config_cls = self.build_dummy_strategy_config_cls( strategy_name) get_strategy_pydantic_config_cls.return_value = dummy_strategy_config_cls cm = ClientConfigAdapter( dummy_strategy_config_cls(no_default="some value")) with TemporaryDirectory() as d: d = Path(d) import_command.STRATEGIES_CONF_DIR_PATH = d temp_file_name = d / strategy_file_name save_to_yml(temp_file_name, cm) self.async_run_with_timeout( self.app.import_config_file(strategy_file_name)) self.assertEqual(strategy_file_name, self.app.strategy_file_name) self.assertEqual(strategy_name, self.app.strategy_name) self.assertTrue( self.cli_mock_assistant.check_log_called_with( "\nEnter \"start\" to start market making.")) self.assertEqual(cm, self.app.strategy_config_map) @patch( "hummingbot.client.config.config_helpers.get_strategy_pydantic_config_cls" ) @patch( "hummingbot.client.command.status_command.StatusCommand.status_check_all" ) def test_import_incomplete_config_file_success( self, status_check_all_mock: AsyncMock, get_strategy_pydantic_config_cls: MagicMock): strategy_name = "perpetual_market_making" strategy_file_name = f"{strategy_name}.yml" status_check_all_mock.return_value = True dummy_strategy_config_cls = self.build_dummy_strategy_config_cls( strategy_name) get_strategy_pydantic_config_cls.return_value = dummy_strategy_config_cls cm = ClientConfigAdapter( dummy_strategy_config_cls(no_default="some value")) with TemporaryDirectory() as d: d = Path(d) import_command.STRATEGIES_CONF_DIR_PATH = d temp_file_name = d / strategy_file_name cm_yml_str = cm.generate_yml_output_str_with_comments() cm_yml_str = cm_yml_str.replace("\nno_default: some value\n", "") with open(temp_file_name, "w+") as outfile: outfile.write(cm_yml_str) self.async_run_with_timeout( self.app.import_config_file(strategy_file_name)) self.assertEqual(strategy_file_name, self.app.strategy_file_name) self.assertEqual(strategy_name, self.app.strategy_name) self.assertTrue( self.cli_mock_assistant.check_log_called_with( "\nEnter \"start\" to start market making.")) self.assertNotEqual(cm, self.app.strategy_config_map) validation_errors = self.app.strategy_config_map.validate_model() self.assertEqual(1, len(validation_errors)) self.assertEqual("no_default - field required", validation_errors[0])
class HistoryCommandTest(unittest.TestCase): @patch("hummingbot.core.utils.trading_pair_fetcher.TradingPairFetcher") def setUp(self, _: MagicMock) -> None: super().setUp() self.app = HummingbotApplication() self.ev_loop = asyncio.get_event_loop() self.cli_mock_assistant = CLIMockingAssistant(self.app.app) self.cli_mock_assistant.start() self.global_config_backup = deepcopy(global_config_map) def tearDown(self) -> None: self.cli_mock_assistant.stop() self.reset_global_config() super().tearDown() def reset_global_config(self): for key, value in self.global_config_backup.items(): global_config_map[key] = value @staticmethod def get_async_sleep_fn(delay: float): async def async_sleep(*_, **__): await asyncio.sleep(delay) return async_sleep def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) return ret def async_run_with_timeout_coroutine_must_raise_timeout(self, coroutine: Awaitable, timeout: float = 1): class DesiredError(Exception): pass async def run_coro_that_raises(coro: Awaitable): try: await coro except asyncio.TimeoutError: raise DesiredError try: self.async_run_with_timeout(run_coro_that_raises(coroutine), timeout) except DesiredError: # the coroutine raised an asyncio.TimeoutError as expected raise asyncio.TimeoutError except asyncio.TimeoutError: # the coroutine did not finish on time raise RuntimeError @staticmethod def get_trades() -> List[TradeFill]: trade_fee = TradeFee(percent=Decimal("5")) trades = [ TradeFill( id=1, config_file_path="some-strategy.yml", strategy="pure_market_making", market="binance", symbol="BTC-USDT", base_asset="BTC", quote_asset="USDT", timestamp=int(time.time()), order_id="someId", trade_type="BUY", order_type="LIMIT", price=1, amount=2, leverage=1, trade_fee=TradeFee.to_json(trade_fee), exchange_trade_id="someExchangeId", ) ] return trades @patch("hummingbot.client.command.history_command.HistoryCommand.get_current_balances") def test_history_report_raises_on_get_current_balances_network_timeout( self, get_current_balances_mock: AsyncMock ): get_current_balances_mock.side_effect = self.get_async_sleep_fn(delay=0.02) global_config_map["other_commands_timeout"].value = 0.01 trades = self.get_trades() with self.assertRaises(asyncio.TimeoutError): self.async_run_with_timeout_coroutine_must_raise_timeout( self.app.history_report(start_time=time.time(), trades=trades), 10000 ) self.assertTrue( self.cli_mock_assistant.check_log_called_with( msg="\nA network error prevented the balances retrieval to complete. See logs for more details." ) )
class BalanceCommandTest(unittest.TestCase): @patch("hummingbot.core.utils.trading_pair_fetcher.TradingPairFetcher") def setUp(self, _: MagicMock) -> None: super().setUp() self.ev_loop = asyncio.get_event_loop() self.async_run_with_timeout(read_system_configs_from_yml()) self.app = HummingbotApplication() self.cli_mock_assistant = CLIMockingAssistant(self.app.app) self.cli_mock_assistant.start() def tearDown(self) -> None: self.cli_mock_assistant.stop() super().tearDown() @staticmethod def get_async_sleep_fn(delay: float): async def async_sleep(*_, **__): await asyncio.sleep(delay) return async_sleep def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def async_run_with_timeout_coroutine_must_raise_timeout( self, coroutine: Awaitable, timeout: float = 1): class DesiredError(Exception): pass async def run_coro_that_raises(coro: Awaitable): try: await coro except asyncio.TimeoutError: raise DesiredError try: self.async_run_with_timeout(run_coro_that_raises(coroutine), timeout) except DesiredError: # the coroutine raised an asyncio.TimeoutError as expected raise asyncio.TimeoutError except asyncio.TimeoutError: # the coroutine did not finish on time raise RuntimeError @patch( "hummingbot.user.user_balances.UserBalances.all_balances_all_exchanges" ) def test_show_balances_handles_network_timeouts( self, all_balances_all_exchanges_mock): all_balances_all_exchanges_mock.side_effect = self.get_async_sleep_fn( delay=0.02) self.app.client_config_map.commands_timeout.other_commands_timeout = Decimal( "0.01") with self.assertRaises(asyncio.TimeoutError): self.async_run_with_timeout_coroutine_must_raise_timeout( self.app.show_balances()) self.assertTrue( self.cli_mock_assistant.check_log_called_with( msg= "\nA network error prevented the balances to update. See logs for more details." ))
class StatusCommandTest(unittest.TestCase): @patch("hummingbot.core.utils.trading_pair_fetcher.TradingPairFetcher") def setUp(self, _: MagicMock) -> None: super().setUp() self.ev_loop = asyncio.get_event_loop() self.app = HummingbotApplication() self.cli_mock_assistant = CLIMockingAssistant(self.app.app) self.cli_mock_assistant.start() self.global_config_backup = deepcopy(global_config_map) def tearDown(self) -> None: self.cli_mock_assistant.stop() self.reset_global_config() super().tearDown() def reset_global_config(self): for key, value in self.global_config_backup.items(): global_config_map[key] = value @staticmethod def get_async_sleep_fn(delay: float): async def async_sleep(*_, **__): await asyncio.sleep(delay) return async_sleep def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) return ret def async_run_with_timeout_coroutine_must_raise_timeout(self, coroutine: Awaitable, timeout: float = 1): class DesiredError(Exception): pass async def run_coro_that_raises(coro: Awaitable): try: await coro except asyncio.TimeoutError: raise DesiredError try: self.async_run_with_timeout(run_coro_that_raises(coroutine), timeout) except DesiredError: # the coroutine raised an asyncio.TimeoutError as expected raise asyncio.TimeoutError except asyncio.TimeoutError: # the coroutine did not finish on time raise RuntimeError @patch("hummingbot.client.command.status_command.StatusCommand.validate_required_connections") @patch("hummingbot.client.config.security.Security.is_decryption_done") def test_status_check_all_handles_network_timeouts( self, is_decryption_done_mock, validate_required_connections_mock ): validate_required_connections_mock.side_effect = self.get_async_sleep_fn(delay=0.02) global_config_map["other_commands_timeout"].value = 0.01 is_decryption_done_mock.return_value = True strategy_name = "some-strategy" self.app.strategy_name = strategy_name self.app.strategy_file_name = f"{strategy_name}.yml" with self.assertRaises(asyncio.TimeoutError): self.async_run_with_timeout_coroutine_must_raise_timeout(self.app.status_check_all()) self.assertTrue( self.cli_mock_assistant.check_log_called_with( msg="\nA network error prevented the connection check to complete. See logs for more details." ) )
class ConfigCommandTest(unittest.TestCase): @patch("hummingbot.core.utils.trading_pair_fetcher.TradingPairFetcher") def setUp(self, _: MagicMock) -> None: super().setUp() self.ev_loop = asyncio.get_event_loop() self.async_run_with_timeout(read_system_configs_from_yml()) self.client_config = ClientConfigMap() self.config_adapter = ClientConfigAdapter(self.client_config) self.app = HummingbotApplication(client_config_map=self.config_adapter) self.cli_mock_assistant = CLIMockingAssistant(self.app.app) self.cli_mock_assistant.start() def tearDown(self) -> None: self.cli_mock_assistant.stop() super().tearDown() def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) return ret @patch("hummingbot.client.hummingbot_application.get_strategy_config_map") @patch("hummingbot.client.hummingbot_application.HummingbotApplication.notify") def test_list_configs(self, notify_mock, get_strategy_config_map_mock): captures = [] notify_mock.side_effect = lambda s: captures.append(s) strategy_name = "some-strategy" self.app.strategy_name = strategy_name strategy_config_map_mock = { "five": ConfigVar(key="five", prompt=""), "six": ConfigVar(key="six", prompt="", default="sixth"), } strategy_config_map_mock["five"].value = "fifth" strategy_config_map_mock["six"].value = "sixth" get_strategy_config_map_mock.return_value = strategy_config_map_mock self.app.list_configs() self.assertEqual(6, len(captures)) self.assertEqual("\nGlobal Configurations:", captures[0]) df_str_expected = (" +--------------------------+----------------------+\n" " | Key | Value |\n" " |--------------------------+----------------------|\n" " | kill_switch_mode | kill_switch_disabled |\n" " | autofill_import | disabled |\n" " | telegram_mode | telegram_disabled |\n" " | send_error_logs | True |\n" " | pmm_script_mode | pmm_script_disabled |\n" " | gateway | |\n" " | ∟ gateway_api_host | localhost |\n" " | ∟ gateway_api_port | 5000 |\n" " | rate_oracle_source | binance |\n" " | global_token | |\n" " | ∟ global_token_symbol | $ |\n" " | rate_limits_share_pct | 100 |\n" " | commands_timeout | |\n" " | ∟ create_command_timeout | 10 |\n" " | ∟ other_commands_timeout | 30 |\n" " | tables_format | psql |\n" " +--------------------------+----------------------+") self.assertEqual(df_str_expected, captures[1]) self.assertEqual("\nColor Settings:", captures[2]) df_str_expected = (" +--------------------+---------+\n" " | Key | Value |\n" " |--------------------+---------|\n" " | ∟ top_pane | #000000 |\n" " | ∟ bottom_pane | #000000 |\n" " | ∟ output_pane | #262626 |\n" " | ∟ input_pane | #1C1C1C |\n" " | ∟ logs_pane | #121212 |\n" " | ∟ terminal_primary | #5FFFD7 |\n" " +--------------------+---------+") self.assertEqual(df_str_expected, captures[3]) self.assertEqual("\nStrategy Configurations:", captures[4]) df_str_expected = ( " +-------+---------+" "\n | Key | Value |" "\n |-------+---------|" "\n | five | fifth |" "\n | six | sixth |" "\n +-------+---------+" ) self.assertEqual(df_str_expected, captures[5]) @patch("hummingbot.client.hummingbot_application.get_strategy_config_map") @patch("hummingbot.client.hummingbot_application.HummingbotApplication.notify") def test_list_configs_pydantic_model(self, notify_mock, get_strategy_config_map_mock): captures = [] notify_mock.side_effect = lambda s: captures.append(s) strategy_name = "some-strategy" self.app.strategy_name = strategy_name class DoubleNestedModel(BaseClientModel): double_nested_attr: float = Field(default=3.0) class Config: title = "double_nested_model" class NestedModelOne(BaseClientModel): nested_attr: str = Field(default="some value") double_nested_model: DoubleNestedModel = Field(default=DoubleNestedModel()) class Config: title = "nested_mode_one" class NestedModelTwo(BaseClientModel): class Config: title = "nested_mode_two" class DummyModel(BaseClientModel): some_attr: int = Field(default=1) nested_model: Union[NestedModelTwo, NestedModelOne] = Field(default=NestedModelOne()) another_attr: Decimal = Field(default=Decimal("1.0")) missing_no_default: int = Field(default=...) class Config: title = "dummy_model" get_strategy_config_map_mock.return_value = ClientConfigAdapter(DummyModel.construct()) self.app.list_configs() self.assertEqual(6, len(captures)) self.assertEqual("\nStrategy Configurations:", captures[4]) df_str_expected = ( " +------------------------+------------------------+" "\n | Key | Value |" "\n |------------------------+------------------------|" "\n | some_attr | 1 |" "\n | nested_model | nested_mode_one |" "\n | ∟ nested_attr | some value |" "\n | ∟ double_nested_model | |" "\n | ∟ double_nested_attr | 3.0 |" "\n | another_attr | 1.0 |" "\n | missing_no_default | &cMISSING_AND_REQUIRED |" "\n +------------------------+------------------------+" ) self.assertEqual(df_str_expected, captures[5]) @patch("hummingbot.client.hummingbot_application.get_strategy_config_map") @patch("hummingbot.client.hummingbot_application.HummingbotApplication.notify") def test_config_non_configurable_key_fails(self, notify_mock, get_strategy_config_map_mock): class DummyModel(BaseStrategyConfigMap): strategy: str = Field(default="pure_market_making", client_data=None) some_attr: int = Field(default=1, client_data=ClientFieldData(prompt=lambda mi: "some prompt")) another_attr: Decimal = Field(default=Decimal("1.0")) class Config: title = "dummy_model" strategy_name = "some-strategy" self.app.strategy_name = strategy_name get_strategy_config_map_mock.return_value = ClientConfigAdapter(DummyModel.construct()) self.app.config(key="some_attr") notify_mock.assert_not_called() self.app.config(key="another_attr") notify_mock.assert_called_once_with("Invalid key, please choose from the list.") notify_mock.reset_mock() self.app.config(key="some_key") notify_mock.assert_called_once_with("Invalid key, please choose from the list.") @patch("hummingbot.client.command.config_command.save_to_yml") @patch("hummingbot.client.hummingbot_application.get_strategy_config_map") @patch("hummingbot.client.hummingbot_application.HummingbotApplication.notify") def test_config_single_keys(self, _, get_strategy_config_map_mock, save_to_yml_mock): class NestedModel(BaseClientModel): nested_attr: str = Field( default="some value", client_data=ClientFieldData(prompt=lambda mi: "some prompt") ) class Config: title = "nested_model" class DummyModel(BaseStrategyConfigMap): strategy: str = Field(default="pure_market_making", client_data=None) some_attr: int = Field(default=1, client_data=ClientFieldData(prompt=lambda mi: "some prompt")) nested_model: NestedModel = Field(default=NestedModel()) class Config: title = "dummy_model" strategy_name = "some-strategy" self.app.strategy_name = strategy_name self.app.strategy_file_name = f"{strategy_name}.yml" config_map = ClientConfigAdapter(DummyModel.construct()) get_strategy_config_map_mock.return_value = config_map self.async_run_with_timeout(self.app._config_single_key(key="some_attr", input_value=2)) self.assertEqual(2, config_map.some_attr) save_to_yml_mock.assert_called_once() save_to_yml_mock.reset_mock() self.cli_mock_assistant.queue_prompt_reply("3") self.async_run_with_timeout(self.app._config_single_key(key="some_attr", input_value=None)) self.assertEqual(3, config_map.some_attr) save_to_yml_mock.assert_called_once() save_to_yml_mock.reset_mock() self.cli_mock_assistant.queue_prompt_reply("another value") self.async_run_with_timeout(self.app._config_single_key(key="nested_model.nested_attr", input_value=None)) self.assertEqual("another value", config_map.nested_model.nested_attr) save_to_yml_mock.assert_called_once()