| # Copyright lowRISC contributors. |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| import unittest |
| from typing import Tuple |
| from unittest.mock import MagicMock, call |
| |
| from bitstream_bisect import (BisectLog, BisectSession, CommitHash, |
| CommitJudgment, GitControllerABC) |
| |
| |
| class MockableGitController(GitControllerABC): |
| """Provides stub implementations of GitControllerABC's abstract methods.""" |
| |
| def _git(self, *args: str) -> Tuple[int, str, str]: |
| raise Exception("_git unexpectedly called") |
| |
| def _git_no_capture_output(self, *args: str) -> int: |
| returncode, _, _ = self._git(*args) |
| return returncode |
| |
| |
| class TestCommitHash(unittest.TestCase): |
| """Test that CommitHash validates its inputs.""" |
| |
| def test_simple(self): |
| c = CommitHash("c1") |
| self.assertEqual(str(c), "c1") |
| |
| c = CommitHash("deadbeef") |
| self.assertEqual(str(c), "deadbeef") |
| self.assertEqual(str(c), CommitHash("deadbeef")) |
| |
| def test_non_lowercase(self): |
| c = CommitHash("C1") |
| self.assertEqual(str(c), "c1") |
| self.assertEqual(str(c), CommitHash("C1")) |
| self.assertEqual(str(c), CommitHash("c1")) |
| |
| c = CommitHash("DEADBEEF") |
| self.assertEqual(str(c), "deadbeef") |
| self.assertEqual(str(c), CommitHash("DEADBEEF")) |
| self.assertEqual(str(c), CommitHash("deadbeef")) |
| |
| c = CommitHash("DeAdBeEf") |
| self.assertEqual(str(c), "deadbeef") |
| self.assertEqual(str(c), CommitHash("DeAdBeEf")) |
| self.assertEqual(str(c), CommitHash("deadbeef")) |
| |
| def test_invalid(self): |
| with self.assertRaises(Exception): |
| CommitHash("") |
| with self.assertRaises(Exception): |
| CommitHash("hello") |
| with self.assertRaises(Exception): |
| CommitHash("HEAD") |
| |
| |
| class TestParseBisectLog(unittest.TestCase): |
| |
| def test_one_line(self): |
| bisect_log = BisectLog("git bisect good c123") |
| self.assertEqual(bisect_log.judgments, [('c123', CommitJudgment.GOOD)]) |
| self.assertEqual(bisect_log.candidates, []) |
| |
| bisect_log = BisectLog("git bisect bad c123") |
| self.assertEqual(bisect_log.judgments, [('c123', CommitJudgment.BAD)]) |
| self.assertEqual(bisect_log.candidates, []) |
| |
| bisect_log = BisectLog("git bisect skip c123") |
| self.assertEqual(bisect_log.judgments, [('c123', CommitJudgment.SKIP)]) |
| self.assertEqual(bisect_log.candidates, []) |
| |
| bisect_log = BisectLog("") |
| self.assertEqual(bisect_log.judgments, []) |
| self.assertEqual(bisect_log.candidates, []) |
| |
| bisect_log = BisectLog("git bisect start") |
| self.assertEqual(bisect_log.judgments, []) |
| self.assertEqual(bisect_log.candidates, []) |
| |
| def test_unrecognized_actions_rejected(self): |
| """The parser should reject non-comment lines it doesn't understand.""" |
| # Unexpected whitespace. |
| with self.assertRaises(BisectLog.ParserBug): |
| _ = BisectLog(" git bisect skip c123") |
| with self.assertRaises(BisectLog.ParserBug): |
| _ = BisectLog("git bisect skip c123") |
| with self.assertRaises(BisectLog.ParserBug): |
| _ = BisectLog("git bisect skip c123") |
| with self.assertRaises(BisectLog.ParserBug): |
| _ = BisectLog(" # git bisect skip c123") |
| # Unrecognized action with commit. |
| with self.assertRaises(BisectLog.ParserBug): |
| _ = BisectLog("git bisect foo c123") |
| # Unrecognized action without commit. |
| with self.assertRaises(BisectLog.ParserBug): |
| _ = BisectLog("git bisect bar") |
| |
| def test_unrecognized_comments_ignored(self): |
| bisect_log = BisectLog("# foo") |
| self.assertEqual(bisect_log.judgments, []) |
| self.assertEqual(bisect_log.candidates, []) |
| |
| def test_candidates_simple(self): |
| bisect_log = BisectLog("") |
| self.assertEqual(bisect_log.judgments, []) |
| self.assertEqual(bisect_log.candidates, []) |
| |
| bisect_log = BisectLog( |
| "# first bad commit: [c42] Commit description...") |
| self.assertEqual(bisect_log.judgments, []) |
| self.assertEqual(bisect_log.candidates, ["c42"]) |
| |
| bisect_log = BisectLog(""" |
| # only skipped commits left to test |
| # possible first bad commit: [c123] Foo description |
| # possible first bad commit: [c234] Bar description |
| """) |
| self.assertEqual(bisect_log.judgments, []) |
| self.assertEqual(bisect_log.candidates, ["c123", "c234"]) |
| |
| def test_realistic_only_skipped_remain(self): |
| # This output was produced by `git` on a temporary repo. |
| BISECT_LOG = """# status: waiting for both good and bad commits |
| # good: [0b47ff5f2ac78589cab7d9b93c4cabb9e4a1f736] First commit |
| git bisect good 0b47ff5f2ac78589cab7d9b93c4cabb9e4a1f736 |
| # status: waiting for bad commit, 1 good commit known |
| # bad: [4f166216806f44b1ddf67c4649d5e646cf9bbf22] Fourth commit |
| git bisect bad 4f166216806f44b1ddf67c4649d5e646cf9bbf22 |
| # skip: [132c4e1f8773cd3e14e2f4de8a8b77422b74e28b] Third commit |
| git bisect skip 132c4e1f8773cd3e14e2f4de8a8b77422b74e28b |
| # skip: [8fe76ab59c4f7d43237292a57f1d84b774436224] Second commit |
| git bisect skip 8fe76ab59c4f7d43237292a57f1d84b774436224 |
| # only skipped commits left to test |
| # possible first bad commit: [4f166216806f44b1ddf67c4649d5e646cf9bbf22] Fourth commit |
| # possible first bad commit: [132c4e1f8773cd3e14e2f4de8a8b77422b74e28b] Third commit |
| # possible first bad commit: [8fe76ab59c4f7d43237292a57f1d84b774436224] Second commit |
| # good: [8fe76ab59c4f7d43237292a57f1d84b774436224] Second commit |
| git bisect good 8fe76ab59c4f7d43237292a57f1d84b774436224 |
| # only skipped commits left to test |
| # possible first bad commit: [4f166216806f44b1ddf67c4649d5e646cf9bbf22] Fourth commit |
| # possible first bad commit: [132c4e1f8773cd3e14e2f4de8a8b77422b74e28b] Third commit |
| """ |
| bisect_log = BisectLog(BISECT_LOG) |
| self.assertEqual(bisect_log.candidates, [ |
| "4f166216806f44b1ddf67c4649d5e646cf9bbf22", |
| "132c4e1f8773cd3e14e2f4de8a8b77422b74e28b" |
| ]) |
| self.assertEqual(bisect_log.judgments, [ |
| ("0b47ff5f2ac78589cab7d9b93c4cabb9e4a1f736", CommitJudgment.GOOD), |
| ("4f166216806f44b1ddf67c4649d5e646cf9bbf22", CommitJudgment.BAD), |
| ("132c4e1f8773cd3e14e2f4de8a8b77422b74e28b", CommitJudgment.SKIP), |
| ("8fe76ab59c4f7d43237292a57f1d84b774436224", CommitJudgment.SKIP), |
| ("8fe76ab59c4f7d43237292a57f1d84b774436224", CommitJudgment.GOOD), |
| ]) |
| |
| def test_realistic_has_first_bad_commit(self): |
| # This output was produced by `git` on a temporary repo. |
| BISECT_LOG = """git bisect start |
| # status: waiting for both good and bad commits |
| # good: [00722da469712cd917e59082601d189fe0c6507e] First commit |
| git bisect good 00722da469712cd917e59082601d189fe0c6507e |
| # status: waiting for bad commit, 1 good commit known |
| # bad: [3e2031c1cb7b27e8bf36c85756c301279b8f9f30] Fourth commit |
| git bisect bad 3e2031c1cb7b27e8bf36c85756c301279b8f9f30 |
| # bad: [dc5320f6d974d3fce7bcc03fd18d920c8b5e3cbf] Third commit |
| git bisect bad dc5320f6d974d3fce7bcc03fd18d920c8b5e3cbf |
| # bad: [d72b7491e6fdadb2ef742370410d5488a7bbfdba] Second commit |
| git bisect bad d72b7491e6fdadb2ef742370410d5488a7bbfdba |
| # first bad commit: [d72b7491e6fdadb2ef742370410d5488a7bbfdba] Second commit |
| """ |
| bisect_log = BisectLog(BISECT_LOG) |
| self.assertEqual(bisect_log.candidates, |
| ["d72b7491e6fdadb2ef742370410d5488a7bbfdba"]) |
| self.assertEqual(bisect_log.judgments, [ |
| ("00722da469712cd917e59082601d189fe0c6507e", CommitJudgment.GOOD), |
| ("3e2031c1cb7b27e8bf36c85756c301279b8f9f30", CommitJudgment.BAD), |
| ("dc5320f6d974d3fce7bcc03fd18d920c8b5e3cbf", CommitJudgment.BAD), |
| ("d72b7491e6fdadb2ef742370410d5488a7bbfdba", CommitJudgment.BAD), |
| ]) |
| |
| |
| class TestBisectSession(unittest.TestCase): |
| |
| def test_two_cached_only_fast(self): |
| """Simple case with two cached commits and only the fast command.""" |
| |
| BISECT_LOG = """git bisect start |
| # status: waiting for both good and bad commits |
| # good: [c1] Fake good commit |
| git bisect good c1 |
| # status: waiting for bad commit, 1 good commit known |
| # bad: [c2] Fake bad commit |
| git bisect bad c2 |
| # first bad commit: [c2] Fake bad commit |
| """ |
| |
| git = MockableGitController() |
| git._git = MagicMock(return_value=(0, "", "")) |
| git.bisect_view = MagicMock(return_value=[ |
| (CommitHash("c1"), "Commit description"), |
| (CommitHash("c2"), "Commit description"), |
| ]) |
| git.bisect_log = MagicMock(return_value=BisectLog(BISECT_LOG)) |
| |
| session = BisectSession(git, cache_keys=set(["c1", "c2"])) |
| result: BisectLog = session.run("c1", "c2", ["fast_script.sh"]) |
| |
| self.assertEqual(result.candidates, ["c2"]) |
| git._git.assert_has_calls([ |
| call("bisect", "start"), |
| call("bisect", "good", "c1"), |
| call("bisect", "bad", "c2"), |
| call('bisect', 'run', 'fast_script.sh'), |
| # We would expect `call('bisect', 'log')` here, but mocking |
| # `git.bisect_log()` prevented it from calling `git._git()`. |
| call('bisect', 'reset'), |
| ]) |
| git.bisect_log.assert_called_once_with() |
| git.bisect_view.assert_called_once_with() |
| |
| def test_only_fast(self): |
| """Mix of cached and uncached with only the fast command.""" |
| |
| BISECT_LOG = """git bisect start |
| # status: waiting for both good and bad commits |
| # skip: [c2] Skipped commit |
| git bisect skip c2 |
| # good: [c1] Fake good commit |
| git bisect good c1 |
| # status: waiting for bad commit, 1 good commit known |
| # bad: [c3] Fake bad commit |
| git bisect bad c3 |
| # only skipped commits left to test |
| # possible first bad commit: [c2] Skipped commit |
| # possible first bad commit: [c3] Fake bad commit |
| """ |
| |
| git = MockableGitController() |
| git._git = MagicMock(return_value=(0, "", "")) |
| git.bisect_view = MagicMock(return_value=[ |
| (CommitHash("c1"), "Fake good commit"), |
| (CommitHash("c2"), "Skipped commit"), |
| (CommitHash("c3"), "Fake bad commit"), |
| ]) |
| |
| git.bisect_log = MagicMock(return_value=BisectLog(BISECT_LOG)) |
| |
| session = BisectSession(git, cache_keys=set(["c1", "c3"])) |
| bisect_log = session.run("c1", "c3", ["fast_script.sh"]) |
| |
| self.assertEqual(bisect_log.candidates, ["c2", "c3"]) |
| git._git.assert_has_calls([ |
| call("bisect", "start"), |
| call("bisect", "good", "c1"), |
| call("bisect", "bad", "c3"), |
| call("bisect", "skip", "c2"), |
| call('bisect', 'run', 'fast_script.sh'), |
| # We would expect `call('bisect', 'log')` here, but mocking |
| # `git.bisect_log()` prevented it from calling `git._git()`. |
| ]) |
| git.bisect_log.assert_called_once_with() |
| git.bisect_view.assert_called_once_with() |
| |
| def test_fast_and_slow(self): |
| """Mix of cached and uncached with fast and slow command.""" |
| |
| BISECT_LOG_1 = """git bisect start |
| # status: waiting for both good and bad commits |
| # good: [c1] Commit 1 |
| git bisect good c1 |
| # status: waiting for bad commit, 1 good commit known |
| # bad: [c5] Commit 5 |
| git bisect bad c5 |
| # skip: [c3] Commit 3 |
| git bisect skip c3 |
| # skip: [c4] Commit 4 |
| git bisect skip c4 |
| # good: [c2] Commit 2 |
| git bisect good c2 |
| # only skipped commits left to test |
| # possible first bad commit: [c3] Commit 3 |
| # possible first bad commit: [c4] Commit 4 |
| """ |
| BISECT_LOG_2 = """git bisect start |
| # status: waiting for both good and bad commits |
| # good: [c1] Commit 1 |
| git bisect good c1 |
| # status: waiting for bad commit, 1 good commit known |
| # bad: [c5] Commit 5 |
| git bisect bad c5 |
| # good: [c2] Commit 2 |
| git bisect good c2 |
| # good: [c3] Commit 3 |
| git bisect good c3 |
| # bad: [c4] Commit 4 |
| git bisect bad c4 |
| # first bad commit: [c4] Commit 4 |
| """ |
| parsed_bisect_logs = [BisectLog(BISECT_LOG_1), BisectLog(BISECT_LOG_2)] |
| |
| git = MockableGitController() |
| git._git = MagicMock(return_value=(0, "", "")) |
| git.bisect_log = MagicMock(side_effect=parsed_bisect_logs) |
| |
| git.bisect_view = MagicMock(return_value=[ |
| (CommitHash("c1"), "Commit 1"), |
| (CommitHash("c2"), "Commit 2"), |
| (CommitHash("c3"), "Commit 3"), |
| (CommitHash("c4"), "Commit 4"), |
| (CommitHash("c5"), "Commit 5"), |
| ]) |
| |
| session = BisectSession(git, cache_keys=set(["c1", "c2"])) |
| bisect_log = session.run("c1", "c5", ["fast_script.sh"], |
| ["slow_script.sh"]) |
| |
| self.assertEqual(bisect_log.candidates, ["c4"]) |
| |
| git._git.assert_has_calls([ |
| call("bisect", "start"), |
| call("bisect", "good", "c1"), |
| call("bisect", "bad", "c5"), |
| call("bisect", "skip", "c3", "c4", "c5"), |
| call('bisect', 'run', 'fast_script.sh'), |
| call('bisect', 'reset'), |
| call('bisect', 'start'), |
| call('bisect', 'good', 'c1'), |
| call('bisect', 'bad', 'c5'), |
| call('bisect', 'good', 'c2'), |
| call('bisect', 'run', 'slow_script.sh'), |
| ]) |
| git.bisect_log.assert_has_calls([call(), call()]) |
| git.bisect_view.assert_called_once_with() |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |