#!/usr/bin/env vpython3
# Copyright 2016 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.
"""Tests for git_dates."""

from io import BytesIO, StringIO
import os
import re
import shutil
import sys
import tempfile
from unittest import mock

DEPOT_TOOLS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, DEPOT_TOOLS_ROOT)

from testing_support import coverage_utils
from testing_support import git_test_utils

import gclient_utils

GitRepo = git_test_utils.GitRepo

# TODO: Should fix these warnings.
# pylint: disable=line-too-long


class GitHyperBlameTestBase(git_test_utils.GitRepoReadOnlyTestBase):
    @classmethod
    def setUpClass(cls):
        super(GitHyperBlameTestBase, cls).setUpClass()
        import git_hyper_blame
        cls.git_hyper_blame = git_hyper_blame

    def setUp(self):
        mock.patch('sys.stderr', StringIO()).start()
        self.addCleanup(mock.patch.stopall)

    def run_hyperblame(self, ignored, filename, revision):
        outbuf = BytesIO()
        ignored = [self.repo[c] for c in ignored]
        retval = self.repo.run(self.git_hyper_blame.hyper_blame, outbuf,
                               ignored, filename, revision)
        return retval, outbuf.getvalue().rstrip().split(b'\n')

    def blame_line(self, commit_name, rest, author=None, filename=None):
        """Generate a blame line from a commit.

    Args:
      commit_name: The commit's schema name.
      rest: The blame line after the timestamp. e.g., '2) file2 - merged'.
      author: The author's name. If omitted, reads the name out of the commit.
      filename: The filename. If omitted, not shown in the blame line.
    """
        short = self.repo[commit_name][:8]
        start = '%s %s' % (short, filename) if filename else short
        if author is None:
            author = self.repo.show_commit(commit_name, format_string='%an %ai')
        else:
            author += self.repo.show_commit(commit_name, format_string=' %ai')
        return ('%s (%s %s' % (start, author, rest)).encode('utf-8')


class GitHyperBlameMainTest(GitHyperBlameTestBase):
    """End-to-end tests on a very simple repo."""
    REPO_SCHEMA = "A B C D"

    COMMIT_A = {
        'some/files/file': {
            'data': b'line 1\nline 2\n'
        },
    }

    COMMIT_B = {
        'some/files/file': {
            'data': b'line 1\nline 2.1\n'
        },
    }

    COMMIT_C = {
        'some/files/file': {
            'data': b'line 1.1\nline 2.1\n'
        },
    }

    COMMIT_D = {
        # This file should be automatically considered for ignore.
        '.git-blame-ignore-revs': {
            'data': b'tag_C'
        },
        # This file should not be considered.
        'some/files/.git-blame-ignore-revs': {
            'data': b'tag_B'
        },
    }

    def setUp(self):
        super(GitHyperBlameMainTest, self).setUp()
        # Most tests want to check out C (so the .git-blame-ignore-revs is not
        # used).
        self.repo.git('checkout', '-f', 'tag_C')

    def testBasicBlame(self):
        """Tests the main function (simple end-to-end test with no ignores)."""
        expected_output = [
            self.blame_line('C', '1) line 1.1'),
            self.blame_line('B', '2) line 2.1')
        ]
        outbuf = BytesIO()
        retval = self.repo.run(self.git_hyper_blame.main,
                               ['tag_C', 'some/files/file'], outbuf)
        self.assertEqual(0, retval)
        self.assertEqual(expected_output,
                         outbuf.getvalue().rstrip().split(b'\n'))
        self.assertEqual('', sys.stderr.getvalue())

    def testIgnoreSimple(self):
        """Tests the main function (simple end-to-end test with ignores)."""
        expected_output = [
            self.blame_line('C', ' 1) line 1.1'),
            self.blame_line('A', '2*) line 2.1')
        ]
        outbuf = BytesIO()
        retval = self.repo.run(self.git_hyper_blame.main,
                               ['-i', 'tag_B', 'tag_C', 'some/files/file'],
                               outbuf)
        self.assertEqual(0, retval)
        self.assertEqual(expected_output,
                         outbuf.getvalue().rstrip().split(b'\n'))
        self.assertEqual('', sys.stderr.getvalue())

    def testBadRepo(self):
        """Tests the main function (not in a repo)."""
        # Make a temp dir that has no .git directory.
        curdir = os.getcwd()
        tempdir = tempfile.mkdtemp(suffix='_nogit', prefix='git_repo')
        try:
            os.chdir(tempdir)
            outbuf = BytesIO()
            retval = self.git_hyper_blame.main(
                ['-i', 'tag_B', 'tag_C', 'some/files/file'], outbuf)
        finally:
            os.chdir(curdir)
            shutil.rmtree(tempdir)

        self.assertNotEqual(0, retval)
        self.assertEqual(b'', outbuf.getvalue())
        r = re.compile('^fatal: Not a git repository', re.I)
        self.assertRegexpMatches(sys.stderr.getvalue(), r)

    def testBadFilename(self):
        """Tests the main function (bad filename)."""
        outbuf = BytesIO()
        retval = self.repo.run(self.git_hyper_blame.main,
                               ['-i', 'tag_B', 'tag_C', 'some/files/xxxx'],
                               outbuf)
        self.assertNotEqual(0, retval)
        self.assertEqual(b'', outbuf.getvalue())
        # TODO(mgiuca): This test used to test the exact string, but it broke
        # due to an upstream bug in git-blame. For now, just check the start of
        # the string. A patch has been sent upstream; when it rolls out we can
        # revert back to the original test logic.
        self.assertTrue(sys.stderr.getvalue().startswith(
            'fatal: no such path some/files/xxxx in '))

    def testBadRevision(self):
        """Tests the main function (bad revision to blame from)."""
        outbuf = BytesIO()
        retval = self.repo.run(self.git_hyper_blame.main,
                               ['-i', 'tag_B', 'xxxx', 'some/files/file'],
                               outbuf)
        self.assertNotEqual(0, retval)
        self.assertEqual(b'', outbuf.getvalue())
        self.assertRegexpMatches(
            sys.stderr.getvalue(),
            '^fatal: ambiguous argument \'xxxx\': unknown '
            'revision or path not in the working tree.')

    def testBadIgnore(self):
        """Tests the main function (bad revision passed to -i)."""
        expected_output = [
            self.blame_line('C', '1) line 1.1'),
            self.blame_line('B', '2) line 2.1')
        ]
        outbuf = BytesIO()
        retval = self.repo.run(self.git_hyper_blame.main,
                               ['-i', 'xxxx', 'tag_C', 'some/files/file'],
                               outbuf)
        self.assertEqual(0, retval)
        self.assertEqual(expected_output,
                         outbuf.getvalue().rstrip().split(b'\n'))
        self.assertEqual('warning: unknown revision \'xxxx\'.\n',
                         sys.stderr.getvalue())

    def testIgnoreFile(self):
        """Tests passing the ignore list in a file."""
        expected_output = [
            self.blame_line('C', ' 1) line 1.1'),
            self.blame_line('A', '2*) line 2.1')
        ]
        outbuf = BytesIO()

        with gclient_utils.temporary_file() as ignore_file:
            gclient_utils.FileWrite(
                ignore_file, '# Line comments are allowed.\n'
                '\n'
                '{}\n'
                'xxxx\n'.format(self.repo['B']))
            retval = self.repo.run(
                self.git_hyper_blame.main,
                ['--ignore-file', ignore_file, 'tag_C', 'some/files/file'],
                outbuf)

        self.assertEqual(0, retval)
        self.assertEqual(expected_output,
                         outbuf.getvalue().rstrip().split(b'\n'))
        self.assertEqual('warning: unknown revision \'xxxx\'.\n',
                         sys.stderr.getvalue())

    def testDefaultIgnoreFile(self):
        """Tests automatically using a default ignore list."""
        # Check out revision D. We expect the script to use the default ignore
        # list that is checked out, *not* the one committed at the given
        # revision.
        self.repo.git('checkout', '-f', 'tag_D')

        expected_output = [
            self.blame_line('A', '1*) line 1.1'),
            self.blame_line('B', ' 2) line 2.1')
        ]
        outbuf = BytesIO()

        retval = self.repo.run(self.git_hyper_blame.main,
                               ['tag_D', 'some/files/file'], outbuf)

        self.assertEqual(0, retval)
        self.assertEqual(expected_output,
                         outbuf.getvalue().rstrip().split(b'\n'))
        self.assertEqual('', sys.stderr.getvalue())

        # Test blame from a different revision. Despite the default ignore file
        # *not* being committed at that revision, it should still be picked up
        # because D is currently checked out.
        outbuf = BytesIO()

        retval = self.repo.run(self.git_hyper_blame.main,
                               ['tag_C', 'some/files/file'], outbuf)

        self.assertEqual(0, retval)
        self.assertEqual(expected_output,
                         outbuf.getvalue().rstrip().split(b'\n'))
        self.assertEqual('', sys.stderr.getvalue())

    def testNoDefaultIgnores(self):
        """Tests the --no-default-ignores switch."""
        # Check out revision D. This has a .git-blame-ignore-revs file, which we
        # expect to be ignored due to --no-default-ignores.
        self.repo.git('checkout', '-f', 'tag_D')

        expected_output = [
            self.blame_line('C', '1) line 1.1'),
            self.blame_line('B', '2) line 2.1')
        ]
        outbuf = BytesIO()

        retval = self.repo.run(
            self.git_hyper_blame.main,
            ['tag_D', 'some/files/file', '--no-default-ignores'], outbuf)

        self.assertEqual(0, retval)
        self.assertEqual(expected_output,
                         outbuf.getvalue().rstrip().split(b'\n'))
        self.assertEqual('', sys.stderr.getvalue())


