|  | 
| 11 | 11 | import sys | 
| 12 | 12 | import unittest | 
| 13 | 13 | from datetime import datetime | 
| 14 |  | -from typing import Any, Dict | 
|  | 14 | +from typing import Any, cast, Dict | 
| 15 | 15 | from unittest.mock import MagicMock, patch | 
| 16 | 16 | 
 | 
| 17 | 17 | import torchx | 
| @@ -726,6 +726,7 @@ def test_runopts(self) -> None: | 
| 726 | 726 |                 "image_repo", | 
| 727 | 727 |                 "service_account", | 
| 728 | 728 |                 "priority_class", | 
|  | 729 | +                "validate_spec", | 
| 729 | 730 |             }, | 
| 730 | 731 |         ) | 
| 731 | 732 | 
 | 
| @@ -929,6 +930,102 @@ def test_min_replicas(self) -> None: | 
| 929 | 930 |         ] | 
| 930 | 931 |         self.assertEqual(min_available, [1, 1, 0]) | 
| 931 | 932 | 
 | 
|  | 933 | +    @patch( | 
|  | 934 | +        "torchx.schedulers.kubernetes_scheduler.KubernetesScheduler._custom_objects_api" | 
|  | 935 | +    ) | 
|  | 936 | +    def test_validate_spec_invalid_name(self, mock_api: MagicMock) -> None: | 
|  | 937 | +        from kubernetes.client.rest import ApiException | 
|  | 938 | + | 
|  | 939 | +        scheduler = create_scheduler("test") | 
|  | 940 | +        app = _test_app() | 
|  | 941 | +        app.name = "Invalid_Name" | 
|  | 942 | + | 
|  | 943 | +        mock_api_instance = MagicMock() | 
|  | 944 | +        mock_api_instance.create_namespaced_custom_object.side_effect = ApiException( | 
|  | 945 | +            status=422, | 
|  | 946 | +            reason="Invalid", | 
|  | 947 | +        ) | 
|  | 948 | +        mock_api.return_value = mock_api_instance | 
|  | 949 | + | 
|  | 950 | +        cfg = cast(KubernetesOpts, {"queue": "testqueue", "validate_spec": True}) | 
|  | 951 | + | 
|  | 952 | +        with self.assertRaises(ValueError) as ctx: | 
|  | 953 | +            scheduler.submit_dryrun(app, cfg) | 
|  | 954 | + | 
|  | 955 | +        self.assertIn("Invalid job spec", str(ctx.exception)) | 
|  | 956 | +        mock_api_instance.create_namespaced_custom_object.assert_called_once() | 
|  | 957 | +        call_kwargs = mock_api_instance.create_namespaced_custom_object.call_args[1] | 
|  | 958 | +        self.assertEqual(call_kwargs["dry_run"], "All") | 
|  | 959 | + | 
|  | 960 | +    def test_validate_spec_enabled_by_default(self) -> None: | 
|  | 961 | +        scheduler = create_scheduler("test") | 
|  | 962 | +        app = _test_app() | 
|  | 963 | + | 
|  | 964 | +        cfg = KubernetesOpts({"queue": "testqueue"}) | 
|  | 965 | + | 
|  | 966 | +        with patch( | 
|  | 967 | +            "torchx.schedulers.kubernetes_scheduler.KubernetesScheduler._custom_objects_api" | 
|  | 968 | +        ) as mock_api: | 
|  | 969 | +            mock_api_instance = MagicMock() | 
|  | 970 | +            mock_api_instance.create_namespaced_custom_object.return_value = {} | 
|  | 971 | +            mock_api.return_value = mock_api_instance | 
|  | 972 | + | 
|  | 973 | +            info = scheduler.submit_dryrun(app, cfg) | 
|  | 974 | + | 
|  | 975 | +        self.assertIsNotNone(info) | 
|  | 976 | +        mock_api_instance.create_namespaced_custom_object.assert_called_once() | 
|  | 977 | +        call_kwargs = mock_api_instance.create_namespaced_custom_object.call_args[1] | 
|  | 978 | +        self.assertEqual(call_kwargs["dry_run"], "All") | 
|  | 979 | + | 
|  | 980 | +    @patch( | 
|  | 981 | +        "torchx.schedulers.kubernetes_scheduler.KubernetesScheduler._custom_objects_api" | 
|  | 982 | +    ) | 
|  | 983 | +    def test_validate_spec_invalid_task_name(self, mock_api: MagicMock) -> None: | 
|  | 984 | +        from kubernetes.client.rest import ApiException | 
|  | 985 | + | 
|  | 986 | +        scheduler = create_scheduler("test") | 
|  | 987 | +        app = _test_app() | 
|  | 988 | +        app.roles[0].name = "Invalid-Task-Name" | 
|  | 989 | + | 
|  | 990 | +        mock_api_instance = MagicMock() | 
|  | 991 | +        mock_api_instance.create_namespaced_custom_object.side_effect = ApiException( | 
|  | 992 | +            status=422, | 
|  | 993 | +            reason="Invalid", | 
|  | 994 | +        ) | 
|  | 995 | +        mock_api.return_value = mock_api_instance | 
|  | 996 | + | 
|  | 997 | +        cfg = cast(KubernetesOpts, {"queue": "testqueue", "validate_spec": True}) | 
|  | 998 | + | 
|  | 999 | +        with self.assertRaises(ValueError) as ctx: | 
|  | 1000 | +            scheduler.submit_dryrun(app, cfg) | 
|  | 1001 | + | 
|  | 1002 | +        self.assertIn("Invalid job spec", str(ctx.exception)) | 
|  | 1003 | + | 
|  | 1004 | +    @patch( | 
|  | 1005 | +        "torchx.schedulers.kubernetes_scheduler.KubernetesScheduler._custom_objects_api" | 
|  | 1006 | +    ) | 
|  | 1007 | +    def test_validate_spec_long_pod_name(self, mock_api: MagicMock) -> None: | 
|  | 1008 | +        scheduler = create_scheduler("test") | 
|  | 1009 | +        app = _test_app() | 
|  | 1010 | +        app.name = "x" * 50 | 
|  | 1011 | +        app.roles[0].name = "y" * 20 | 
|  | 1012 | + | 
|  | 1013 | +        mock_api_instance = MagicMock() | 
|  | 1014 | +        mock_api_instance.create_namespaced_custom_object.return_value = {} | 
|  | 1015 | +        mock_api.return_value = mock_api_instance | 
|  | 1016 | + | 
|  | 1017 | +        cfg = cast(KubernetesOpts, {"queue": "testqueue", "validate_spec": True}) | 
|  | 1018 | + | 
|  | 1019 | +        with patch( | 
|  | 1020 | +            "torchx.schedulers.kubernetes_scheduler.make_unique" | 
|  | 1021 | +        ) as make_unique_ctx: | 
|  | 1022 | +            make_unique_ctx.return_value = "x" * 50 | 
|  | 1023 | +            with self.assertRaises(ValueError) as ctx: | 
|  | 1024 | +                scheduler.submit_dryrun(app, cfg) | 
|  | 1025 | + | 
|  | 1026 | +        self.assertIn("Pod name", str(ctx.exception)) | 
|  | 1027 | +        self.assertIn("exceeds 63 character limit", str(ctx.exception)) | 
|  | 1028 | + | 
| 932 | 1029 | 
 | 
| 933 | 1030 | class KubernetesSchedulerNoImportTest(unittest.TestCase): | 
| 934 | 1031 |     """ | 
|  | 
0 commit comments