diff --git a/rsconnect/bundle.py b/rsconnect/bundle.py index 7969dc75..c6c87502 100644 --- a/rsconnect/bundle.py +++ b/rsconnect/bundle.py @@ -816,8 +816,8 @@ def make_notebook_html_bundle( cmd.append("--TagRemovePreprocessor.remove_input_tags=['hide_input']") try: output = check_output(cmd) - except subprocess.CalledProcessError: - raise + except subprocess.CalledProcessError as error: + raise RSConnectException(f"Rendering {filename} with nbconvert failed (see output above)") from error nb_name = basename(filename) filename = splitext(nb_name)[0] + ".html" diff --git a/tests/test_main.py b/tests/test_main.py index 94a59c2f..df63baec 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -4,6 +4,7 @@ import re import shutil import sys +import venv from os.path import join from types import SimpleNamespace from unittest import TestCase, mock @@ -352,6 +353,62 @@ def test_deploy_notebook_static_passes_interpreter_not_version(self): assert bundle_args[0] is make_notebook_html_bundle assert bundle_args[2] == sys.executable + @pytest.mark.parametrize( + ("contents", "diagnostic", "missing_nbconvert"), + [ + ("not json", "NotJSONError", False), + ("not json", "No module named nbconvert", True), + ( + json.dumps( + { + "cells": [ + { + "cell_type": "code", + "execution_count": None, + "metadata": {}, + "outputs": [], + "source": ["raise ValueError('boom')"], + } + ], + "metadata": {"kernelspec": {"name": "python3", "display_name": "Python 3"}}, + "nbformat": 4, + "nbformat_minor": 5, + } + ), + "ValueError: boom", + False, + ), + ], + ) + def test_deploy_static_notebook_render_failure(self, tmp_path, capfd, contents, diagnostic, missing_nbconvert): + notebook = tmp_path / "broken.ipynb" + notebook.write_text(contents) + interpreter = sys.executable + if missing_nbconvert: + env_dir = tmp_path / "empty-env" + venv.EnvBuilder(with_pip=False, symlinks=os.name != "nt").create(env_dir) + interpreter = str(env_dir / ("Scripts/python.exe" if os.name == "nt" else "bin/python")) + fake_env = SimpleNamespace(python="3.12.1", python_interpreter=interpreter) + + with mock.patch("rsconnect.main.Environment.create_python_environment", return_value=fake_env), mock.patch( + "rsconnect.main.RSConnectExecutor" + ) as mock_executor: + mock_executor.return_value.make_bundle.side_effect = lambda func, *args: func(*args) + args = apply_common_args( + ["deploy", "notebook", str(notebook), "--static", "--no-verify"], + server="http://fake_server", + key="FAKE_API_KEY", + ) + result = CliRunner().invoke(cli, args) + + stderr = capfd.readouterr().err + assert result.exit_code == 1 + assert diagnostic in re.sub(r"\x1b\[[0-9;]*m", "", stderr) + assert f"Error: Rendering {notebook} with nbconvert failed (see output above)" in result.output + assert "Traceback (most recent call last)" not in result.output + assert "Internal error" not in result.output + stderr + assert "subprocess.CalledProcessError" not in result.output + stderr + @httpretty.activate(verbose=True, allow_net_connect=False) def test_deploy_verify_before_activate(self, caplog): # Without --draft or --no-verify, the default flow deploys the bundle as a