class GitHyperBlameSimpleTest(GitHyperBlameTestBase):
    REPO_SCHEMA = """
  A B D E F G H
  A C D
  """

    COMMIT_A = {
        'some/files/file1': {
            'data': b'file1'
        },
        'some/files/file2': {
            'data': b'file2'
        },
        'some/files/empty': {
            'data': b''
        },
        'some/other/file': {
            'data': b'otherfile'
        },
    }

    COMMIT_B = {
        'some/files/file2': {
            'mode': 0o755,
            'data': b'file2 - vanilla\n'
        },
        'some/files/empty': {
            'data': b'not anymore'
        },
        'some/files/file3': {
            'data': b'file3'
        },
    }

    COMMIT_C = {
        'some/files/file2': {
            'data': b'file2 - merged\n'
        },
    }

    COMMIT_D = {
        'some/files/file2': {
            'data': b'file2 - vanilla\nfile2 - merged\n'
        },
    }

    COMMIT_E = {
        'some/files/file2': {
            'data': b'file2 - vanilla\nfile_x - merged\n'
        },
    }

    COMMIT_F = {
        'some/files/file2': {
            'data': b'file2 - vanilla\nfile_y - merged\n'
        },
    }

    # Move file2 from files to other.
    COMMIT_G = {
        'some/files/file2': {
            'data': None
        },
        'some/other/file2': {
            'data': b'file2 - vanilla\nfile_y - merged\n'
        },
    }

    COMMIT_H = {
        'some/other/file2': {
            'data': b'file2 - vanilla\nfile_z - merged\n'
        },
    }

    def testBlameError(self):
        """Tests a blame on a non-existent file."""
        expected_output = [b'']
        retval, output = self.run_hyperblame([], 'some/other/file2', 'tag_D')
        self.assertNotEqual(0, retval)
        self.assertEqual(expected_output, output)

    def testBlameEmpty(self):
        """Tests a blame of an empty file with no ignores."""
        expected_output = [b'']
        retval, output = self.run_hyperblame([], 'some/files/empty', 'tag_A')
        self.assertEqual(0, retval)
        self.assertEqual(expected_output, output)

    def testBasicBlame(self):
        """Tests a basic blame with no ignores."""
        # Expect to blame line 1 on B, line 2 on C.
        expected_output = [
            self.blame_line('B', '1) file2 - vanilla'),
            self.blame_line('C', '2) file2 - merged')
        ]
        retval, output = self.run_hyperblame([], 'some/files/file2', 'tag_D')
        self.assertEqual(0, retval)
        self.assertEqual(expected_output, output)

    def testBlameRenamed(self):
        """Tests a blame with no ignores on a renamed file."""
        # Expect to blame line 1 on B, line 2 on H.
        # Because the file has a different name than it had when (some of) these
        # lines were changed, expect the filenames to be displayed.
        expected_output = [
            self.blame_line('B',
                            '1) file2 - vanilla',
                            filename='some/files/file2'),
            self.blame_line('H',
                            '2) file_z - merged',
                            filename='some/other/file2')
        ]
        retval, output = self.run_hyperblame([], 'some/other/file2', 'tag_H')
        self.assertEqual(0, retval)
        self.assertEqual(expected_output, output)

    def testIgnoreSimpleEdits(self):
        """Tests a blame with simple (line-level changes) commits ignored."""
        # Expect to blame line 1 on B, line 2 on E.
        expected_output = [
            self.blame_line('B', '1) file2 - vanilla'),
            self.blame_line('E', '2) file_x - merged')
        ]
        retval, output = self.run_hyperblame([], 'some/files/file2', 'tag_E')
        self.assertEqual(0, retval)
        self.assertEqual(expected_output, output)

        # Ignore E; blame line 1 on B, line 2 on C.
        expected_output = [
            self.blame_line('B', ' 1) file2 - vanilla'),
            self.blame_line('C', '2*) file_x - merged')
        ]
        retval, output = self.run_hyperblame(['E'], 'some/files/file2', 'tag_E')
        self.assertEqual(0, retval)
        self.assertEqual(expected_output, output)

        # Ignore E and F; blame line 1 on B, line 2 on C.
        expected_output = [
            self.blame_line('B', ' 1) file2 - vanilla'),
            self.blame_line('C', '2*) file_y - merged')
        ]
        retval, output = self.run_hyperblame(['E', 'F'], 'some/files/file2',
                                             'tag_F')
        self.assertEqual(0, retval)
        self.assertEqual(expected_output, output)

    def testIgnoreInitialCommit(self):
        """Tests a blame with the initial commit ignored."""
        # Ignore A. Expect A to get blamed anyway.
        expected_output = [self.blame_line('A', '1) file1')]
        retval, output = self.run_hyperblame(['A'], 'some/files/file1', 'tag_A')
        self.assertEqual(0, retval)
        self.assertEqual(expected_output, output)

    def testIgnoreFileAdd(self):
        """Tests a blame ignoring the commit that added this file."""
        # Ignore A. Expect A to get blamed anyway.
        expected_output = [self.blame_line('B', '1) file3')]
        retval, output = self.run_hyperblame(['B'], 'some/files/file3', 'tag_B')
        self.assertEqual(0, retval)
        self.assertEqual(expected_output, output)

    def testIgnoreFilePopulate(self):
        """Tests a blame ignoring the commit that added data to an empty file."""
        # Ignore A. Expect A to get blamed anyway.
        expected_output = [self.blame_line('B', '1) not anymore')]
        retval, output = self.run_hyperblame(['B'], 'some/files/empty', 'tag_B')
        self.assertEqual(0, retval)
        self.assertEqual(expected_output, output)


