main
py 216 lines 7.36 KB
Raw
1 # Copyright 2026 Google LLC
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 import json
16 import nbformat
17 import os
18 import pytest
19 import shutil
20 import sys
21 import tempfile
22 import unittest
23 from unittest.mock import patch, MagicMock, ANY
24 from colab_cli.cli import main
25
26
27 class TestIpynbExec(unittest.TestCase):
28 def setUp(self):
29 self.temp_dir = tempfile.mkdtemp()
30 self.nb_path = os.path.join(self.temp_dir, "test.ipynb")
31
32 # Create a simple v4 notebook
33 nb = {
34 "cells": [
35 {
36 "cell_type": "code",
37 "execution_count": None,
38 "id": "cell1",
39 "metadata": {},
40 "outputs": [],
41 "source": "print('cell 1')",
42 },
43 {
44 "cell_type": "markdown",
45 "id": "cell2",
46 "metadata": {},
47 "source": "# Markdown cell",
48 },
49 {
50 "cell_type": "code",
51 "execution_count": None,
52 "id": "cell3",
53 "metadata": {},
54 "outputs": [],
55 "source": "print('cell 2')",
56 },
57 ],
58 "metadata": {},
59 "nbformat": 4,
60 "nbformat_minor": 5,
61 }
62 with open(self.nb_path, "w", encoding="utf-8") as f:
63 json.dump(nb, f)
64
65 def tearDown(self):
66 shutil.rmtree(self.temp_dir)
67
68 @patch("colab_cli.commands.execution.ColabRuntime")
69 @patch("colab_cli.state.StateStore")
70 def test_exec_ipynb(
71 self,
72 mock_store_class,
73 mock_runtime_class,
74 ):
75 with patch.object(
76 sys, "argv", ["colab", "exec", "-s", "test-s", "-f", self.nb_path]
77 ):
78 mock_store = mock_store_class.return_value
79 mock_store.get.return_value = MagicMock(
80 name="test-s", url="http://url", token="token"
81 )
82
83 mock_runtime = mock_runtime_class.return_value
84 mock_runtime.execute_code.side_effect = [
85 [], # os.makedirs and os.chdir setup
86 [{"text": "cell 1\n"}],
87 [{"text": "cell 2\n"}],
88 ]
89
90 with patch("builtins.print"), pytest.raises(SystemExit) as error:
91 main()
92
93 assert error.value.code == 0
94
95 # Verify both code cells were executed (plus the setup cell)
96 self.assertEqual(mock_runtime.execute_code.call_count, 3)
97 self.assertIn(
98 "os.chdir", mock_runtime.execute_code.call_args_list[0].args[0]
99 )
100 mock_runtime.execute_code.assert_any_call(
101 "print('cell 1')", output_hook=ANY, timeout=30.0
102 )
103 mock_runtime.execute_code.assert_any_call(
104 "print('cell 2')", output_hook=ANY, timeout=30.0
105 )
106
107 @patch("colab_cli.commands.execution.ColabRuntime")
108 @patch("colab_cli.state.StateStore")
109 @patch("colab_cli.commands.execution.typer.echo")
110 def test_exec_ipynb_output_format(
111 self,
112 mock_echo,
113 mock_store_class,
114 mock_runtime_class,
115 ):
116 nb_path = os.path.join(self.temp_dir, "test_format.ipynb")
117 nb = {
118 "cells": [
119 {
120 "cell_type": "code",
121 "execution_count": None,
122 "id": "my-cell-id-123",
123 "metadata": {},
124 "outputs": [],
125 "source": "# @title My Special Cell\nprint('hello')",
126 },
127 {
128 "cell_type": "code",
129 "execution_count": None,
130 "id": "fallback-id-456",
131 "metadata": {},
132 "outputs": [],
133 "source": "print('world')",
134 },
135 ],
136 "metadata": {},
137 "nbformat": 4,
138 "nbformat_minor": 5,
139 }
140 with open(nb_path, "w", encoding="utf-8") as f:
141 json.dump(nb, f)
142
143 with patch.object(
144 sys, "argv", ["colab", "exec", "-s", "test-s", "-f", nb_path]
145 ):
146 mock_store = mock_store_class.return_value
147 mock_store.get.return_value = MagicMock(
148 name="test-s", url="http://url", token="token"
149 )
150
151 mock_runtime = mock_runtime_class.return_value
152 mock_runtime.execute_code.side_effect = [
153 [], # setup
154 [{"text": "hello\n"}],
155 [{"text": "world\n"}],
156 ]
157
158 with patch("builtins.print"), pytest.raises(SystemExit) as error:
159 main()
160
161 assert error.value.code == 0
162
163 mock_echo.assert_any_call("[colab] Executing cell 1/2 - My Special Cell...")
164 mock_echo.assert_any_call("[colab] Executing cell 2/2 - fallback-id-456...")
165
166 @patch("colab_cli.commands.execution.ColabRuntime")
167 @patch("colab_cli.state.StateStore")
168 @patch("colab_cli.commands.execution.typer.echo")
169 def test_exec_ipynb_creates_output_file(
170 self,
171 mock_echo,
172 mock_store_class,
173 mock_runtime_class,
174 ):
175 with patch.object(
176 sys, "argv", ["colab", "exec", "-s", "test-s", "-f", self.nb_path]
177 ):
178 mock_store = mock_store_class.return_value
179 mock_store.get.return_value = MagicMock(
180 name="test-s", url="http://url", token="token"
181 )
182
183 mock_runtime = mock_runtime_class.return_value
184 mock_runtime.execute_code.side_effect = [
185 [],
186 [{"output_type": "stream", "name": "stdout", "text": "cell 1\n"}],
187 [{"output_type": "stream", "name": "stdout", "text": "cell 2\n"}],
188 ]
189
190 with patch("builtins.print"), pytest.raises(SystemExit) as error:
191 main()
192
193 assert error.value.code == 0
194
195 output_nb_path = self.nb_path.replace(".ipynb", "_output.ipynb")
196 self.assertTrue(os.path.exists(output_nb_path))
197 with open(output_nb_path, "r", encoding="utf-8") as f:
198 output_nb = nbformat.read(f, as_version=4)
199
200 self.assertEqual(len(output_nb.cells), 3)
201 # cell 1 outputs
202 self.assertEqual(len(output_nb.cells[0].outputs), 1)
203 self.assertEqual(output_nb.cells[0].outputs[0].text, "cell 1\n")
204 # cell 2 is markdown
205 self.assertEqual(output_nb.cells[1].cell_type, "markdown")
206 self.assertFalse(
207 hasattr(output_nb.cells[1], "outputs")
208 and len(output_nb.cells[1].outputs) > 0
209 )
210 # cell 3 outputs
211 self.assertEqual(len(output_nb.cells[2].outputs), 1)
212 self.assertEqual(output_nb.cells[2].outputs[0].text, "cell 2\n")
213
214
215 if __name__ == "__main__":
216 unittest.main()