blob: 44e1ff883f8b8520b8da8278c184346a8b40ecca [file] [log] [blame]
Dan McArdle3d6b8a22022-12-01 09:45:14 -05001# Copyright lowRISC contributors.
2# Licensed under the Apache License, Version 2.0, see LICENSE for details.
3# SPDX-License-Identifier: Apache-2.0
4
5import unittest
6from typing import Tuple
7from unittest.mock import MagicMock, call
8
9from bitstream_bisect import (BisectLog, BisectSession, CommitHash,
10 CommitJudgment, GitControllerABC)
11
12
13class MockableGitController(GitControllerABC):
14 """Provides stub implementations of GitControllerABC's abstract methods."""
15
16 def _git(self, *args: str) -> Tuple[int, str, str]:
17 raise Exception("_git unexpectedly called")
18
19 def _git_no_capture_output(self, *args: str) -> int:
20 returncode, _, _ = self._git(*args)
21 return returncode
22
23
24class TestCommitHash(unittest.TestCase):
25 """Test that CommitHash validates its inputs."""
26
27 def test_simple(self):
28 c = CommitHash("c1")
29 self.assertEqual(str(c), "c1")
30
31 c = CommitHash("deadbeef")
32 self.assertEqual(str(c), "deadbeef")
33 self.assertEqual(str(c), CommitHash("deadbeef"))
34
35 def test_non_lowercase(self):
36 c = CommitHash("C1")
37 self.assertEqual(str(c), "c1")
38 self.assertEqual(str(c), CommitHash("C1"))
39 self.assertEqual(str(c), CommitHash("c1"))
40
41 c = CommitHash("DEADBEEF")
42 self.assertEqual(str(c), "deadbeef")
43 self.assertEqual(str(c), CommitHash("DEADBEEF"))
44 self.assertEqual(str(c), CommitHash("deadbeef"))
45
46 c = CommitHash("DeAdBeEf")
47 self.assertEqual(str(c), "deadbeef")
48 self.assertEqual(str(c), CommitHash("DeAdBeEf"))
49 self.assertEqual(str(c), CommitHash("deadbeef"))
50
51 def test_invalid(self):
52 with self.assertRaises(Exception):
53 CommitHash("")
54 with self.assertRaises(Exception):
55 CommitHash("hello")
56 with self.assertRaises(Exception):
57 CommitHash("HEAD")
58
59
60class TestParseBisectLog(unittest.TestCase):
61
62 def test_one_line(self):
63 bisect_log = BisectLog("git bisect good c123")
64 self.assertEqual(bisect_log.judgments, [('c123', CommitJudgment.GOOD)])
65 self.assertEqual(bisect_log.candidates, [])
66
67 bisect_log = BisectLog("git bisect bad c123")
68 self.assertEqual(bisect_log.judgments, [('c123', CommitJudgment.BAD)])
69 self.assertEqual(bisect_log.candidates, [])
70
71 bisect_log = BisectLog("git bisect skip c123")
72 self.assertEqual(bisect_log.judgments, [('c123', CommitJudgment.SKIP)])
73 self.assertEqual(bisect_log.candidates, [])
74
75 bisect_log = BisectLog("")
76 self.assertEqual(bisect_log.judgments, [])
77 self.assertEqual(bisect_log.candidates, [])
78
79 bisect_log = BisectLog("git bisect start")
80 self.assertEqual(bisect_log.judgments, [])
81 self.assertEqual(bisect_log.candidates, [])
82
83 def test_unrecognized_actions_rejected(self):
84 """The parser should reject non-comment lines it doesn't understand."""
85 # Unexpected whitespace.
86 with self.assertRaises(BisectLog.ParserBug):
87 _ = BisectLog(" git bisect skip c123")
88 with self.assertRaises(BisectLog.ParserBug):
89 _ = BisectLog("git bisect skip c123")
90 with self.assertRaises(BisectLog.ParserBug):
91 _ = BisectLog("git bisect skip c123")
92 with self.assertRaises(BisectLog.ParserBug):
93 _ = BisectLog(" # git bisect skip c123")
94 # Unrecognized action with commit.
95 with self.assertRaises(BisectLog.ParserBug):
96 _ = BisectLog("git bisect foo c123")
97 # Unrecognized action without commit.
98 with self.assertRaises(BisectLog.ParserBug):
99 _ = BisectLog("git bisect bar")
100
101 def test_unrecognized_comments_ignored(self):
102 bisect_log = BisectLog("# foo")
103 self.assertEqual(bisect_log.judgments, [])
104 self.assertEqual(bisect_log.candidates, [])
105
106 def test_candidates_simple(self):
107 bisect_log = BisectLog("")
108 self.assertEqual(bisect_log.judgments, [])
109 self.assertEqual(bisect_log.candidates, [])
110
111 bisect_log = BisectLog(
112 "# first bad commit: [c42] Commit description...")
113 self.assertEqual(bisect_log.judgments, [])
114 self.assertEqual(bisect_log.candidates, ["c42"])
115
116 bisect_log = BisectLog("""
117# only skipped commits left to test
118# possible first bad commit: [c123] Foo description
119# possible first bad commit: [c234] Bar description
120""")
121 self.assertEqual(bisect_log.judgments, [])
122 self.assertEqual(bisect_log.candidates, ["c123", "c234"])
123
124 def test_realistic_only_skipped_remain(self):
125 # This output was produced by `git` on a temporary repo.
126 BISECT_LOG = """# status: waiting for both good and bad commits
127# good: [0b47ff5f2ac78589cab7d9b93c4cabb9e4a1f736] First commit
128git bisect good 0b47ff5f2ac78589cab7d9b93c4cabb9e4a1f736
129# status: waiting for bad commit, 1 good commit known
130# bad: [4f166216806f44b1ddf67c4649d5e646cf9bbf22] Fourth commit
131git bisect bad 4f166216806f44b1ddf67c4649d5e646cf9bbf22
132# skip: [132c4e1f8773cd3e14e2f4de8a8b77422b74e28b] Third commit
133git bisect skip 132c4e1f8773cd3e14e2f4de8a8b77422b74e28b
134# skip: [8fe76ab59c4f7d43237292a57f1d84b774436224] Second commit
135git bisect skip 8fe76ab59c4f7d43237292a57f1d84b774436224
136# only skipped commits left to test
137# possible first bad commit: [4f166216806f44b1ddf67c4649d5e646cf9bbf22] Fourth commit
138# possible first bad commit: [132c4e1f8773cd3e14e2f4de8a8b77422b74e28b] Third commit
139# possible first bad commit: [8fe76ab59c4f7d43237292a57f1d84b774436224] Second commit
140# good: [8fe76ab59c4f7d43237292a57f1d84b774436224] Second commit
141git bisect good 8fe76ab59c4f7d43237292a57f1d84b774436224
142# only skipped commits left to test
143# possible first bad commit: [4f166216806f44b1ddf67c4649d5e646cf9bbf22] Fourth commit
144# possible first bad commit: [132c4e1f8773cd3e14e2f4de8a8b77422b74e28b] Third commit
145"""
146 bisect_log = BisectLog(BISECT_LOG)
147 self.assertEqual(bisect_log.candidates, [
148 "4f166216806f44b1ddf67c4649d5e646cf9bbf22",
149 "132c4e1f8773cd3e14e2f4de8a8b77422b74e28b"
150 ])
151 self.assertEqual(bisect_log.judgments, [
152 ("0b47ff5f2ac78589cab7d9b93c4cabb9e4a1f736", CommitJudgment.GOOD),
153 ("4f166216806f44b1ddf67c4649d5e646cf9bbf22", CommitJudgment.BAD),
154 ("132c4e1f8773cd3e14e2f4de8a8b77422b74e28b", CommitJudgment.SKIP),
155 ("8fe76ab59c4f7d43237292a57f1d84b774436224", CommitJudgment.SKIP),
156 ("8fe76ab59c4f7d43237292a57f1d84b774436224", CommitJudgment.GOOD),
157 ])
158
159 def test_realistic_has_first_bad_commit(self):
160 # This output was produced by `git` on a temporary repo.
161 BISECT_LOG = """git bisect start
162# status: waiting for both good and bad commits
163# good: [00722da469712cd917e59082601d189fe0c6507e] First commit
164git bisect good 00722da469712cd917e59082601d189fe0c6507e
165# status: waiting for bad commit, 1 good commit known
166# bad: [3e2031c1cb7b27e8bf36c85756c301279b8f9f30] Fourth commit
167git bisect bad 3e2031c1cb7b27e8bf36c85756c301279b8f9f30
168# bad: [dc5320f6d974d3fce7bcc03fd18d920c8b5e3cbf] Third commit
169git bisect bad dc5320f6d974d3fce7bcc03fd18d920c8b5e3cbf
170# bad: [d72b7491e6fdadb2ef742370410d5488a7bbfdba] Second commit
171git bisect bad d72b7491e6fdadb2ef742370410d5488a7bbfdba
172# first bad commit: [d72b7491e6fdadb2ef742370410d5488a7bbfdba] Second commit
173"""
174 bisect_log = BisectLog(BISECT_LOG)
175 self.assertEqual(bisect_log.candidates,
176 ["d72b7491e6fdadb2ef742370410d5488a7bbfdba"])
177 self.assertEqual(bisect_log.judgments, [
178 ("00722da469712cd917e59082601d189fe0c6507e", CommitJudgment.GOOD),
179 ("3e2031c1cb7b27e8bf36c85756c301279b8f9f30", CommitJudgment.BAD),
180 ("dc5320f6d974d3fce7bcc03fd18d920c8b5e3cbf", CommitJudgment.BAD),
181 ("d72b7491e6fdadb2ef742370410d5488a7bbfdba", CommitJudgment.BAD),
182 ])
183
184
185class TestBisectSession(unittest.TestCase):
186
187 def test_two_cached_only_fast(self):
188 """Simple case with two cached commits and only the fast command."""
189
190 BISECT_LOG = """git bisect start
191# status: waiting for both good and bad commits
192# good: [c1] Fake good commit
193git bisect good c1
194# status: waiting for bad commit, 1 good commit known
195# bad: [c2] Fake bad commit
196git bisect bad c2
197# first bad commit: [c2] Fake bad commit
198"""
199
200 git = MockableGitController()
201 git._git = MagicMock(return_value=(0, "", ""))
202 git.bisect_view = MagicMock(return_value=[
203 (CommitHash("c1"), "Commit description"),
204 (CommitHash("c2"), "Commit description"),
205 ])
206 git.bisect_log = MagicMock(return_value=BisectLog(BISECT_LOG))
207
208 session = BisectSession(git, cache_keys=set(["c1", "c2"]))
209 result: BisectLog = session.run("c1", "c2", ["fast_script.sh"])
210
211 self.assertEqual(result.candidates, ["c2"])
212 git._git.assert_has_calls([
213 call("bisect", "start"),
214 call("bisect", "good", "c1"),
215 call("bisect", "bad", "c2"),
216 call('bisect', 'run', 'fast_script.sh'),
217 # We would expect `call('bisect', 'log')` here, but mocking
218 # `git.bisect_log()` prevented it from calling `git._git()`.
219 call('bisect', 'reset'),
220 ])
221 git.bisect_log.assert_called_once_with()
222 git.bisect_view.assert_called_once_with()
223
224 def test_only_fast(self):
225 """Mix of cached and uncached with only the fast command."""
226
227 BISECT_LOG = """git bisect start
228# status: waiting for both good and bad commits
229# skip: [c2] Skipped commit
230git bisect skip c2
231# good: [c1] Fake good commit
232git bisect good c1
233# status: waiting for bad commit, 1 good commit known
234# bad: [c3] Fake bad commit
235git bisect bad c3
236# only skipped commits left to test
237# possible first bad commit: [c2] Skipped commit
238# possible first bad commit: [c3] Fake bad commit
239"""
240
241 git = MockableGitController()
242 git._git = MagicMock(return_value=(0, "", ""))
243 git.bisect_view = MagicMock(return_value=[
244 (CommitHash("c1"), "Fake good commit"),
245 (CommitHash("c2"), "Skipped commit"),
246 (CommitHash("c3"), "Fake bad commit"),
247 ])
248
249 git.bisect_log = MagicMock(return_value=BisectLog(BISECT_LOG))
250
251 session = BisectSession(git, cache_keys=set(["c1", "c3"]))
252 bisect_log = session.run("c1", "c3", ["fast_script.sh"])
253
254 self.assertEqual(bisect_log.candidates, ["c2", "c3"])
255 git._git.assert_has_calls([
256 call("bisect", "start"),
257 call("bisect", "good", "c1"),
258 call("bisect", "bad", "c3"),
259 call("bisect", "skip", "c2"),
260 call('bisect', 'run', 'fast_script.sh'),
261 # We would expect `call('bisect', 'log')` here, but mocking
262 # `git.bisect_log()` prevented it from calling `git._git()`.
263 ])
264 git.bisect_log.assert_called_once_with()
265 git.bisect_view.assert_called_once_with()
266
267 def test_fast_and_slow(self):
268 """Mix of cached and uncached with fast and slow command."""
269
270 BISECT_LOG_1 = """git bisect start
271# status: waiting for both good and bad commits
272# good: [c1] Commit 1
273git bisect good c1
274# status: waiting for bad commit, 1 good commit known
275# bad: [c5] Commit 5
276git bisect bad c5
277# skip: [c3] Commit 3
278git bisect skip c3
279# skip: [c4] Commit 4
280git bisect skip c4
281# good: [c2] Commit 2
282git bisect good c2
283# only skipped commits left to test
284# possible first bad commit: [c3] Commit 3
285# possible first bad commit: [c4] Commit 4
286"""
287 BISECT_LOG_2 = """git bisect start
288# status: waiting for both good and bad commits
289# good: [c1] Commit 1
290git bisect good c1
291# status: waiting for bad commit, 1 good commit known
292# bad: [c5] Commit 5
293git bisect bad c5
294# good: [c2] Commit 2
295git bisect good c2
296# good: [c3] Commit 3
297git bisect good c3
298# bad: [c4] Commit 4
299git bisect bad c4
300# first bad commit: [c4] Commit 4
301"""
302 parsed_bisect_logs = [BisectLog(BISECT_LOG_1), BisectLog(BISECT_LOG_2)]
303
304 git = MockableGitController()
305 git._git = MagicMock(return_value=(0, "", ""))
306 git.bisect_log = MagicMock(side_effect=parsed_bisect_logs)
307
308 git.bisect_view = MagicMock(return_value=[
309 (CommitHash("c1"), "Commit 1"),
310 (CommitHash("c2"), "Commit 2"),
311 (CommitHash("c3"), "Commit 3"),
312 (CommitHash("c4"), "Commit 4"),
313 (CommitHash("c5"), "Commit 5"),
314 ])
315
316 session = BisectSession(git, cache_keys=set(["c1", "c2"]))
317 bisect_log = session.run("c1", "c5", ["fast_script.sh"],
318 ["slow_script.sh"])
319
320 self.assertEqual(bisect_log.candidates, ["c4"])
321
322 git._git.assert_has_calls([
323 call("bisect", "start"),
324 call("bisect", "good", "c1"),
325 call("bisect", "bad", "c5"),
Dan McArdle38940812022-12-21 10:18:55 -0500326 call("bisect", "skip", "c3", "c4", "c5"),
Dan McArdle3d6b8a22022-12-01 09:45:14 -0500327 call('bisect', 'run', 'fast_script.sh'),
328 call('bisect', 'reset'),
329 call('bisect', 'start'),
330 call('bisect', 'good', 'c1'),
331 call('bisect', 'bad', 'c5'),
332 call('bisect', 'good', 'c2'),
333 call('bisect', 'run', 'slow_script.sh'),
334 ])
335 git.bisect_log.assert_has_calls([call(), call()])
336 git.bisect_view.assert_called_once_with()
337
338
339if __name__ == '__main__':
340 unittest.main()