| #!/usr/bin/env vpython3 |
| # Copyright 2024 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Tests for recipe.py""" |
| |
| import json |
| import os |
| import pathlib |
| import shutil |
| import tempfile |
| import unittest |
| from unittest import mock |
| |
| import recipe |
| |
| |
| class LegacyRunnerTests(unittest.TestCase): |
| |
| class AsyncMock(mock.MagicMock): |
| |
| def __init__(self, *args, **kwargs): |
| super().__init__(*args, **kwargs) |
| self.returncode = 0 |
| |
| async def wait(self): |
| pass |
| |
| def setUp(self): |
| self.tmp_dir = pathlib.Path(tempfile.mkdtemp()) |
| self.tmp_dir.joinpath('recipes').touch() |
| self.build_dir = self.tmp_dir.joinpath('some', 'build', 'dir') |
| self.addCleanup(shutil.rmtree, self.tmp_dir) |
| |
| self.subp_mock = self.AsyncMock() |
| |
| patch_tempdir = mock.patch('tempfile.TemporaryDirectory') |
| self.mock_tempdir = patch_tempdir.start() |
| self.mock_tempdir.return_value.__enter__.return_value = self.tmp_dir |
| self.addCleanup(patch_tempdir.stop) |
| |
| patch_input = mock.patch('builtins.input') |
| self.mock_input = patch_input.start() |
| self.addCleanup(patch_input.stop) |
| |
| patch_terminal_size = mock.patch('os.get_terminal_size') |
| mock_terminal_size = patch_terminal_size.start() |
| mock_terminal_size.return_value = (128, 1) |
| self.addCleanup(patch_terminal_size.stop) |
| |
| def testProps(self): |
| runner = recipe.LegacyRunner(self.tmp_dir, {}, 'some-project', |
| 'some-bucket', 'some-builder', [], False, |
| False, False, self.build_dir) |
| self.assertEqual( |
| runner._input_props['$recipe_engine/buildbucket']['build']['builder'] |
| ['builder'], 'some-builder') |
| |
| def testRun(self): |
| runner = recipe.LegacyRunner(self.tmp_dir, {}, 'some-project', |
| 'some-bucket', 'some-builder', [], False, |
| False, False, self.build_dir) |
| self.subp_mock.returncode = 123 |
| with mock.patch('asyncio.create_subprocess_exec', |
| return_value=self.subp_mock): |
| exit_code, _ = runner.run_recipe() |
| self.assertEqual(exit_code, 123) |
| |
| def testJson(self): |
| runner = recipe.LegacyRunner(self.tmp_dir, {}, 'some-project', |
| 'some-bucket', 'some-builder', [], False, |
| False, False, self.build_dir) |
| with mock.patch('asyncio.create_subprocess_exec', |
| return_value=self.subp_mock): |
| # Passing run. |
| self.subp_mock.returncode = 0 |
| with open(self.tmp_dir.joinpath('out.json'), 'w') as f: |
| json.dump({}, f) |
| _, error_msg = runner.run_recipe() |
| self.assertIsNone(error_msg) |
| |
| # Missing json file |
| self.subp_mock.returncode = 1 |
| rc, error_msg = runner.run_recipe() |
| self.assertEqual(rc, 1) |
| self.assertIsNone(error_msg) |
| |
| # Broken json |
| with open(self.tmp_dir.joinpath('out.json'), 'w') as f: |
| f.write('this-is-not-json') |
| rc, error_msg = runner.run_recipe() |
| self.assertEqual(rc, 1) |
| self.assertIsNone(error_msg) |
| |
| # Actual json. It'll get printed to the terminal, so all that run_recipe() |
| # returns is a generic failure message. |
| with open(self.tmp_dir.joinpath('out.json'), 'w') as f: |
| json.dump({'failure': {'humanReason': 'it exploded'}}, f) |
| rc, error_msg = runner.run_recipe() |
| self.assertEqual(rc, 1) |
| self.assertIsNone(error_msg) |
| |
| def testReruns(self): |
| runner = recipe.LegacyRunner(self.tmp_dir, {}, 'some-project', |
| 'some-bucket', 'some-builder', [], False, |
| False, False, self.build_dir) |
| with mock.patch('asyncio.create_subprocess_exec', |
| return_value=self.subp_mock): |
| # Input "n" to the first re-run prompt. |
| self.mock_input.return_value = 'n' |
| with open(self.tmp_dir.joinpath('rerun_props.json'), 'w') as f: |
| json.dump([['y', {'some-new-prop': 'some-val'}], ['n', {}]], f) |
| _, error_msg = runner.run_recipe() |
| self.assertEqual(error_msg, 'User-aborted due to warning') |
| |
| # Input "y" to too many re-runs. |
| self.mock_input.return_value = 'y' |
| with open(self.tmp_dir.joinpath('rerun_props.json'), 'w') as f: |
| json.dump([['y', {'some-new-prop': 'some-val'}], ['n', {}]], f) |
| _, error_msg = runner.run_recipe() |
| self.assertEqual(error_msg, 'Exceeded too many recipe re-runs') |
| |
| # Re-running once and succeeding. Need to manage two different tmp dirs, |
| # one for each recipe invocations. |
| first_tmp_dir = self.tmp_dir |
| second_tmp_dir = pathlib.Path(tempfile.mkdtemp()) |
| self.addCleanup(shutil.rmtree, second_tmp_dir) |
| self.mock_input.return_value = 'y' |
| with open(first_tmp_dir.joinpath('rerun_props.json'), 'w') as f: |
| json.dump([['y', {'some-new-prop': 'some-val'}], ['n', {}]], f) |
| self.mock_tempdir.side_effect = [first_tmp_dir, second_tmp_dir] |
| _, error_msg = runner.run_recipe() |
| self.assertIsNone(error_msg) |
| |
| |
| def testRerunsWithForce(self): |
| runner = recipe.LegacyRunner(self.tmp_dir, {}, 'some-project', |
| 'some-bucket', 'some-builder', [], False, |
| False, True, self.build_dir) |
| with mock.patch('asyncio.create_subprocess_exec', |
| return_value=self.subp_mock): |
| # Re-running once and succeeding. Need to manage two different tmp dirs, |
| # one for each recipe invocations. input() shouldn't be called since we |
| # pass --force. |
| first_tmp_dir = self.tmp_dir |
| second_tmp_dir = pathlib.Path(tempfile.mkdtemp()) |
| self.addCleanup(shutil.rmtree, second_tmp_dir) |
| with open(first_tmp_dir.joinpath('rerun_props.json'), 'w') as f: |
| json.dump([['y', {'some-new-prop': 'some-val'}], ['n', {}]], f) |
| self.mock_tempdir.side_effect = [first_tmp_dir, second_tmp_dir] |
| _, error_msg = runner.run_recipe() |
| self.assertIsNone(error_msg) |
| self.mock_input.assert_not_called() |
| |
| def testRerunsWithOverwrite(self): |
| runner = recipe.LegacyRunner(self.tmp_dir, {}, |
| 'some-project', |
| 'some-bucket', |
| 'some-builder', [], |
| False, |
| False, |
| False, |
| self.build_dir, |
| skip_coverage=True) |
| with mock.patch('asyncio.create_subprocess_exec', |
| return_value=self.subp_mock): |
| self.mock_input.return_value = 'n' |
| with open(self.tmp_dir.joinpath('rerun_props.json'), 'w') as f: |
| json.dump([['y', {'some-new-prop': 'some-val'}], ['n', {}]], f) |
| runner.run_recipe() |
| |
| # The first run of the recipe should have coverage-related fields off |
| # due to skip_coverage=True. |
| stdin_write = self.subp_mock.mock_calls[0] |
| input_props = json.loads(stdin_write.args[0]) |
| self.assertTrue(input_props['rerun_options']['bypass_branch_check']) |
| self.assertTrue(input_props['rerun_options']['skip_instrumentation']) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |