| #!/usr/bin/env vpython3 | |
| # Copyright 2026 The Chromium Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """Unit tests for get_task_digest.py.""" | |
| import contextlib | |
| import io | |
| import json | |
| import sys | |
| import unittest | |
| from unittest import mock | |
| import get_task_digest as gtd | |
| class GetTaskDigestTest(unittest.TestCase): | |
| """Unit tests for get_task_digest.py.""" | |
| def setUp(self) -> None: | |
| """Sets up subprocess run mocks.""" | |
| self.run_patcher = mock.patch('subprocess.run') | |
| self.mock_run = self.run_patcher.start() | |
| self.addCleanup(self.run_patcher.stop) | |
| def test_input_properties_success(self) -> None: | |
| """Verifies hash extraction from input swarm_hashes.""" | |
| data = { | |
| 'input': {'properties': {'swarm_hashes': {'net_unittests': 'abc123/810'}}} | |
| } | |
| mock_proc = mock.MagicMock(returncode=0, stdout=json.dumps(data)) | |
| self.mock_run.return_value = mock_proc | |
| res = gtd.get_digest_from_properties('123', 'net_unittests') | |
| self.assertEqual(res, 'abc123') | |
| def test_output_properties_success(self) -> None: | |
| """Verifies fallback to output properties when input misses.""" | |
| data_in = {'input': {'properties': {'swarm_hashes': {}}}} | |
| data_out = {'output': {'properties': {'net_unittests': 'def456/999'}}} | |
| proc_in = mock.MagicMock(returncode=0, stdout=json.dumps(data_in)) | |
| proc_out = mock.MagicMock(returncode=0, stdout=json.dumps(data_out)) | |
| self.mock_run.side_effect = [proc_in, proc_out] | |
| res = gtd.get_digest_from_properties('123', 'net_unittests') | |
| self.assertEqual(res, 'def456') | |
| def test_output_swarm_hashes(self) -> None: | |
| """Verifies extraction from output swarm_hashes.""" | |
| data_in = {'input': {'properties': {}}} | |
| data_out = { | |
| 'output': { | |
| 'properties': {'swarm_hashes': {'net_unittests': 'hash111/50'}} | |
| } | |
| } | |
| proc_in = mock.MagicMock(returncode=0, stdout=json.dumps(data_in)) | |
| proc_out = mock.MagicMock(returncode=0, stdout=json.dumps(data_out)) | |
| self.mock_run.side_effect = [proc_in, proc_out] | |
| res = gtd.get_digest_from_properties('123', 'net_unittests') | |
| self.assertEqual(res, 'hash111') | |
| def test_output_swarming_trigger_properties(self) -> None: | |
| """Verifies extraction from swarming_trigger_properties.""" | |
| data_in = {'input': {'properties': {}}} | |
| data_out = { | |
| 'output': { | |
| 'properties': { | |
| 'swarming_trigger_properties': { | |
| 'swarm_hashes': {'net_unittests': 'hash789/100'} | |
| } | |
| } | |
| } | |
| } | |
| proc_in = mock.MagicMock(returncode=0, stdout=json.dumps(data_in)) | |
| proc_out = mock.MagicMock(returncode=0, stdout=json.dumps(data_out)) | |
| self.mock_run.side_effect = [proc_in, proc_out] | |
| res = gtd.get_digest_from_properties('123', 'net_unittests') | |
| self.assertEqual(res, 'hash789') | |
| def test_orchestrator_child_build(self) -> None: | |
| """Verifies fallback to steps scan for child build.""" | |
| data_in1 = {'input': {'properties': {}}} | |
| data_out1 = {'output': {'properties': {}}} | |
| data_steps = { | |
| 'steps': [{'summaryMarkdown': '* [8000000000000000002](url)'}] | |
| } | |
| data_in2 = {'input': {'properties': {}}} | |
| data_out2 = {'output': {'properties': {'net_unittests': 'childhash/999'}}} | |
| proc_in1 = mock.MagicMock(returncode=0, stdout=json.dumps(data_in1)) | |
| proc_out1 = mock.MagicMock(returncode=0, stdout=json.dumps(data_out1)) | |
| proc_steps = mock.MagicMock(returncode=0, stdout=json.dumps(data_steps)) | |
| proc_in2 = mock.MagicMock(returncode=0, stdout=json.dumps(data_in2)) | |
| proc_out2 = mock.MagicMock(returncode=0, stdout=json.dumps(data_out2)) | |
| self.mock_run.side_effect = [ | |
| proc_in1, | |
| proc_out1, | |
| proc_steps, | |
| proc_in2, | |
| proc_out2, | |
| ] | |
| res = gtd.get_digest_from_properties('orch', 'net_unittests') | |
| self.assertEqual(res, 'childhash') | |
| def test_bad_json_handling(self) -> None: | |
| """Verifies JSONDecodeError handling across bb calls.""" | |
| proc_bad = mock.MagicMock(returncode=0, stdout='invalid json') | |
| self.mock_run.side_effect = [proc_bad, proc_bad, proc_bad] | |
| res = gtd.get_digest_from_properties('123', 'net_unittests') | |
| self.assertIsNone(res) | |
| def test_bb_cli_failure(self) -> None: | |
| """Verifies handling when bb CLI returns non-zero code.""" | |
| proc_fail = mock.MagicMock(returncode=1, stdout='') | |
| self.mock_run.side_effect = [proc_fail, proc_fail, proc_fail] | |
| res = gtd.get_digest_from_properties('123', 'net_unittests') | |
| self.assertIsNone(res) | |
| @mock.patch('get_task_digest.get_digest_from_properties', return_value=None) | |
| def test_main_not_found(self, _) -> None: | |
| """Verifies sys.exit(1) when digest is missing.""" | |
| err_buf = io.StringIO() | |
| with mock.patch.object( | |
| sys, 'argv', ['gtd.py', '--build', '1', '--step', 's'] | |
| ): | |
| with contextlib.redirect_stderr(err_buf): | |
| with self.assertRaises(SystemExit) as cm: | |
| gtd.main() | |
| self.assertEqual(cm.exception.code, 1) | |
| self.assertIn('not found', err_buf.getvalue()) | |
| @mock.patch( | |
| 'get_task_digest.get_digest_from_properties', return_value='hash789' | |
| ) | |
| def test_main_success(self, _) -> None: | |
| """Verifies digest is printed to stdout.""" | |
| out_buf = io.StringIO() | |
| with mock.patch.object( | |
| sys, 'argv', ['gtd.py', '--build', '1', '--step', 's'] | |
| ): | |
| with contextlib.redirect_stdout(out_buf): | |
| gtd.main() | |
| self.assertIn('hash789', out_buf.getvalue()) | |
| if __name__ == '__main__': | |
| unittest.main() |