Files
ray-project--ray/python/ray/tests/unit/test_cli_output_context.py
T
2026-07-13 13:17:40 +08:00

151 lines
5.2 KiB
Python

"""Tests for CLI output context disambiguation (ray start / ray up).
These tests directly import and call the output helper functions from
``cli_output_helpers``, capturing ``cli_logger`` calls via mocks.
No Ray C++ runtime (``_raylet``) is required. The helpers module is
imported by path to avoid triggering ``import ray``.
Validates that:
1. ``print_next_steps_context_note`` emits "head node" and "cluster network".
2. ``print_head_node_context_separator`` emits "head node" and "cluster network".
3. ``USEFUL_COMMANDS_HEADING`` contains "local machine".
4. The head-node separator is skipped when ``ray start`` did not run.
5. Neither helper references ``ray up`` (valid for direct ``ray start`` use).
"""
import importlib.util
import os
import unittest
from unittest.mock import MagicMock
# ---------------------------------------------------------------------------
# Import cli_output_helpers by file path so we bypass ``import ray`` entirely.
# ---------------------------------------------------------------------------
_HELPERS_PATH = os.path.join(
os.path.dirname(__file__),
os.pardir,
os.pardir,
"autoscaler",
"_private",
"cli_output_helpers.py",
)
_spec = importlib.util.spec_from_file_location(
"cli_output_helpers", os.path.abspath(_HELPERS_PATH)
)
_helpers = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_helpers)
print_next_steps_context_note = _helpers.print_next_steps_context_note
print_head_node_context_separator = _helpers.print_head_node_context_separator
print_head_node_context_separator_if_needed = (
_helpers.print_head_node_context_separator_if_needed
)
USEFUL_COMMANDS_HEADING = _helpers.USEFUL_COMMANDS_HEADING
def _capture_printed_text(helper_fn):
"""Call *helper_fn(cli_logger, cf)* with mocks and return all text
that was passed to ``cli_logger.print`` as a single concatenated string.
``cf.dimmed`` is mocked as an identity function so the raw text
passes through unchanged.
"""
mock_logger = MagicMock()
mock_cf = MagicMock()
# cf.dimmed should return its argument so we can inspect the raw text
mock_cf.dimmed = lambda x: x
helper_fn(mock_logger, mock_cf)
parts = []
for c in mock_logger.print.call_args_list:
args, _ = c
parts.extend(str(a) for a in args)
return "\n".join(parts)
class TestNextStepsContextNote(unittest.TestCase):
"""Tests for ``print_next_steps_context_note``."""
def test_mentions_head_node(self):
text = _capture_printed_text(print_next_steps_context_note)
self.assertIn("head node", text)
def test_mentions_cluster_network(self):
text = _capture_printed_text(print_next_steps_context_note)
self.assertIn("cluster network", text)
def test_calls_newline(self):
mock_logger = MagicMock()
mock_cf = MagicMock()
mock_cf.dimmed = lambda x: x
print_next_steps_context_note(mock_logger, mock_cf)
mock_logger.newline.assert_called()
def test_does_not_mention_ray_up(self):
text = _capture_printed_text(print_next_steps_context_note)
self.assertNotIn("ray up", text)
class TestHeadNodeContextSeparator(unittest.TestCase):
"""Tests for ``print_head_node_context_separator``."""
def test_mentions_head_node(self):
text = _capture_printed_text(print_head_node_context_separator)
self.assertIn("head node", text)
def test_mentions_cluster_network(self):
text = _capture_printed_text(print_head_node_context_separator)
self.assertIn("cluster network", text)
def test_prints_ascii_separator(self):
text = _capture_printed_text(print_head_node_context_separator)
# Should contain a run of ASCII dashes as a visual separator
self.assertIn("----", text)
def test_calls_newline(self):
mock_logger = MagicMock()
mock_cf = MagicMock()
mock_cf.dimmed = lambda x: x
print_head_node_context_separator(mock_logger, mock_cf)
mock_logger.newline.assert_called()
class TestUsefulCommandsHeading(unittest.TestCase):
"""Tests for the ``USEFUL_COMMANDS_HEADING`` constant."""
def test_contains_local_machine(self):
self.assertIn("local machine", USEFUL_COMMANDS_HEADING)
def test_does_not_use_old_label(self):
# The old heading was exactly "Useful commands:" with no qualifier
self.assertNotEqual(USEFUL_COMMANDS_HEADING, "Useful commands:")
class TestHeadNodeContextGating(unittest.TestCase):
"""Tests for gating the head-node context separator."""
def test_skips_separator_when_ray_start_commands_are_empty(self):
mock_logger = MagicMock()
mock_cf = MagicMock()
mock_cf.dimmed = lambda x: x
print_head_node_context_separator_if_needed([], mock_logger, mock_cf)
mock_logger.print.assert_not_called()
mock_logger.newline.assert_not_called()
def test_prints_separator_when_ray_start_commands_exist(self):
text = _capture_printed_text(
lambda cli_logger, cf: print_head_node_context_separator_if_needed(
["ray start --head"], cli_logger, cf
)
)
self.assertIn("head node", text)
if __name__ == "__main__":
unittest.main()