class GitHyperBlameLineMotionTest(GitHyperBlameTestBase):
    REPO_SCHEMA = """
  A B C D E F
  """

    COMMIT_A = {
        'file': {
            'data': b'A\ngreen\nblue\n'
        },
    }

    # Change "green" to "yellow".
    COMMIT_B = {
        'file': {
            'data': b'A\nyellow\nblue\n'
        },
    }

    # Insert 2 lines at the top,
    # Change "yellow" to "red".
    # Insert 1 line at the bottom.
    COMMIT_C = {
        'file': {
            'data': b'X\nY\nA\nred\nblue\nZ\n'
        },
    }

    # Insert 2 more lines at the top.
    COMMIT_D = {
        'file': {
            'data': b'earth\nfire\nX\nY\nA\nred\nblue\nZ\n'
        },
    }

    # Insert a line before "red", and indent "red" and "blue".
    COMMIT_E = {
        'file': {
            'data': b'earth\nfire\nX\nY\nA\ncolors:\n red\n blue\nZ\n'
        },
    }

    # Insert a line between "A" and "colors".
    COMMIT_F = {
        'file': {
            'data': b'earth\nfire\nX\nY\nA\nB\ncolors:\n red\n blue\nZ\n'
        },
    }

    def testCacheDiffHunks(self):
        """Tests the cache_diff_hunks internal function."""
        expected_hunks = [
            ((0, 0), (1, 2)),
            ((2, 1), (4, 1)),
            ((3, 0), (6, 1)),
        ]
        hunks = self.repo.run(self.git_hyper_blame.cache_diff_hunks, 'tag_B',
                              'tag_C')
        self.assertEqual(expected_hunks, hunks)

    def testApproxLinenoAcrossRevs(self):
        """Tests the approx_lineno_across_revs internal function."""
        # Note: For all of these tests, the "old revision" and "new revision"
        # are reversed, which matches the usage by hyper_blame.

        # Test an unchanged line before any hunks in the diff. Should be
        # unchanged.
        lineno = self.repo.run(self.git_hyper_blame.approx_lineno_across_revs,
                               'file', 'file', 'tag_B', 'tag_A', 1)
        self.assertEqual(1, lineno)

        # Test an unchanged line after all hunks in the diff. Should be matched
        # to the line's previous position in the file.
        lineno = self.repo.run(self.git_hyper_blame.approx_lineno_across_revs,
                               'file', 'file', 'tag_D', 'tag_C', 6)
        self.assertEqual(4, lineno)

        # Test a line added in a new hunk. Should be matched to the line
        # *before* where the hunk was inserted in the old version of the file.
        lineno = self.repo.run(self.git_hyper_blame.approx_lineno_across_revs,
                               'file', 'file', 'tag_F', 'tag_E', 6)
        self.assertEqual(5, lineno)

        # Test lines added in a new hunk at the very start of the file. This
        # tests an edge case: normally it would be matched to the line *before*
        # where the hunk was inserted (Line 0), but since the hunk is at the
        # start of the file, we match to Line 1.
        lineno = self.repo.run(self.git_hyper_blame.approx_lineno_across_revs,
                               'file', 'file', 'tag_C', 'tag_B', 1)
        self.assertEqual(1, lineno)
        lineno = self.repo.run(self.git_hyper_blame.approx_lineno_across_revs,
                               'file', 'file', 'tag_C', 'tag_B', 2)
        self.assertEqual(1, lineno)

        # Test an unchanged line in between hunks in the diff. Should be matched
        # to the line's previous position in the file.
        lineno = self.repo.run(self.git_hyper_blame.approx_lineno_across_revs,
                               'file', 'file', 'tag_C', 'tag_B', 3)
        self.assertEqual(1, lineno)

        # Test a changed line. Should be matched to the hunk's previous position
        # in the file.
        lineno = self.repo.run(self.git_hyper_blame.approx_lineno_across_revs,
                               'file', 'file', 'tag_C', 'tag_B', 4)
        self.assertEqual(2, lineno)

        # Test a line added in a new hunk at the very end of the file. Should be
        # matched to the line *before* where the hunk was inserted (the last
        # line of the file). Technically same as the case above but good to
        # boundary test.
        lineno = self.repo.run(self.git_hyper_blame.approx_lineno_across_revs,
                               'file', 'file', 'tag_C', 'tag_B', 6)
        self.assertEqual(3, lineno)

    def testInterHunkLineMotion(self):
        """Tests a blame with line motion in another hunk in the ignored commit."""
        # Blame from D, ignoring C.

        # Lines 1, 2 were added by D.
        # Lines 3, 4 were added by C (but ignored, so blame A).
        # Line 5 was added by A.
        # Line 6 was modified by C (but ignored, so blame B). (Note: This
        # requires the algorithm to figure out that Line 6 in D == Line 4 in C
        # ~= Line 2 in B, so it blames B. Otherwise, it would blame A.) Line 7
        # was added by A. Line 8 was added by C (but ignored, so blame A).
        expected_output = [
            self.blame_line('D', ' 1) earth'),
            self.blame_line('D', ' 2) fire'),
            self.blame_line('A', '3*) X'),
            self.blame_line('A', '4*) Y'),
            self.blame_line('A', ' 5) A'),
            self.blame_line('B', '6*) red'),
            self.blame_line('A', ' 7) blue'),
            self.blame_line('A', '8*) Z'),
        ]
        retval, output = self.run_hyperblame(['C'], 'file', 'tag_D')
        self.assertEqual(0, retval)
        self.assertEqual(expected_output, output)

    def testIntraHunkLineMotion(self):
        """Tests a blame with line motion in the same hunk in the ignored commit."""
        # This test was mostly written as a demonstration of the limitations of
        # the current algorithm (it exhibits non-ideal behaviour).

        # Blame from E, ignoring E.
        # Line 6 was added by E (but ignored, so blame C).
        # Lines 7, 8 were modified by E (but ignored, so blame A).
        # TODO(mgiuca): Ideally, this would blame Line 7 on C, because the line
        # "red" was added by C, and this is just a small change to that line.
        # But the current algorithm can't deal with line motion within a hunk,
        # so it just assumes Line 7 in E ~= Line 7 in D == Line 3 in A (which
        # was "blue").
        expected_output = [
            self.blame_line('D', ' 1) earth'),
            self.blame_line('D', ' 2) fire'),
            self.blame_line('C', ' 3) X'),
            self.blame_line('C', ' 4) Y'),
            self.blame_line('A', ' 5) A'),
            self.blame_line('C', '6*) colors:'),
            self.blame_line('A', '7*)  red'),
            self.blame_line('A', '8*)  blue'),
            self.blame_line('C', ' 9) Z'),
        ]
        retval, output = self.run_hyperblame(['E'], 'file', 'tag_E')
        self.assertEqual(0, retval)
        self.assertEqual(expected_output, output)


class GitHyperBlameLineNumberTest(GitHyperBlameTestBase):
    REPO_SCHEMA = """
  A B C D
  """

    COMMIT_A = {
        'file': {
            'data': b'red\nblue\n'
        },
    }

    # Change "blue" to "green".
    COMMIT_B = {
        'file': {
            'data': b'red\ngreen\n'
        },
    }

    # Insert 2 lines at the top,
    COMMIT_C = {
        'file': {
            'data': b'\n\nred\ngreen\n'
        },
    }

    # Change "green" to "yellow".
    COMMIT_D = {
        'file': {
            'data': b'\n\nred\nyellow\n'
        },
    }

    def testTwoChangesWithAddedLines(self):
        """Regression test for https://crbug.com/709831.

    Tests a line with multiple ignored edits, and a line number change in
    between (such that the line number in the current revision is bigger than
    the file's line count at the older ignored revision).
    """
        expected_output = [
            self.blame_line('C', ' 1) '),
            self.blame_line('C', ' 2) '),
            self.blame_line('A', ' 3) red'),
            self.blame_line('A', '4*) yellow'),
        ]
        # Due to https://crbug.com/709831, ignoring both B and D would crash,
        # because of C (in between those revisions) which moves Line 2 to Line
        # 4. The algorithm would incorrectly think that Line 4 was still on Line
        # 4 in Commit B, even though it was Line 2 at that time. Its index is
        # out of range in the number of lines in Commit B.
        retval, output = self.run_hyperblame(['B', 'D'], 'file', 'tag_D')
        self.assertEqual(0, retval)
        self.assertEqual(expected_output, output)


