Skip to content

Commit 6b10da3

Browse files
authored
[CLI] Migrate bootstrap (#5059)
### Changes This PR adds the new `casp bootstrap` command to the `casp` CLI. Which acts as a convenient wrapper for the root `butler.py bootstrap` script. ### How it works: - `casp bootstrap` Install all required dependencies from Clusterfuzz ### Changes in butler Regarding the refactor/improvement in `butler.py`, here are some screenshots comparing running `python butler.py bootstrap` in this branch and master. They show that the behavior is the same. <img width="1350" height="295" alt="image" src="https://github.com/user-attachments/assets/f608d300-5e05-48c9-9092-01687dfb76fa" /> <img width="1349" height="295" alt="image" src="https://github.com/user-attachments/assets/8e2b55cb-66a5-41ac-b043-05034a17d163" /> <img width="1349" height="295" alt="image" src="https://github.com/user-attachments/assets/16dcecab-57fd-481c-b6ed-a893bb576b50" /> <img width="1349" height="295" alt="image" src="https://github.com/user-attachments/assets/8d22a4ec-e23d-4b38-bfaa-be48e5c995db" /> ### Demo prints Here are some screenshots of `casp bootstrap` in action: <img width="1349" height="295" alt="image" src="https://github.com/user-attachments/assets/95516c06-28bb-4765-9689-49e856e5ce10" /> <img width="1350" height="295" alt="image" src="https://github.com/user-attachments/assets/7bf8e128-4582-4c5d-be48-a72bdda68210" />
1 parent 70e387b commit 6b10da3

File tree

3 files changed

+105
-10
lines changed

3 files changed

+105
-10
lines changed

butler.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ def _setup_args_for_remote(parser):
9898
subparsers.add_parser('reboot', help='Reboot with `sudo reboot`.')
9999

100100

101+
def _add_bootstrap_subparser(toplevel_subparsers):
102+
"""Adds a parser for the `bootstrap` command."""
103+
toplevel_subparsers.add_parser(
104+
'bootstrap',
105+
help=('Install all required dependencies for running an appengine, a bot,'
106+
'and a mapreduce locally.'))
107+
108+
101109
def _add_py_unittest_subparser(toplevel_subparsers):
102110
"""Adds a parser for the `py_unittest` command."""
103111
parser_py_unittest = toplevel_subparsers.add_parser(
@@ -317,11 +325,6 @@ def main():
317325
help='Force logs to be local-only.')
318326
subparsers = parser.add_subparsers(dest='command')
319327

320-
subparsers.add_parser(
321-
'bootstrap',
322-
help=('Install all required dependencies for running an appengine, a bot,'
323-
'and a mapreduce locally.'))
324-
325328
parser_js_unittest = subparsers.add_parser(
326329
'js_unittest', help='Run Javascript unit tests.')
327330
parser_js_unittest.add_argument(
@@ -454,6 +457,7 @@ def main():
454457
default='us-central',
455458
help='Location for App Engine.')
456459

460+
_add_bootstrap_subparser(subparsers)
457461
_add_py_unittest_subparser(subparsers)
458462
_add_lint_subparser(subparsers)
459463
_add_format_subparser(subparsers)

cli/casp/src/casp/commands/bootstrap.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,33 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
"""Bootstrap command."""
14+
"""Install all required dependencies from Clusterfuzz"""
1515

16+
import subprocess
17+
import sys
18+
19+
from casp.utils import local_butler
1620
import click
1721

1822

1923
@click.command(
2024
name='bootstrap',
2125
help=('Install all required dependencies for running an appengine, a bot,'
2226
'and a mapreduce locally.'))
23-
def cli():
24-
"""Install all required dependencies for running an appengine, a bot,
25-
and a mapreduce locally."""
26-
click.echo('To be implemented...')
27+
def cli() -> None:
28+
"""Performs the installation of all required dependencies."""
29+
30+
try:
31+
command = local_butler.build_command('bootstrap', None)
32+
except FileNotFoundError:
33+
click.echo('butler.py not found in this directory.', err=True)
34+
sys.exit(1)
35+
36+
try:
37+
subprocess.run(command, check=True)
38+
except FileNotFoundError:
39+
click.echo('python not found in PATH.', err=True)
40+
sys.exit(1)
41+
except subprocess.CalledProcessError as e:
42+
click.echo(f'Error running butler.py bootstrap: {e}', err=True)
43+
sys.exit(1)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Copyright 2025 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+
"""Tests for the bootstrap command.
15+
16+
For running all the tests, use (from the root of the project):
17+
python -m unittest discover -s cli/casp/src/casp/tests -p bootstrap_test.py -v
18+
"""
19+
20+
import subprocess
21+
import unittest
22+
from unittest.mock import patch
23+
24+
from casp.commands import bootstrap
25+
from click.testing import CliRunner
26+
27+
28+
class BootstrapCliTest(unittest.TestCase):
29+
"""Tests for the bootstrap command."""
30+
31+
def setUp(self):
32+
self.runner = CliRunner()
33+
self.mock_local_butler = self.enterContext(
34+
patch('casp.commands.bootstrap.local_butler', autospec=True))
35+
self.mock_subprocess_run = self.enterContext(
36+
patch('subprocess.run', autospec=True))
37+
38+
def test_bootstrap_success(self):
39+
"""Tests successful execution of `casp bootstrap`."""
40+
self.mock_local_butler.build_command.return_value = ['cmd']
41+
result = self.runner.invoke(bootstrap.cli)
42+
self.assertEqual(0, result.exit_code, msg=result.output)
43+
self.mock_local_butler.build_command.assert_called_once_with(
44+
'bootstrap', None)
45+
self.mock_subprocess_run.assert_called_once_with(['cmd'], check=True)
46+
47+
def test_butler_not_found(self):
48+
"""Tests when `butler.py` is not found."""
49+
self.mock_local_butler.build_command.side_effect = FileNotFoundError
50+
result = self.runner.invoke(bootstrap.cli)
51+
self.assertNotEqual(0, result.exit_code)
52+
self.assertIn('butler.py not found', result.output)
53+
self.mock_subprocess_run.assert_not_called()
54+
55+
def test_subprocess_run_fails(self):
56+
"""Tests when `subprocess.run` fails."""
57+
self.mock_local_butler.build_command.return_value = ['cmd']
58+
self.mock_subprocess_run.side_effect = subprocess.CalledProcessError(
59+
1, 'cmd')
60+
result = self.runner.invoke(bootstrap.cli)
61+
self.assertNotEqual(0, result.exit_code)
62+
self.assertIn('Error running butler.py bootstrap', result.output)
63+
64+
def test_python_not_found(self):
65+
"""Tests when `python` command is not found."""
66+
self.mock_local_butler.build_command.return_value = ['cmd']
67+
self.mock_subprocess_run.side_effect = FileNotFoundError
68+
result = self.runner.invoke(bootstrap.cli)
69+
self.assertNotEqual(0, result.exit_code)
70+
self.assertIn('python not found in PATH', result.output)
71+
72+
73+
if __name__ == '__main__':
74+
unittest.main()

0 commit comments

Comments
 (0)