#!/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 cl_format.py and git cl format."""

import io
import json
import logging
import optparse
import os
import shutil
import sys
import tempfile
import threading
import unittest
from unittest import mock

# Add parent directory to sys.path
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _ROOT not in sys.path:
    sys.path.insert(0, _ROOT)

import cl_format  # noqa: E402
import gclient_utils  # noqa: E402
import git_cl  # noqa: E402


test_format_input_diff = """
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -6,11 +6,8 @@
 The project's web site is https://www.chromium.org.

 To check out the source code locally, don't use `git clone`! Instead,
-follow [the instructions on how to get the code](docs/get_the_code.md).
+follow [the instructionource is rooted in [docs/README.md](docs/README.md).

-Documentation in the source is rooted in [docs/README.md](docs/README.md).
-
-Learn how to [Get Around the Chromium Source Code Directory
 Structure](https://www.chromium.org/developers/how-tos/getting-around-the-chrome-source-code).

 For historical reasons, there are some small top level directories. Now the
diff --git a/net/base/net_error_details.h b/net/base/net_error_details.h
--- a/net/base/net_error_details.h
+++ b/net/base/net_error_details.h
@@ -13,13 +13,12 @@
 namespace net {

 // A record of net errors with granular error specification generated by
-// net stack.
 struct NET_EXPORT NetErrorDetails {
   NetErrorDetails()
       : quic_broken(false), quic_connection_error(quic::QUIC_NO_ERROR) {}

   NetErrorDetails(bool quic_broken, quic::QuicErrorCode quic_connection_error)
-      : quic_broken(quic_broken),
+           : quic_broken(quic_broken),
         quic_connection_error(quic_connection_error) {}

   // True if all QUIC alternative services are marked broken for the origin.
diff --git a/net/base/net_error_list.h b/net/base/net_error_list.h
--- a/net/base/net_error_list.h
+++ b/net/base/net_error_list.h
@@ -1063,7 +1063,6 @@
 // were no outstanding references (no one is waiting to read) to keep the
 // blob alive.
 NET_ERROR(BLOB_DEREFERENCED_WHILE_BUILDING, -904)
-
 // A blob that we referenced during construction is broken, or a browser-side
 // builder tries to build a blob with a blob reference that isn't finished
 // constructing.
diff --git a/testing/xvfb_unittest.py b/testing/xvfb_unittest.py
--- a/testing/xvfb_unittest.py
+++ b/testing/xvfb_unittest.py
@@ -18,7 +18,7 @@

 TEST_FILE = __file__.replace('.pyc', '.py')
-XVFB = TEST_FILE.replace('_unittest', '')
+XVFB =      TEST_FILE.replace('_unittest', '')
 XVFB_TEST_SCRIPT = TEST_FILE.replace('_unittest', '_test_script')

"""

test_format_input_diff_windows = "\r\n".join(
    [
        'diff --git "a/C:\\\\path\\\\to\\\\src\\\\file.cc" "b/C:\\\\path\\\\to\\\\dst\\\\file.cc"',
        "index ce013625..dd7e1c6f 100644",
        '--- "a/C:\\\\path\\\\to\\\\src\\\\file.cc',
        '+++ "b/C:\\\\path\\\\to\\\\dst\\\\file.cc',
        "@@ -1 +1 @@",
        "-random",
        "+content",
    ]
)


class CMDFormatTestCase(unittest.TestCase):
    def setUp(self):
        super(CMDFormatTestCase, self).setUp()
        self._top_dir = tempfile.mkdtemp()
        mock.patch("cl_format.RunCommand", return_value="").start()
        mock.patch("cl_format.RunGit", return_value="dummy_commit").start()
        mock.patch("clang_format.FindClangFormatToolInChromiumTree").start()
        mock.patch("clang_format.FindClangFormatScriptInChromiumTree").start()
        mock_settings = mock.patch("cl_format.settings").start()
        mock_settings.GetRelativeRoot.return_value = ""
        mock_settings.GetRoot.return_value = self._top_dir
        mock_settings.GetFormatJs.return_value = False
        mock_settings.GetFormatFullByDefault.return_value = False
        self.addCleanup(mock.patch.stopall)

    def tearDown(self):
        shutil.rmtree(self._top_dir)
        super(CMDFormatTestCase, self).tearDown()

    def test_compute_format_diff_line_ranges_starts_at_line_1(self):
        diff = (
            "diff --git a/foo.py b/foo.py\n"
            "index 0000000..1111111\n"
            "--- /dev/null\n"
            "+++ b/foo.py\n"
            "@@ -0,0 +1,5 @@\n"
            "+# new file\n"
            "+x = 1\n"
            "+y = 2\n"
            "+z = 3\n"
            "+w = 4\n"
        )
        res = cl_format._ComputeFormatDiffLineRanges(
            ["foo.py"], {"foo.py": diff}
        )
        self.assertEqual({"foo.py": [(1, 5)]}, res)

    def test_compute_format_diff_line_ranges_with_at_at_in_header(self):
        diff = (
            "diff --git a/foo.py b/foo.py\n"
            "index 0000000..1111111 100644\n"
            "--- a/foo.py\n"
            "+++ b/foo.py\n"
            '@@ -17 +17 @@ _REGEX = re.compile(r"^@@ \\-(\\d+),?(\\d+)? \\+(\\d+),?(\\d+)? @@")\n'
            "-x = 1\n"
            "+x = 2\n"
        )
        res = cl_format._ComputeFormatDiffLineRanges(
            ["foo.py"], {"foo.py": diff}
        )
        self.assertEqual({"foo.py": [(17, 17)]}, res)

    def _make_temp_file(self, fname, contents):
        dir_path = os.path.dirname(fname)
        if dir_path:
            os.makedirs(os.path.join(self._top_dir, dir_path), exist_ok=True)

        gclient_utils.FileWrite(
            os.path.join(self._top_dir, fname), ("\n".join(contents))
        )

    def _make_yapfignore(self, contents):
        self._make_temp_file(".yapfignore", contents)

    def _check_yapf_filtering(self, files, expected):
        self.assertEqual(
            expected,
            cl_format._FilterYapfIgnoredFiles(
                files, cl_format._GetYapfIgnorePatterns(self._top_dir)
            ),
        )

    def _make_markdown_config(self, path):
        self._make_temp_file(os.path.join(path, ".style.mdformat"), [])

    def _make_lit_template_formatter_config(self, path):
        self._make_temp_file(
            os.path.join(path, ".style.lit_template_formatter"), []
        )

    def testMarkdownFormat(self):
        self._make_markdown_config("agents")
        to_be_fixed_md = os.path.join(self._top_dir, "agents/to_be_fixed.md")
        remain_intact_md = os.path.join(self._top_dir, "other/remain_intact.md")
        self._make_temp_file(
            "agents/to_be_fixed.md", ["#  Hello", "", "world  "]
        )
        self._make_temp_file(
            "other/remain_intact.md", ["#  Hello", "", "world  "]
        )

        files = [to_be_fixed_md, remain_intact_md]
        mock_opts = mock.Mock(check=False, dry_run=False, diff=False)

        # Only agents/to_be_fixed.md should be formatted because of the config file.
        # other/remain_intact.md won't be formatted.
        ret = cl_format._RunMarkdownFormat(
            mock_opts, files, self._top_dir, None
        )
        self.assertEqual(0, ret)

        with open(to_be_fixed_md, "r") as f:
            self.assertEqual("# Hello\n\nworld\n", f.read())

        with open(remain_intact_md, "r") as f:
            self.assertEqual("#  Hello\n\nworld  ", f.read())

    def testMarkdownFormatNumberedList(self):
        self._make_markdown_config("agents")
        foo_md = os.path.join(self._top_dir, "agents/foo.md")
        self._make_temp_file(
            "agents/foo.md", ["#  Hello", "", "1. First item", "2. Second item"]
        )

        files = [foo_md]
        mock_opts = mock.Mock(check=False, dry_run=False, diff=False)

        ret = cl_format._RunMarkdownFormat(
            mock_opts, files, self._top_dir, None
        )
        self.assertEqual(0, ret)

        with open(foo_md, "r") as f:
            self.assertEqual(
                "# Hello\n\n1. First item\n2. Second item\n", f.read()
            )

    def testMarkdownFormatCheck(self):
        self._make_markdown_config("agents")
        foo_md = os.path.join(self._top_dir, "agents/foo.md")
        self._make_temp_file("agents/foo.md", ["#  Hello", "", "world  "])

        files = [foo_md]
        # --check should return 2 if files are unformatted
        mock_opts = mock.Mock(dry_run=True, diff=False)
        self.assertEqual(
            cl_format._RunMarkdownFormat(mock_opts, files, self._top_dir, None),
            2,
        )

        with open(foo_md, "r") as f:
            self.assertEqual("#  Hello\n\nworld  ", f.read())

    @mock.patch("cl_format.subprocess2.call")
    def testMarkdownFormatDiff(self, mock_call):
        self._make_markdown_config("agents")
        foo_md = os.path.join(self._top_dir, "agents/foo.md")
        # Add an empty string at the end so it ends with a newline
        self._make_temp_file("agents/foo.md", ["#  Hello", "", "world  ", ""])

        captured_stdout = []

        def fake_call(cmd, **kwargs):
            import subprocess

            res = subprocess.run(cmd, capture_output=True, text=True)
            captured_stdout.append(res.stdout)
            return res.returncode

        mock_call.side_effect = fake_call

        files = [foo_md]
        # --diff should return 2 if there's a diff, but not modify the file.
        mock_opts = mock.Mock(dry_run=False, diff=True)
        ret = cl_format._RunMarkdownFormat(
            mock_opts, files, self._top_dir, None
        )
        self.assertEqual(2, ret)

        with open(foo_md, "r") as f:
            self.assertEqual("#  Hello\n\nworld  \n", f.read())

        stdout_val = captured_stdout[0]
        expected_diff = (
            f"--- a/{foo_md}\n"
            f"+++ b/{foo_md}\n"
            "@@ -1,3 +1,3 @@\n"
            "-#  Hello\n"
            "+# Hello\n"
            " \n"
            "-world  \n"
            "+world\n"
        )
        self.assertEqual(expected_diff, stdout_val)

    @mock.patch("gclient_paths.GetPrimarySolutionPath")
    @mock.patch("cl_format.subprocess2.call")
    def testLitTemplateFormatter(self, mock_call, mock_solution_path):
        """Note: This test does not run the real formatter. It validates that
        the correct arguments are passed to it and that it is invoked for
        .html.ts files when the style file is found.
        """
        mock_solution_path.return_value = self._top_dir
        self._make_temp_file(
            os.path.join(
                "ui",
                "webui",
                "resources",
                "tools",
                "lit_template_formatter",
                "main.js",
            ),
            ["// dummy"],
        )
        self._make_temp_file(
            os.path.join("third_party", "node", "node.py"), ["# dummy"]
        )
        self._make_lit_template_formatter_config("webui")
        foo_html_ts = os.path.join(self._top_dir, "webui", "foo.html.ts")
        bar_html_ts = os.path.join(self._top_dir, "other", "bar.html.ts")
        self._make_temp_file(os.path.join("webui", "foo.html.ts"), ["// test"])
        self._make_temp_file(os.path.join("other", "bar.html.ts"), ["// test"])

        expected_formatter = os.path.join(
            self._top_dir,
            "ui",
            "webui",
            "resources",
            "tools",
            "lit_template_formatter",
            "main.js",
        )
        expected_node = os.path.join(
            self._top_dir, "third_party", "node", "node.py"
        )

        # 1. Normal format: only foo_html_ts should be formatted (bar_html_ts skipped).
        mock_call.return_value = 0
        mock_opts = mock.Mock(dry_run=False, diff=False)
        self.assertEqual(
            0,
            cl_format._RunLitTemplateFormatter(
                mock_opts, [foo_html_ts, bar_html_ts], self._top_dir, None
            ),
        )
        mock_call.assert_called_once_with(
            ["vpython3", expected_node, expected_formatter, foo_html_ts],
            cwd=self._top_dir,
        )

        # 2. Dry-run mode: should pass --dry-run and return 2 when unformatted.
        mock_call.reset_mock()
        mock_call.return_value = 2
        mock_opts = mock.Mock(dry_run=True, diff=False)
        self.assertEqual(
            2,
            cl_format._RunLitTemplateFormatter(
                mock_opts, [foo_html_ts], self._top_dir, None
            ),
        )
        mock_call.assert_called_once_with(
            [
                "vpython3",
                expected_node,
                expected_formatter,
                "--dry-run",
                foo_html_ts,
            ],
            cwd=self._top_dir,
        )

        # 3. Diff mode: should pass --diff.
        mock_call.reset_mock()
        mock_call.return_value = 0
        mock_opts = mock.Mock(dry_run=False, diff=True)
        self.assertEqual(
            0,
            cl_format._RunLitTemplateFormatter(
                mock_opts, [foo_html_ts], self._top_dir, None
            ),
        )
        mock_call.assert_called_once_with(
            [
                "vpython3",
                expected_node,
                expected_formatter,
                "--diff",
                foo_html_ts,
            ],
            cwd=self._top_dir,
        )

    def _run_command_mock(self, return_value):
        def f(*args, **kwargs):
            if "stdin" in kwargs:
                self.assertIsInstance(kwargs["stdin"], bytes)
            return return_value

        return f

    def testClangFormatDryRun(self):
        diffs = cl_format._SplitDiffsByFile(test_format_input_diff)
        files = [f for f in diffs if f.endswith(".h")]
        mock_opts = mock.Mock(full=True, dry_run=True, diff=False)
        for f in files:
            self._make_temp_file(f, ["// test"])

        try:
            previous_cwd = os.getcwd()
            os.chdir(self._top_dir)
            # If the clang-format-diff returns 0, if the input and output codes
            # are the same.
            cl_format.RunCommand.side_effect = self._run_command_mock("// test")
            return_value = cl_format._RunClangFormatDiff(
                mock_opts, files, self._top_dir, None
            )
            self.assertEqual(0, return_value)

            # Returns 2, otherwise.
            cl_format.RunCommand.side_effect = self._run_command_mock(
                "  // test"
            )
            return_value = cl_format._RunClangFormatDiff(
                mock_opts, files, self._top_dir, None
            )
            self.assertEqual(2, return_value)
        finally:
            os.chdir(previous_cwd)

    def testClangFormatDiff(self):
        diffs = cl_format._SplitDiffsByFile(test_format_input_diff)
        files = [f for f in diffs if f.endswith(".h")]
        mock_opts = mock.Mock(full=False, dry_run=False, diff=False)

        # Diff
        cl_format.RunCommand.return_value = ""
        return_value = cl_format._RunClangFormatDiff(
            mock_opts, files, self._top_dir, diffs
        )
        self.assertEqual(0, return_value)
        cl_format.RunCommand.assert_called_with(
            mock.ANY,  # command
            error_ok=True,
            # it should stream the patches for the selected files only.
            stdin="\n".join(diffs.get(f, "") for f in files).encode(),
            cwd=self._top_dir,
            env=mock.ANY,
            shell=mock.ANY,
        )

    def testClangFormatDiffFilter(self):
        diffs = cl_format._SplitDiffsByFile(test_format_input_diff)
        files = [f for f in diffs if f.endswith(".h")]
        mock_opts = mock.Mock(full=False, dry_run=True, diff=True)

        # Simulate clang-format-diff.py returning a full-deletion diff for an
        # ignored file, plus a regular valid diff for another file.
        mock_stdout = (
            "--- a/ignored_file.h\n"
            "+++ b/ignored_file.h\n"
            "@@ -1,10 +0,0 @@\n"
            "-deleted\n"
            "--- a/valid_file.h\n"
            "+++ b/valid_file.h\n"
            "@@ -5,2 +5,2 @@\n"
            "-old\n"
            "+new\n"
        )
        cl_format.RunCommand.return_value = mock_stdout

        with mock.patch("sys.stdout.write") as mock_stdout_write:
            return_value = cl_format._RunClangFormatDiff(
                mock_opts, files, self._top_dir, diffs
            )

            self.assertEqual(2, return_value)

            expected_output = (
                "--- a/valid_file.h\n"
                "+++ b/valid_file.h\n"
                "@@ -5,2 +5,2 @@\n"
                "-old\n"
                "+new\n"
            )
            mock_stdout_write.assert_called_once_with(expected_output)

    def testYapfignoreExplicit(self):
        self._make_yapfignore(["foo/bar.py", "foo/bar/baz.py"])
        files = [
            "bar.py",
            "foo/bar.py",
            "foo/baz.py",
            "foo/bar/baz.py",
            "foo/bar/foobar.py",
        ]
        expected = [
            "bar.py",
            "foo/baz.py",
            "foo/bar/foobar.py",
        ]
        self._check_yapf_filtering(files, expected)

    def testYapfignoreSingleWildcards(self):
        self._make_yapfignore(["*bar.py", "foo*", "baz*.py"])
        files = [
            "bar.py",  # Matched by *bar.py.
            "bar.txt",
            "foobar.py",  # Matched by *bar.py, foo*.
            "foobar.txt",  # Matched by foo*.
            "bazbar.py",  # Matched by *bar.py, baz*.py.
            "bazbar.txt",
            "foo/baz.txt",  # Matched by foo*.
            "bar/bar.py",  # Matched by *bar.py.
            "baz/foo.py",  # Matched by baz*.py, foo*.
            "baz/foo.txt",
        ]
        expected = [
            "bar.txt",
            "bazbar.txt",
            "baz/foo.txt",
        ]
        self._check_yapf_filtering(files, expected)

    def testYapfignoreMultiplewildcards(self):
        self._make_yapfignore(["*bar*", "*foo*baz.txt"])
        files = [
            "bar.py",  # Matched by *bar*.
            "bar.txt",  # Matched by *bar*.
            "abar.py",  # Matched by *bar*.
            "foobaz.txt",  # Matched by *foo*baz.txt.
            "foobaz.py",
            "afoobaz.txt",  # Matched by *foo*baz.txt.
        ]
        expected = [
            "foobaz.py",
        ]
        self._check_yapf_filtering(files, expected)

    def testYapfignoreComments(self):
        self._make_yapfignore(["test.py", "#test2.py"])
        files = [
            "test.py",
            "test2.py",
        ]
        expected = [
            "test2.py",
        ]
        self._check_yapf_filtering(files, expected)

    def testYapfHandleUtf8(self):
        self._make_yapfignore(["test.py", "test_🌐.py"])
        files = [
            "test.py",
            "test_🌐.py",
            "test2.py",
        ]
        expected = [
            "test2.py",
        ]
        self._check_yapf_filtering(files, expected)

    def testYapfignoreBlankLines(self):
        self._make_yapfignore(["test.py", "", "", "test2.py"])
        files = [
            "test.py",
            "test2.py",
            "test3.py",
        ]
        expected = [
            "test3.py",
        ]
        self._check_yapf_filtering(files, expected)

    def testYapfignoreWhitespace(self):
        self._make_yapfignore([" test.py "])
        files = [
            "test.py",
            "test2.py",
        ]
        expected = [
            "test2.py",
        ]
        self._check_yapf_filtering(files, expected)

    def testYapfignoreNoFiles(self):
        self._make_yapfignore(["test.py"])
        self._check_yapf_filtering([], [])

    def testYapfignoreMissingYapfignore(self):
        files = [
            "test.py",
        ]
        expected = [
            "test.py",
        ]
        self._check_yapf_filtering(files, expected)

    @mock.patch("gclient_paths.GetPrimarySolutionPath")
    def testRunMetricsXMLFormatSkipIfPresubmit(self, find_top_dir):
        """Verifies that it skips the formatting if opts.presubmit is True."""
        find_top_dir.return_value = self._top_dir
        mock_opts = mock.Mock(
            full=True, dry_run=True, diff=False, presubmit=True
        )
        files = [
            os.path.join(self._top_dir, "tools", "metrics", "ukm", "ukm.xml"),
        ]
        return_value = cl_format._RunMetricsXMLFormat(
            mock_opts, files, self._top_dir, "HEAD"
        )
        cl_format.RunCommand.assert_not_called()
        self.assertEqual(0, return_value)

    @mock.patch("gclient_paths.GetPrimarySolutionPath")
    def testRunMetricsFormatWithUkm(self, find_top_dir):
        """Checks if the command line arguments do not contain the input path."""
        find_top_dir.return_value = self._top_dir
        mock_opts = mock.Mock(
            full=True, dry_run=False, diff=False, presubmit=False
        )
        files = [
            os.path.join(self._top_dir, "tools", "metrics", "ukm", "ukm.xml"),
        ]
        cl_format._RunMetricsXMLFormat(mock_opts, files, self._top_dir, "HEAD")
        cl_format.RunCommand.assert_called_with(
            [
                mock.ANY,
                os.path.join(
                    self._top_dir, "tools", "metrics", "ukm", "pretty_print.py"
                ),
                "--non-interactive",
            ],
            cwd=self._top_dir,
        )

    @mock.patch("gclient_paths.GetPrimarySolutionPath")
    def testRunMetricsFormatWithHistograms(self, find_top_dir):
        """Checks if the command line arguments contain the input file paths."""
        find_top_dir.return_value = self._top_dir
        mock_opts = mock.Mock(
            full=True, dry_run=False, diff=False, presubmit=False
        )
        files = [
            os.path.join(
                self._top_dir, "tools", "metrics", "histograms", "enums.xml"
            ),
            os.path.join(
                self._top_dir,
                "tools",
                "metrics",
                "histograms",
                "test_data",
                "enums.xml",
            ),
        ]
        cl_format._RunMetricsXMLFormat(mock_opts, files, self._top_dir, "HEAD")

        pretty_print_path = os.path.join(
            self._top_dir, "tools", "metrics", "histograms", "pretty_print.py"
        )
        cl_format.RunCommand.assert_has_calls(
            [
                mock.call(
                    [
                        mock.ANY,
                        pretty_print_path,
                        "--non-interactive",
                        files[0],
                    ],
                    cwd=self._top_dir,
                ),
                mock.call(
                    [
                        mock.ANY,
                        pretty_print_path,
                        "--non-interactive",
                        files[1],
                    ],
                    cwd=self._top_dir,
                ),
            ]
        )

    @mock.patch("subprocess2.call")
    def testLUCICfgFormatWorks(self, mock_call):
        """Checks if lucicfg is given then input file path."""
        mock_opts = mock.Mock(dry_run=False)
        files = ["test/main.star"]
        mock_call.return_value = 0
        ret = cl_format._RunLUCICfgFormat(
            mock_opts, files, self._top_dir, "HEAD"
        )
        mock_call.assert_called_with(
            [
                mock.ANY,
                "fmt",
                "test/main.star",
            ]
        )
        self.assertEqual(ret, 0)

    @mock.patch("subprocess2.call")
    def testLUCICfgFormatWithDryRun(self, mock_call):
        """Tests the command with --dry-run."""
        mock_opts = mock.Mock(dry_run=True)
        files = ["test/main.star"]
        cl_format._RunLUCICfgFormat(mock_opts, files, self._top_dir, "HEAD")
        mock_call.assert_called_with(
            [
                mock.ANY,
                "fmt",
                "--dry-run",
                "test/main.star",
            ]
        )

    @mock.patch("subprocess2.call")
    def testLUCICfgFormatWithDryRunReturnCode(self, mock_call):
        """Tests that it returns 2 for non-zero exit codes."""
        mock_opts = mock.Mock(dry_run=True)
        files = ["test/main.star"]
        run = cl_format._RunLUCICfgFormat

        mock_call.return_value = 0
        self.assertEqual(run(mock_opts, files, self._top_dir, "HEAD"), 0)
        mock_call.return_value = 1
        self.assertEqual(run(mock_opts, files, self._top_dir, "HEAD"), 2)
        mock_call.return_value = 2
        self.assertEqual(run(mock_opts, files, self._top_dir, "HEAD"), 2)
        mock_call.return_value = 255
        self.assertEqual(run(mock_opts, files, self._top_dir, "HEAD"), 2)

    @mock.patch("sys.stderr", io.StringIO())
    def testInputDiffFileIsMutallyExclusiveWithFull(self):
        """Verifies that opts.input_diff_file cannot be set with opts.full."""
        ret = git_cl.main(
            [
                "format",
                "--input_diff_file",
                "/tmp/patch.diff",
                "--full",
            ]
        )
        self.assertEqual(1, ret)
        self.assertEqual(
            sys.stderr.getvalue().strip(),
            "--full and --input_diff_file cannot be used together.",
        )

    @mock.patch("sys.stderr", io.StringIO())
    def testInputDiffFileIsMutallyExclusiveWithPositionalFilePaths(self):
        """Verifies that opts.input_diff_file cannot be set if file given."""
        ret = git_cl.main(
            [
                "format",
                "--input_diff_file",
                "/tmp/patch.diff",
                "common.cc",
            ]
        )
        self.assertEqual(1, ret)
        self.assertEqual(
            sys.stderr.getvalue().strip(),
            "No file paths to format are allowed "
            "if --input_diff_file is given.",
        )

    @mock.patch("cl_format._IsRuffBatchSupported", return_value=False)
    @mock.patch("cl_format._RunClangFormatDiff", return_value=0)
    def testInputDiffFile(self, clang_formatter, mock_supported):
        """Tests git cl format with --input_diff_file."""
        # Windows doesn't allow a file to be reopened while it's open by
        # another handler.
        with tempfile.NamedTemporaryFile(mode="w+", delete=False) as input_diff:
            input_diff.write(test_format_input_diff)

        try:
            previous_cwd = os.getcwd()
            os.chdir(self._top_dir)
            ret = git_cl.main(
                [
                    "format",
                    "--input_diff_file",
                    input_diff.name,
                    "--presubmit",
                    "--dry-run",
                    "--python",
                ]
            )
            self.assertEqual(0, ret)

            clang_formatter.assert_called_with(
                mock.ANY,
                ["net/base/net_error_details.h", "net/base/net_error_list.h"],
                mock.ANY,
                mock.ANY,
            )
            cl_format.RunCommand.assert_called_with(
                [
                    "vpython3",
                    mock.ANY,
                    "--style",
                    mock.ANY,
                    "testing/xvfb_unittest.py",
                    "-l",
                    "18-24",
                    "--diff",
                ],
                cwd=self._top_dir,
                error_ok=True,
                shell=mock.ANY,
                stderr=-1,
            )
        finally:
            os.remove(input_diff.name)
            os.chdir(previous_cwd)

    @mock.patch("cl_format._IsRuffBatchSupported", return_value=False)
    @mock.patch("cl_format._RunClangFormatDiff", return_value=0)
    def testInputDiffFile_Stdin(self, clang_formatter, mock_supported):
        """Tests git cl format with --input_diff_file - reading from stdin."""
        previous_cwd = os.getcwd()
        os.chdir(self._top_dir)
        try:
            with mock.patch("sys.stdin", io.StringIO(test_format_input_diff)):
                ret = git_cl.main(
                    [
                        "format",
                        "--input_diff_file",
                        "-",
                        "--presubmit",
                        "--dry-run",
                        "--python",
                    ]
                )
            self.assertEqual(0, ret)

            clang_formatter.assert_called_with(
                mock.ANY,
                ["net/base/net_error_details.h", "net/base/net_error_list.h"],
                mock.ANY,
                mock.ANY,
            )
            cl_format.RunCommand.assert_called_with(
                [
                    "vpython3",
                    mock.ANY,
                    "--style",
                    mock.ANY,
                    "testing/xvfb_unittest.py",
                    "-l",
                    "18-24",
                    "--diff",
                ],
                cwd=self._top_dir,
                error_ok=True,
                shell=mock.ANY,
                stderr=-1,
            )
        finally:
            os.chdir(previous_cwd)

    @mock.patch("cl_format._IsRuffBatchSupported", return_value=False)
    @mock.patch("cl_format._RunClangFormatDiff", return_value=0)
    def testInputDiffFile_Stdin_Buffer(self, clang_formatter, mock_supported):
        """Tests git cl format with --input_diff_file - reading UTF-8 bytes from sys.stdin.buffer."""
        previous_cwd = os.getcwd()
        os.chdir(self._top_dir)
        try:
            mock_stdin = io.TextIOWrapper(
                io.BytesIO(test_format_input_diff.encode("utf-8")),
                encoding="latin-1",
            )
            with mock.patch("sys.stdin", mock_stdin):
                ret = git_cl.main(
                    [
                        "format",
                        "--input_diff_file",
                        "-",
                        "--presubmit",
                        "--dry-run",
                        "--python",
                    ]
                )
            self.assertEqual(0, ret)

            clang_formatter.assert_called_with(
                mock.ANY,
                ["net/base/net_error_details.h", "net/base/net_error_list.h"],
                mock.ANY,
                mock.ANY,
            )
            cl_format.RunCommand.assert_called_with(
                [
                    "vpython3",
                    mock.ANY,
                    "--style",
                    mock.ANY,
                    "testing/xvfb_unittest.py",
                    "-l",
                    "18-24",
                    "--diff",
                ],
                cwd=self._top_dir,
                error_ok=True,
                shell=mock.ANY,
                stderr=-1,
            )
        finally:
            os.chdir(previous_cwd)

    @mock.patch("cl_format._RunClangFormatDiff", return_value=0)
    def testInputDiffFileWithWindowsPatch(self, clang_formatter):
        # Windows doesn't allow a file to be reopened while it's open by
        # another handler.
        with tempfile.NamedTemporaryFile(mode="w+", delete=False) as input_diff:
            input_diff.write(test_format_input_diff_windows)

        try:
            ret = git_cl.main(
                [
                    "format",
                    "--input_diff_file",
                    input_diff.name,
                    "--presubmit",
                ]
            )
            self.assertEqual(0, ret)
            clang_formatter.assert_called_with(
                mock.ANY,
                ["C:\\\\path\\\\to\\\\dst\\\\file.cc"],
                mock.ANY,
                mock.ANY,
            )
        finally:
            os.remove(input_diff.name)

    @mock.patch("cl_format._RunClangFormatDiff", return_value=0)
    def testInputDiffFileWithUtf8(self, clang_formatter):
        utf8_diff = (
            "diff --git a/test.cc b/test.cc\n"
            "--- a/test.cc\n"
            "+++ b/test.cc\n"
            "@@ -1,1 +1,1 @@\n"
            "-// English comment\n"
            "+// 日本語コメント (Unicode: \u3042\u3044\u3046\u3048\u304a \u2603)\n"
        )
        with tempfile.TemporaryDirectory() as tmp_dir:
            input_diff_path = os.path.join(tmp_dir, "input.diff")
            with open(input_diff_path, "wb") as input_diff:
                input_diff.write(utf8_diff.encode("utf-8"))

            ret = git_cl.main(
                [
                    "format",
                    "--input_diff_file",
                    input_diff_path,
                    "--presubmit",
                ]
            )
            self.assertEqual(0, ret)
            clang_formatter.assert_called_with(
                mock.ANY,
                ["test.cc"],
                mock.ANY,
                mock.ANY,
            )

    @mock.patch("cl_format._RunClangFormatDiff", return_value=0)
    @mock.patch("cl_format.settings.GetFormatJs", return_value=True)
    def testJsDefaultTrue(self, get_format_js_mock, clang_formatter):
        # Note: The paths in this diff are dummy files used for testing and do not
        # actually exist in the repository.
        test_format_input_diff_html_ts = """
diff --git a/ui/webui/resources/tools/foo.ts b/ui/webui/resources/tools/foo.ts
--- a/ui/webui/resources/tools/foo.ts
+++ b/ui/webui/resources/tools/foo.ts
@@ -1,2 +1,2 @@
-const a = 1;
+const a  =  1;
diff --git a/ui/webui/resources/tools/bar.html.ts b/ui/webui/resources/tools/bar.html.ts
--- a/ui/webui/resources/tools/bar.html.ts
+++ b/ui/webui/resources/tools/bar.html.ts
@@ -1,2 +1,2 @@
-const html = `<div></div>`;
+const html  =  `<div></div>`;
         """

        with tempfile.NamedTemporaryFile(mode="w+", delete=False) as input_diff:
            input_diff.write(test_format_input_diff_html_ts)

        try:
            previous_cwd = os.getcwd()
            os.chdir(self._top_dir)
            ret = git_cl.main(
                [
                    "format",
                    "--input_diff_file",
                    input_diff.name,
                    "--presubmit",
                    "--dry-run",
                ]
            )
            self.assertEqual(0, ret)

            # If GetFormatJs is True, it should format JS/TS by default, but exempt .html.ts files.
            # So only foo.ts should be passed to the formatter, and bar.html.ts should be skipped.
            clang_formatter.assert_called_with(
                mock.ANY,
                ["ui/webui/resources/tools/foo.ts"],
                mock.ANY,
                mock.ANY,
            )
        finally:
            os.remove(input_diff.name)
            os.chdir(previous_cwd)

    @mock.patch("cl_format._RunClangFormatDiff", return_value=0)
    @mock.patch("cl_format.settings.GetFormatJs", return_value=False)
    def testJsDefaultFalse(self, get_format_js_mock, clang_formatter):
        # Note: The paths in this diff are dummy files used for testing and do not
        # actually exist in the repository.
        test_format_input_diff_html_ts = """
diff --git a/ui/webui/resources/tools/foo.ts b/ui/webui/resources/tools/foo.ts
--- a/ui/webui/resources/tools/foo.ts
+++ b/ui/webui/resources/tools/foo.ts
@@ -1,2 +1,2 @@
-const a = 1;
+const a  =  1;
diff --git a/ui/webui/resources/tools/bar.html.ts b/ui/webui/resources/tools/bar.html.ts
--- a/ui/webui/resources/tools/bar.html.ts
+++ b/ui/webui/resources/tools/bar.html.ts
@@ -1,2 +1,2 @@
-const html = `<div></div>`;
+const html  =  `<div></div>`;
         """

        with tempfile.NamedTemporaryFile(mode="w+", delete=False) as input_diff:
            input_diff.write(test_format_input_diff_html_ts)

        try:
            previous_cwd = os.getcwd()
            os.chdir(self._top_dir)
            ret = git_cl.main(
                [
                    "format",
                    "--input_diff_file",
                    input_diff.name,
                    "--presubmit",
                    "--dry-run",
                ]
            )
            self.assertEqual(0, ret)

            # If format_js is False, JS formatting is off by default, so clang_formatter should NOT be called.
            clang_formatter.assert_not_called()
        finally:
            os.remove(input_diff.name)
            os.chdir(previous_cwd)

    @mock.patch("cl_format._RunClangFormatDiff", return_value=0)
    @mock.patch("cl_format.settings.GetFormatJs", return_value=False)
    def testJsOptIn(self, get_format_js_mock, clang_formatter):
        # Note: The paths in this diff are dummy files used for testing and do not
        # actually exist in the repository.
        test_format_input_diff_html_ts = """
diff --git a/ui/webui/resources/tools/foo.ts b/ui/webui/resources/tools/foo.ts
--- a/ui/webui/resources/tools/foo.ts
+++ b/ui/webui/resources/tools/foo.ts
@@ -1,2 +1,2 @@
-const a = 1;
+const a  =  1;
diff --git a/ui/webui/resources/tools/bar.html.ts b/ui/webui/resources/tools/bar.html.ts
--- a/ui/webui/resources/tools/bar.html.ts
+++ b/ui/webui/resources/tools/bar.html.ts
@@ -1,2 +1,2 @@
-const html = `<div></div>`;
+const html  =  `<div></div>`;
         """

        with tempfile.NamedTemporaryFile(mode="w+", delete=False) as input_diff:
            input_diff.write(test_format_input_diff_html_ts)

        try:
            previous_cwd = os.getcwd()
            os.chdir(self._top_dir)
            ret = git_cl.main(
                [
                    "format",
                    "--input_diff_file",
                    input_diff.name,
                    "--presubmit",
                    "--dry-run",
                    "--js",
                ]
            )
            self.assertEqual(0, ret)

            # If format_js is False, JS formatting is off by default, but we can opt-in using --js.
            # bar.html.ts should still be exempted.
            clang_formatter.assert_called_with(
                mock.ANY,
                ["ui/webui/resources/tools/foo.ts"],
                mock.ANY,
                mock.ANY,
            )
        finally:
            os.remove(input_diff.name)
            os.chdir(previous_cwd)

    def testFindFilesToFormatInputDiffFileReturnsList(self):
        sample_diff = (
            "diff --git a/foo.cc b/foo.cc\n"
            "index 111..222 100644\n"
            "--- a/foo.cc\n"
            "+++ b/foo.cc\n"
            "@@ -1 +1 @@\n"
            "-old\n"
            "+new\n"
        )
        with tempfile.NamedTemporaryFile(
            mode="w", encoding="utf-8", delete=False
        ) as f:
            f.write(sample_diff)
            diff_file = f.name
        try:
            opts = optparse.Values(
                {"input_diff_file": diff_file, "full": False}
            )
            files, diffs = cl_format._FindFilesToFormat(opts, None, "HEAD")
            self.assertIsInstance(files, list)
            self.assertEqual(["foo.cc"], files)
            self.assertIn("foo.cc", diffs)
        finally:
            os.remove(diff_file)

    @mock.patch(
        "cl_format.RunGitDiffCmd",
        return_value=(
            "diff --git a/bar.py b/bar.py\n"
            "index 111..222 100644\n"
            "--- a/bar.py\n"
            "+++ b/bar.py\n"
            "@@ -1 +1 @@\n"
            "-old\n"
            "+new\n"
        ),
    )
    def testFindFilesToFormatDiffReturnsList(self, mock_diff):
        opts = optparse.Values({"input_diff_file": None, "full": False})
        files, diffs = cl_format._FindFilesToFormat(opts, None, "HEAD")
        self.assertIsInstance(files, list)
        self.assertEqual(["bar.py"], files)
        self.assertIn("bar.py", diffs)

    @mock.patch("cl_format.RunGitDiffCmd", return_value="baz.gn\n")
    def testFindFilesToFormatFullReturnsList(self, mock_diff):
        opts = optparse.Values({"input_diff_file": None, "full": True})
        files, diffs = cl_format._FindFilesToFormat(opts, None, "HEAD")
        self.assertIsInstance(files, list)
        self.assertEqual(["baz.gn"], files)
        self.assertIsNone(diffs)

    @mock.patch("cl_format._RunClangFormatDiff")
    @mock.patch("cl_format._RunPythonFormat")
    def testConcurrentMultiLanguageFormat(self, mock_py, mock_clang):
        """Tests concurrent execution when formatting multi-language changes."""
        mock_py_event = threading.Event()
        mock_clang_event = threading.Event()

        def py_side_effect(*args, **kwargs):
            mock_py_event.set()
            mock_clang_event.wait(timeout=2.0)
            print("python formatted")
            return 0

        def clang_side_effect(*args, **kwargs):
            mock_clang_event.set()
            mock_py_event.wait(timeout=2.0)
            sys.stdout.buffer.write(b"clang formatted\n")
            return 0

        mock_py.side_effect = py_side_effect
        mock_clang.side_effect = clang_side_effect

        with tempfile.NamedTemporaryFile(mode="w+", delete=False) as input_diff:
            input_diff.write(
                "diff --git a/foo.cc b/foo.cc\n"
                "+++ b/foo.cc\n"
                "@@ -0,0 +1 @@\n"
                "+int x;\n"
                "diff --git a/bar.py b/bar.py\n"
                "+++ b/bar.py\n"
                "@@ -0,0 +1 @@\n"
                "+x = 1\n"
            )

        try:
            previous_cwd = os.getcwd()
            os.chdir(self._top_dir)
            ret = git_cl.main(
                [
                    "format",
                    "--input_diff_file",
                    input_diff.name,
                    "--presubmit",
                    "--python",
                ]
            )
            self.assertEqual(0, ret)
            mock_clang.assert_called_once()
            mock_py.assert_called_once()
        finally:
            os.remove(input_diff.name)
            os.chdir(previous_cwd)

    @mock.patch("cl_format._RunClangFormatDiff", return_value=0)
    @mock.patch("cl_format._RunPythonFormat", return_value=2)
    def testConcurrentMultiLanguageFormatNonZeroReturn(
        self, mock_py, mock_clang
    ):
        """Tests that non-zero return code from any formatter is propagated."""
        with tempfile.NamedTemporaryFile(mode="w+", delete=False) as input_diff:
            input_diff.write(
                "diff --git a/foo.cc b/foo.cc\n"
                "+++ b/foo.cc\n"
                "@@ -0,0 +1 @@\n"
                "+int x;\n"
                "diff --git a/bar.py b/bar.py\n"
                "+++ b/bar.py\n"
                "@@ -0,0 +1 @@\n"
                "+x = 1\n"
            )

        try:
            previous_cwd = os.getcwd()
            os.chdir(self._top_dir)
            ret = git_cl.main(
                [
                    "format",
                    "--input_diff_file",
                    input_diff.name,
                    "--presubmit",
                    "--python",
                ]
            )
            self.assertEqual(2, ret)
            mock_clang.assert_called_once()
            mock_py.assert_called_once()
        finally:
            os.remove(input_diff.name)
            os.chdir(previous_cwd)

    @mock.patch("cl_format._RunClangFormatDiff", return_value=0)
    @mock.patch("cl_format._RunPythonFormat")
    def testConcurrentMultiLanguageFormatSystemExit(self, mock_py, mock_clang):
        """Tests that SystemExit from a formatter flushes output and re-raises."""

        def py_side_effect(*args, **kwargs):
            sys.stderr.write("python formatter error\n")
            raise SystemExit(1)

        mock_py.side_effect = py_side_effect

        with tempfile.NamedTemporaryFile(mode="w+", delete=False) as input_diff:
            input_diff.write(
                "diff --git a/foo.cc b/foo.cc\n"
                "+++ b/foo.cc\n"
                "@@ -0,0 +1 @@\n"
                "+int x;\n"
                "diff --git a/bar.py b/bar.py\n"
                "+++ b/bar.py\n"
                "@@ -0,0 +1 @@\n"
                "+x = 1\n"
            )

        try:
            previous_cwd = os.getcwd()
            os.chdir(self._top_dir)
            with mock.patch("sys.stderr", io.StringIO()) as mock_err:
                with self.assertRaises(SystemExit) as cm:
                    git_cl.main(
                        [
                            "format",
                            "--input_diff_file",
                            input_diff.name,
                            "--presubmit",
                            "--python",
                        ]
                    )
                self.assertEqual(1, cm.exception.code)
                self.assertIn("python formatter error", mock_err.getvalue())
        finally:
            os.remove(input_diff.name)
            os.chdir(previous_cwd)

    def testThreadLocalStreamCustomEncoding(self):
        """Tests that _ThreadLocalStream uses the fallback stream's encoding."""
        fallback = mock.Mock(encoding="iso-8859-1", errors="replace")
        stream = cl_format._ThreadLocalStream(fallback)
        buf = io.BytesIO()
        stream.set_buffer(buf)
        stream.write("café")
        self.assertEqual("café".encode("iso-8859-1"), buf.getvalue())


class TestRuffBatchIntegration(unittest.TestCase):
    def setUp(self):
        super(TestRuffBatchIntegration, self).setUp()
        self.test_dir = tempfile.mkdtemp()
        self.orig_cwd = os.getcwd()
        os.chdir(self.test_dir)
        self.addCleanup(shutil.rmtree, self.test_dir)
        self.addCleanup(os.chdir, self.orig_cwd)

    @mock.patch("cl_format._IsRuffBatchSupported", return_value=False)
    @mock.patch("cl_format._RunYapf")
    def test_fallback_to_yapf(self, mock_run_yapf, mock_supported):
        mock_opts = mock.Mock(python=True, full=True, diff=False, dry_run=False)
        with mock.patch("os.path.exists", return_value=True):
            cl_format._RunPythonFormat(
                mock_opts, ["foo.py"], self.test_dir, None
            )
        mock_run_yapf.assert_called_once_with(
            mock_opts, ["foo.py"], self.test_dir, None
        )

    @mock.patch("cl_format._IsRuffBatchSupported", return_value=True)
    @mock.patch("subprocess2.communicate")
    def test_ruff_batch_success(self, mock_communicate, mock_supported):
        mock_opts = mock.Mock(python=True, full=True, diff=False, dry_run=False)
        mock_communicate.return_value = ((b"", b""), 0)

        with mock.patch("os.path.exists", return_value=True):
            code = cl_format._RunPythonFormat(
                mock_opts, ["foo.py"], self.test_dir, None
            )

        self.assertEqual(0, code)
        expected_config = {
            "root": self.test_dir,
            "diff": False,
            "dry_run": False,
            "full": True,
            "files": [{"path": "foo.py"}],
        }
        self.assertEqual(1, mock_communicate.call_count)
        call_args, call_kwargs = mock_communicate.call_args
        self.assertEqual(
            ["vpython3", cl_format._GetRuffChromiumPath(), "--batch"],
            call_args[0],
        )
        self.assertEqual(
            json.dumps(expected_config).encode("utf-8"), call_kwargs["stdin"]
        )


if __name__ == "__main__":
    logging.basicConfig(
        level=logging.DEBUG if "-v" in sys.argv else logging.ERROR
    )
    unittest.main()