class GitHyperBlameUnicodeTest(GitHyperBlameTestBase):
    REPO_SCHEMA = """
  A B C
  """

    COMMIT_A = {
        GitRepo.AUTHOR_NAME: 'ASCII Author',
        'file': {
            'data': b'red\nblue\n'
        },
    }

    # Add a line.
    COMMIT_B = {
        GitRepo.AUTHOR_NAME: '\u4e2d\u56fd\u4f5c\u8005',
        'file': {
            'data': b'red\ngreen\nblue\n'
        },
    }

    # Modify a line with non-UTF-8 author and file text.
    COMMIT_C = {
        GitRepo.AUTHOR_NAME: 'Lat\xedn-1 Author',
        'file': {
            'data': 'red\ngre\u00e9n\nblue\n'.encode('latin-1')
        },
    }

    def testNonUTF8Data(self):
        """Ensures correct behaviour even if author or file data is not UTF-8.

    There is no guarantee that a file will be UTF-8-encoded, so this is
    realistic.
    """
        expected_output = [
            self.blame_line('A', '1) red', author='ASCII Author  '),
            # The Author has been re-encoded as UTF-8. The file data is
            # converted to UTF8 and unknown characters replaced.
            self.blame_line('C', '2) gre\ufffdn', author='Lat\xedn-1 Author'),
            self.blame_line('A', '3) blue', author='ASCII Author  '),
        ]
        retval, output = self.run_hyperblame([], 'file', 'tag_C')
        self.assertEqual(0, retval)
        self.assertEqual(expected_output, output)


if __name__ == '__main__':
    sys.exit(
        coverage_utils.covered_main(
            os.path.join(DEPOT_TOOLS_ROOT, 'git_hyper_blame.py')))