| # 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() |