import pytest import requests from unittest.mock import patch from ray.dashboard.metrics_exporter.actions import ActionHandler from ray.dashboard.metrics_exporter.client import MetricsExportClient from ray.dashboard.metrics_exporter.client import Exporter from ray.dashboard.metrics_exporter.schema import (AuthResponse, BaseModel, ValidationError, Field) MOCK_DASHBOARD_ID = "1234" MOCK_DASHBOARD_ADDRESS = "http://127.0.0.1:9081" MOCK_ACCESS_TOKEN = "1234" def _setup_client_and_exporter(controller): exporter = Exporter(MOCK_DASHBOARD_ID, MOCK_DASHBOARD_ADDRESS, controller) client = MetricsExportClient(MOCK_DASHBOARD_ADDRESS, controller, MOCK_DASHBOARD_ID, exporter) return exporter, client """ Test Exporter """ @patch("ray.dashboard.dashboard.DashboardController") def test_verify_exporter_cannot_run_without_access_token(mock_controller): exporter, client = _setup_client_and_exporter(mock_controller) # Should raise an assertion error because there's no access token set. with pytest.raises(AssertionError): exporter.run() """ Test Client """ @patch("ray.dashboard.dashboard.DashboardController") @patch( "ray.dashboard.metrics_exporter.api.authentication_request", side_effect=requests.exceptions.HTTPError) def test_client_invalid_request_status_returned(auth_request, mock_controller): """ If authentication request fails with an invalid status code, `start_exporting_metrics` should fail. """ exporter, client = _setup_client_and_exporter(mock_controller) # authenticate should throw an exception because API request fails. with pytest.raises(requests.exceptions.HTTPError): client._authenticate() # This should fail because authentication throws an exception. result, error = client.start_exporting_metrics() assert result is False @patch("ray.dashboard.dashboard.DashboardController") @patch("ray.dashboard.metrics_exporter.api.authentication_request") def test_authentication(auth_request, mock_controller): auth_request.return_value = AuthResponse( access_token_dashboard=MOCK_ACCESS_TOKEN, access_token_ingest=MOCK_ACCESS_TOKEN) exporter, client = _setup_client_and_exporter(mock_controller) assert client.enabled is False client._authenticate() assert client.dashboard_url == "{address}/dashboard/{access_token}".format( address=MOCK_DASHBOARD_ADDRESS, access_token=MOCK_ACCESS_TOKEN) assert client.enabled is True @patch.object(Exporter, "start") @patch("ray.dashboard.dashboard.DashboardController") @patch("ray.dashboard.metrics_exporter.api.authentication_request") def test_start_exporting_metrics_without_authentication( auth_request, mock_controller, start): """ `start_exporting_metrics` should trigger authentication if users are not authenticated. """ auth_request.return_value = AuthResponse( access_token_dashboard=MOCK_ACCESS_TOKEN, access_token_ingest=MOCK_ACCESS_TOKEN) exporter, client = _setup_client_and_exporter(mock_controller) # start_exporting_metrics should succeed. result, error = client.start_exporting_metrics() assert result is True assert error is None assert client.enabled is True @patch.object(Exporter, "start") @patch("ray.dashboard.dashboard.DashboardController") @patch("ray.dashboard.metrics_exporter.api.authentication_request") def test_start_exporting_metrics_with_authentication(auth_request, mock_controller, start): """ If users are already authenticated, `start_exporting_metrics` should not authenticate users. """ auth_request.return_value = AuthResponse( access_token_dashboard=MOCK_ACCESS_TOKEN, access_token_ingest=MOCK_ACCESS_TOKEN) exporter, client = _setup_client_and_exporter(mock_controller) # Already authenticated. client._authenticate() assert client.enabled is True result, error = client.start_exporting_metrics() # Auth request should be called only once because # it was already authenticated. auth_request.call_count == 1 assert result is True assert error is None @patch.object(Exporter, "start") @patch("ray.dashboard.dashboard.DashboardController") @patch("ray.dashboard.metrics_exporter.api.authentication_request") def test_start_exporting_metrics_succeed(auth_request, mock_controller, start): auth_request.return_value = AuthResponse( access_token_dashboard=MOCK_ACCESS_TOKEN, access_token_ingest=MOCK_ACCESS_TOKEN) exporter, client = _setup_client_and_exporter(mock_controller) result, error = client.start_exporting_metrics() assert result is True assert error is None assert client.is_exporting_started is True start.call_count == 1 with pytest.raises(AssertionError): client.start_exporting_metrics() """ BaseModel Test """ def test_base_model(): DEFAULT_VALUE = "default" class A(BaseModel): __schema__ = { "a": Field(required=True, default=None, type=str), "b": Field(required=False, default=DEFAULT_VALUE, type=str) } # Test the correct case. obj = {"a": "1", "b": "1"} a = A.parse_obj(obj) assert a.a == "1" assert a.b == "1" assert a._dict == obj string = "{name}\n{dict}".format(name=A.__name__, dict=str(obj)) assert str(a) == string # Test wrong types. It is not checked in the current implementation. obj = {"a": 1, "b": 2} a = A.parse_obj(obj) assert a.a == 1 assert a.b == 2 # Test wrong types. parse_obj can only parse dictionary. obj = None with pytest.raises(AssertionError): a = A.parse_obj(obj) # Test when required fields are not provided. obj = {"b": "1"} with pytest.raises(ValidationError): a = A.parse_obj(obj) # Test optional fields are set to default when fields are not given. obj = {"a": "1"} a = A.parse_obj(obj) assert a.b == DEFAULT_VALUE # Test when fields that are not defined in the schema is given. # It should be ignoered obj = {"a": "a", "b": "b", "c": "c"} a = A.parse_obj(obj) assert a.a == "a" assert a.b == "b" assert a.c == "c" """ Test Action Handler """ def _get_mock_kill_action(): return { "type": "KILL_ACTOR", "actor_id": "1234", "ip_address": "1234", "port": 30 } @patch("ray.dashboard.dashboard.DashboardController") def test_handle_kill_action(mock_controller): action_handler = ActionHandler(mock_controller) kill_action = _get_mock_kill_action() action_handler.handle_kill_action(kill_action) assert mock_controller.kill_actor.call_count == 1 @patch("ray.dashboard.dashboard.DashboardController") def test_handle_kill_action_invalid_dict(mock_controller): action_handler = ActionHandler(mock_controller) kill_action = {"type": "KILL_ACTOR", "ip_address": "1234", "port": 30} with pytest.raises(ValidationError): action_handler.handle_kill_action(kill_action) @patch("ray.dashboard.dashboard.DashboardController") def test_handle_actions_many_kill_actor(mock_controller): action_handler = ActionHandler(mock_controller) # 10 actions required. actions = [_get_mock_kill_action() for _ in range(10)] action_handler.handle_actions(actions) assert mock_controller.kill_actor.call_count == 10 @patch("ray.dashboard.dashboard.DashboardController") def test_handle_actions_kill_actor_and_mixed_type(mock_controller): action_handler = ActionHandler(mock_controller) wrong_type_action = {"type": "NON_EXIST"} actions = [ _get_mock_kill_action(), wrong_type_action, _get_mock_kill_action() ] action_handler.handle_actions(actions) assert mock_controller.kill_actor.call_count == 2 @patch("ray.dashboard.dashboard.DashboardController") def test_handle_actions_only_wrong_type(mock_controller): action_handler = ActionHandler(mock_controller) wrong_type_action = {"type": "NON_EXIST"} actions = [wrong_type_action for _ in range(10)] action_handler.handle_actions(actions) assert mock_controller.kill_actor.call_count == 0 if __name__ == "__main__": import sys sys.exit(pytest.main(["-v", __file__]))