#!/usr/bin/env python3 # Copyright 2024 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import io import os.path import sys import unittest from unittest import mock ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, ROOT_DIR) import gclient_utils import presubmit_support import subprocess2 from testing_support import fake_repos class PresubmitSupportTest(unittest.TestCase): def test_environ(self): self.assertIsNone(os.environ.get('PRESUBMIT_FOO_ENV', None)) kv = {'PRESUBMIT_FOO_ENV': 'FOOBAR'} with presubmit_support.setup_environ(kv): self.assertEqual(os.environ.get('PRESUBMIT_FOO_ENV', None), 'FOOBAR') self.assertIsNone(os.environ.get('PRESUBMIT_FOO_ENV', None)) class ProvidedDiffChangeFakeRepo(fake_repos.FakeReposBase): NB_GIT_REPOS = 1 def populateGit(self): self._commit_git( 'repo_1', { 'to_be_modified': 'please change me\n', 'to_be_deleted': 'delete\nme\n', 'somewhere/else': 'not a top level file!\n', }) self._commit_git( 'repo_1', { 'to_be_modified': 'changed me!\n', 'to_be_deleted': None, 'somewhere/else': 'still not a top level file!\n', 'added': 'a new file\n', }) class ProvidedDiffChangeTest(fake_repos.FakeReposTestBase): FAKE_REPOS_CLASS = ProvidedDiffChangeFakeRepo def setUp(self): super(ProvidedDiffChangeTest, self).setUp() self.enabled = self.FAKE_REPOS.set_up_git() if not self.enabled: self.skipTest('git fake repos not available') self.repo = os.path.join(self.FAKE_REPOS.git_base, 'repo_1') diff = subprocess2.check_output(['git', 'show'], cwd=self.repo).decode('utf-8') self.change = self._create_change(diff) def _create_change(self, diff): with gclient_utils.temporary_file() as tmp: gclient_utils.FileWrite(tmp, diff) options = mock.Mock(root=self.repo, all_files=False, generate_diff=False, description='description', files=None, diff_file=tmp) change = presubmit_support._parse_change(None, options) assert isinstance(change, presubmit_support.ProvidedDiffChange) return change def _get_affected_file_from_name(self, change, name): for file in change._affected_files: if file.LocalPath() == os.path.normpath(name): return file self.fail(f'No file named {name}') def _test(self, name, old, new): affected_file = self._get_affected_file_from_name(self.change, name) self.assertEqual(affected_file.OldContents(), old) self.assertEqual(affected_file.NewContents(), new) def test_old_contents_of_added_file_returns_empty(self): self._test('added', [], ['a new file']) def test_old_contents_of_deleted_file_returns_whole_file(self): self._test('to_be_deleted', ['delete', 'me'], []) def test_old_contents_of_modified_file(self): self._test('to_be_modified', ['please change me'], ['changed me!']) def test_old_contents_of_file_with_nested_dirs(self): self._test('somewhere/else', ['not a top level file!'], ['still not a top level file!']) def test_unix_local_paths(self): if sys.platform == 'win32': self.assertIn(r'somewhere\else', self.change.LocalPaths()) else: self.assertIn('somewhere/else', self.change.LocalPaths()) self.assertIn('somewhere/else', self.change.UnixLocalPaths()) class TestGenerateDiff(fake_repos.FakeReposTestBase): """ Tests for --generate_diff. The option is used to generate diffs of given files against the upstream server as base. """ FAKE_REPOS_CLASS = ProvidedDiffChangeFakeRepo def setUp(self): super().setUp() self.repo = os.path.join(self.FAKE_REPOS.git_base, 'repo_1') self.parser = mock.Mock() self.parser.error.side_effect = SystemExit def test_with_diff_file(self): """Tests that only either --generate_diff or --diff_file is allowed.""" options = mock.Mock(root=self.repo, all_files=False, generate_diff=True, description='description', files=None, diff_file="patch.diff") with self.assertRaises(SystemExit): presubmit_support._parse_change(self.parser, options) self.parser.error.assert_called_once_with( ' cannot be specified when is set.', ) @mock.patch('presubmit_diff.create_diffs') def test_with_all_files(self, create_diffs): """Ensures --generate_diff is noop if --all_files is specified.""" options = mock.Mock(root=self.repo, all_files=True, generate_diff=True, description='description', files=None, source_controlled_only=False, diff_file=None) changes = presubmit_support._parse_change(self.parser, options) self.assertEqual(changes.AllFiles(), ['added', 'somewhere/else', 'to_be_modified']) create_diffs.assert_not_called() @mock.patch('presubmit_diff.fetch_content') def test_with_files(self, fetch_content): """Tests --generate_diff with files, which should call create_diffs().""" # fetch_content would return the old content of a given file. # In this test case, the mocked file is a newly added file. # hence, empty content. fetch_content.side_effect = [''] options = mock.Mock(root=self.repo, all_files=False, gerrit_url='https://chromium.googlesource.com', generate_diff=True, description='description', files=['added'], source_controlled_only=False, diff_file=None) change = presubmit_support._parse_change(self.parser, options) affected_files = change.AffectedFiles() self.assertEqual(len(affected_files), 1) self.assertEqual(affected_files[0].LocalPath(), 'added') class TestParseDiff(unittest.TestCase): """A suite of tests related to diff parsing and processing.""" def _test_diff_to_change_files(self, diff, expected): with gclient_utils.temporary_file() as tmp: gclient_utils.FileWrite(tmp, diff, mode='w+') content, change_files = presubmit_support._process_diff_file(tmp) self.assertCountEqual(content, diff) self.assertCountEqual(change_files, expected) def test_diff_to_change_files_raises_on_empty_diff_header(self): diff = """ diff --git a/foo b/foo """ with self.assertRaises(presubmit_support.PresubmitFailure): self._test_diff_to_change_files(diff=diff, expected=[]) def test_diff_to_change_files_simple_add(self): diff = """ diff --git a/foo b/foo new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/foo @@ -0,0 +1 @@ +add """ self._test_diff_to_change_files(diff=diff, expected=[('A', 'foo')]) def test_diff_to_change_files_simple_delete(self): diff = """ diff --git a/foo b/foo deleted file mode 100644 index f675c2a..0000000 --- a/foo +++ /dev/null @@ -1,1 +0,0 @@ -delete """ self._test_diff_to_change_files(diff=diff, expected=[('D', 'foo')]) def test_diff_to_change_files_simple_modification(self): diff = """ diff --git a/foo b/foo index d7ba659f..b7957f3 100644 --- a/foo +++ b/foo @@ -29,7 +29,7 @@ other random text - foo1 + foo2 other random text """ self._test_diff_to_change_files(diff=diff, expected=[('M', 'foo')]) def test_diff_to_change_files_multiple_changes(self): diff = """ diff --git a/foo b/foo index d7ba659f..b7957f3 100644 --- a/foo +++ b/foo @@ -29,7 +29,7 @@ other random text - foo1 + foo2 other random text diff --git a/bar b/bar new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/bar @@ -0,0 +1 @@ +add diff --git a/baz b/baz deleted file mode 100644 index f675c2a..0000000 --- a/baz +++ /dev/null @@ -1,1 +0,0 @@ -delete """ self._test_diff_to_change_files(diff=diff, expected=[('M', 'foo'), ('A', 'bar'), ('D', 'baz')]) def test_parse_unified_diff_with_valid_diff(self): diff = """ diff --git a/foo b/foo new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/foo @@ -0,0 +1 @@ +add """ res = presubmit_support._parse_unified_diff(diff) self.assertCountEqual( res, { 'foo': """ new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/foo @@ -0,0 +1 @@ +add """ }) def test_parse_unified_diff_with_valid_diff_noprefix(self): diff = """ diff --git foo foo new file mode 100644 index 0000000..9daeafb --- /dev/null +++ foo @@ -0,0 +1 @@ +add """ res = presubmit_support._parse_unified_diff(diff) self.assertCountEqual( res, { 'foo': """ new file mode 100644 index 0000000..9daeafb --- /dev/null +++ foo @@ -0,0 +1 @@ +add """ }) def test_parse_unified_diff_with_invalid_diff(self): diff = """ diff --git a/ffoo b/foo """ with self.assertRaises(presubmit_support.PresubmitFailure): presubmit_support._parse_unified_diff(diff) def test_diffs_to_change_files_with_empty_diff(self): res = presubmit_support._diffs_to_change_files({'file': ''}) self.assertEqual(res, [('M', 'file')]) class PresubmitResultLocationTest(unittest.TestCase): def test_invalid_missing_filepath(self): with self.assertRaisesRegex(ValueError, 'file path is required'): presubmit_support._PresubmitResultLocation(file_path='').validate() def test_invalid_abs_filepath_except_for_commit_msg(self): loc = presubmit_support._PresubmitResultLocation(file_path='/foo') with self.assertRaisesRegex(ValueError, 'file path must be relative path'): loc.validate() loc = presubmit_support._PresubmitResultLocation( file_path='/COMMIT_MSG') try: loc.validate() except ValueError: self.fail("validate should not fail for /COMMIT_MSG path") def test_invalid_end_line_without_start_line(self): loc = presubmit_support._PresubmitResultLocation(file_path='foo', end_line=5) with self.assertRaisesRegex(ValueError, 'end_line must be empty'): loc.validate() def test_invalid_start_col_without_start_line(self): loc = presubmit_support._PresubmitResultLocation(file_path='foo', start_col=5) with self.assertRaisesRegex(ValueError, 'start_col must be empty'): loc.validate() def test_invalid_end_col_without_start_line(self): loc = presubmit_support._PresubmitResultLocation(file_path='foo', end_col=5) with self.assertRaisesRegex(ValueError, 'end_col must be empty'): loc.validate() def test_invalid_negative_start_line(self): loc = presubmit_support._PresubmitResultLocation(file_path='foo', start_line=-1) with self.assertRaisesRegex(ValueError, 'start_line MUST not be negative'): loc.validate() def test_invalid_non_positive_end_line(self): loc = presubmit_support._PresubmitResultLocation(file_path='foo', start_line=1, end_line=0) with self.assertRaisesRegex(ValueError, 'end_line must be positive'): loc.validate() def test_invalid_negative_start_col(self): loc = presubmit_support._PresubmitResultLocation(file_path='foo', start_line=1, end_line=1, start_col=-1) with self.assertRaisesRegex(ValueError, 'start_col MUST not be negative'): loc.validate() def test_invalid_negative_end_col(self): loc = presubmit_support._PresubmitResultLocation(file_path='foo', start_line=1, end_line=1, end_col=-1) with self.assertRaisesRegex(ValueError, 'end_col MUST not be negative'): loc.validate() def test_invalid_start_after_end_line(self): loc = presubmit_support._PresubmitResultLocation(file_path='foo', start_line=6, end_line=5) with self.assertRaisesRegex(ValueError, 'must not be after'): loc.validate() def test_invalid_start_after_end_col(self): loc = presubmit_support._PresubmitResultLocation(file_path='foo', start_line=5, start_col=11, end_line=5, end_col=10) with self.assertRaisesRegex(ValueError, 'must not be after'): loc.validate() class PresubmitResultTest(unittest.TestCase): def test_handle_message_only(self): result = presubmit_support._PresubmitResult('Simple message') out = io.StringIO() result.handle(out) self.assertEqual(out.getvalue(), 'Simple message\n') def test_handle_full_args(self): result = presubmit_support._PresubmitResult( 'This is a message', items=['item1', 'item2'], long_text='Long text here.', locations=[ presubmit_support._PresubmitResultLocation( file_path=presubmit_support._PresubmitResultLocation. COMMIT_MSG_PATH), presubmit_support._PresubmitResultLocation( file_path='file1', start_line=10, end_line=10, ), presubmit_support._PresubmitResultLocation( file_path='file2', start_line=11, end_line=15, ), presubmit_support._PresubmitResultLocation( file_path='file3', start_line=5, start_col=0, end_line=8, end_col=5, ) ]) out = io.StringIO() result.handle(out) expected = ('This is a message\n' ' item1\n' ' item2\n' 'Found in:\n' ' - Commit Message\n' ' - file1 [Ln 10]\n' ' - file2 [Ln 11 - 15]\n' ' - file3 [Ln 5, Col 0 - Ln 8, Col 5]\n' '\n***************\n' 'Long text here.\n' '***************\n') self.assertEqual(out.getvalue(), expected) def test_json_format(self): loc1 = presubmit_support._PresubmitResultLocation(file_path='file1', start_line=1, end_line=1) loc2 = presubmit_support._PresubmitResultLocation(file_path='file2', start_line=5, start_col=2, end_line=6, end_col=10) result = presubmit_support._PresubmitResult('This is a message', items=['item1', 'item2'], long_text='Long text here.', locations=[loc1, loc2]) expected = { 'message': 'This is a message', 'items': ['item1', 'item2'], 'locations': [ { 'file_path': 'file1', 'start_line': 1, 'start_col': 0, 'end_line': 1, 'end_col': 0 }, { 'file_path': 'file2', 'start_line': 5, 'start_col': 2, 'end_line': 6, 'end_col': 10 }, ], 'long_text': 'Long text here.', 'fatal': False, } self.assertEqual(result.json_format(), expected) if __name__ == "__main__": unittest.main()