From 630e09bf949aca33ba02a18e19b8d17431fb8bbe Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 08:08:22 +0000 Subject: [PATCH] test: add unit tests for the CLI commands Adds comprehensive unit tests for `neuroscope/cli.py` to ensure proper argument parsing, command routing, and correct behavior of `main()`, `run_server()`, `run_demo()`, and `show_version()`. Mocking is used heavily to verify behavior deterministically without the need to bind network ports, load slow machine learning framework binaries, or start infinite sleep loops. Co-authored-by: harshithluc073 <101515387+harshithluc073@users.noreply.github.com> --- patch.diff | 32 ++++++++ tests/test_cli.py | 195 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 patch.diff create mode 100644 tests/test_cli.py diff --git a/patch.diff b/patch.diff new file mode 100644 index 0000000..bc406e8 --- /dev/null +++ b/patch.diff @@ -0,0 +1,32 @@ +<<<<<<< SEARCH +@patch("builtins.__import__", side_effect=ImportError) +def test_run_demo_no_torch(mock_import, capsys): + """Test running the demo when torch is not installed.""" + # We only want to mock torch import, let others pass + original_import = __import__ + def side_effect(name, *args, **kwargs): + if name == "torch": + raise ImportError("No module named 'torch'") + return original_import(name, *args, **kwargs) + + mock_import.side_effect = side_effect + + args = MagicMock() + args.model = "mlp" + + assert run_demo(args) == 1 + + out, err = capsys.readouterr() + assert "[ERROR] PyTorch is required for demo" in out +======= +def test_run_demo_no_torch(capsys): + """Test running the demo when torch is not installed.""" + args = MagicMock() + args.model = "mlp" + + with patch.dict("sys.modules", {"torch": None}): + assert run_demo(args) == 1 + + out, err = capsys.readouterr() + assert "[ERROR] PyTorch is required for demo" in out +>>>>>>> REPLACE diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..b4eafdc --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,195 @@ +"""Tests for the command-line interface.""" + +import sys +from unittest.mock import MagicMock, patch + +import pytest + +from neuroscope.cli import main, run_demo, run_server, show_version + + +@pytest.fixture +def mock_argv(monkeypatch): + """Fixture to easily mock sys.argv.""" + def _mock(*args): + monkeypatch.setattr(sys, "argv", ["neuroscope", *args]) + return _mock + + +def test_main_no_args(mock_argv, capsys): + """Test that main with no args prints help and returns 0.""" + mock_argv() + + # We need to mock parser.print_help since argparse's default behavior + # might differ slightly, but we can just let it print and capture it. + with patch("argparse.ArgumentParser.print_help") as mock_print_help: + assert main() == 0 + mock_print_help.assert_called_once() + + +@patch("neuroscope.cli.run_server") +def test_main_server_command(mock_run_server, mock_argv): + """Test routing to server command.""" + mock_run_server.return_value = 0 + mock_argv("server", "--port", "9000", "--host", "0.0.0.0", "--no-browser") + + assert main() == 0 + + mock_run_server.assert_called_once() + args = mock_run_server.call_args[0][0] + assert args.port == 9000 + assert args.host == "0.0.0.0" + assert args.no_browser is True + + +@patch("neuroscope.cli.run_demo") +def test_main_demo_command(mock_run_demo, mock_argv): + """Test routing to demo command.""" + mock_run_demo.return_value = 0 + mock_argv("demo", "--model", "cnn") + + assert main() == 0 + + mock_run_demo.assert_called_once() + args = mock_run_demo.call_args[0][0] + assert args.model == "cnn" + + +@patch("neuroscope.cli.show_version") +def test_main_version_command(mock_show_version, mock_argv): + """Test routing to version command.""" + mock_show_version.return_value = 0 + mock_argv("version") + + assert main() == 0 + mock_show_version.assert_called_once() + + +@patch("neuroscope.core.server.NeuroScopeServer") +@patch("time.sleep", side_effect=KeyboardInterrupt) +def test_run_server_normal(mock_sleep, mock_server_cls, capsys): + """Test running the server with normal shutdown.""" + # Setup mock server + mock_server = MagicMock() + mock_server_cls.return_value = mock_server + + # Setup args + args = MagicMock() + args.host = "localhost" + args.port = 8765 + args.no_browser = False + + assert run_server(args) == 0 + + mock_server_cls.assert_called_once_with(host="localhost", port=8765) + mock_server.start.assert_called_once_with(open_browser=True) + mock_server.stop.assert_called_once() + + out, err = capsys.readouterr() + assert "Starting server..." in out + assert "Shutting down..." in out + + +def test_show_version(capsys): + """Test the version display.""" + from neuroscope import __version__ + assert show_version() == 0 + out, err = capsys.readouterr() + assert f"NeuroScope {__version__}" in out + + +@patch("neuroscope.cli.run_server") +def test_main_server_default_args(mock_run_server, mock_argv): + """Test routing to server command with default arguments.""" + mock_run_server.return_value = 0 + mock_argv("server") + + assert main() == 0 + + mock_run_server.assert_called_once() + args = mock_run_server.call_args[0][0] + assert args.port == 8765 + assert args.host == "localhost" + assert args.no_browser is False + + +@patch("neuroscope.cli.run_demo") +def test_main_demo_default_args(mock_run_demo, mock_argv): + """Test routing to demo command with default arguments.""" + mock_run_demo.return_value = 0 + mock_argv("demo") + + assert main() == 0 + + mock_run_demo.assert_called_once() + args = mock_run_demo.call_args[0][0] + assert args.model == "mlp" + + +@patch("neuroscope.attach") +@patch("neuroscope.start_server") +@patch("neuroscope.detach") +@patch("neuroscope.stop_server") +@patch("time.sleep", side_effect=KeyboardInterrupt) +def test_run_demo_mlp(mock_sleep, mock_stop, mock_detach, mock_start, mock_attach, capsys): + """Test running the demo with mlp model.""" + args = MagicMock() + args.model = "mlp" + + assert run_demo(args) == 0 + + mock_attach.assert_called_once() + mock_start.assert_called_once() + mock_detach.assert_called_once() + mock_stop.assert_called_once() + + out, err = capsys.readouterr() + assert "Creating mlp model..." in out + assert "Attaching tracer..." in out + assert "Running forward pass..." in out + + +@patch("neuroscope.attach") +@patch("neuroscope.start_server") +@patch("neuroscope.detach") +@patch("neuroscope.stop_server") +@patch("time.sleep", side_effect=KeyboardInterrupt) +def test_run_demo_cnn(mock_sleep, mock_stop, mock_detach, mock_start, mock_attach, capsys): + """Test running the demo with cnn model.""" + args = MagicMock() + args.model = "cnn" + + assert run_demo(args) == 0 + + mock_attach.assert_called_once() + out, err = capsys.readouterr() + assert "Creating cnn model..." in out + + +@patch("neuroscope.attach") +@patch("neuroscope.start_server") +@patch("neuroscope.detach") +@patch("neuroscope.stop_server") +@patch("time.sleep", side_effect=KeyboardInterrupt) +def test_run_demo_transformer(mock_sleep, mock_stop, mock_detach, mock_start, mock_attach, capsys): + """Test running the demo with transformer model.""" + args = MagicMock() + args.model = "transformer" + + assert run_demo(args) == 0 + + mock_attach.assert_called_once() + out, err = capsys.readouterr() + assert "Creating transformer model..." in out + + +def test_run_demo_no_torch(capsys): + """Test running the demo when torch is not installed.""" + args = MagicMock() + args.model = "mlp" + + with patch.dict("sys.modules", {"torch": None}): + assert run_demo(args) == 1 + + out, err = capsys.readouterr() + assert "[ERROR] PyTorch is required for demo" in out