איך למצוא באגים אוטומטית עם git bisect

16/09/2018
git

פוסט זה כולל טיפ קצר לעבודה יעילה יותר עם Git.
בשביל להבין את גיט לעומק אני ממליץ על קורס גיט שלנו כאן באתר.
בקורס תלמדו איך גיט עובד מתחת למכסה המנוע ואיך להשתמש בו בצורה יעילה מהבסיס ועד לנושאים המתקדמים.

אתם כבר עובדים בגיט? מעולה! עכשיו כדאי שתכירו את הפקודה git bisect שתעזור לכם למצוא באגים באופן אוטומטי בתוכניות שלכם.

1. איך זה עובד

הרבה פעמים אנחנו עובדים על קוד ומגלים לפתע באג בחלק בקוד שבכלל לא קשור לדברים שהרגע עבדנו עליהם. זה קורה בגלל שעידכון ישן שינה התנהגות של אותו חלק בקוד אבל אותו מתכנת שביצע את העדכון לא ממש בדק שהתיקון שלו נכון ולכן הכניס באג בלי להתכוון.

הדבר הראשון שכדאי לעשות במצב כזה הוא לייצר בדיקה אוטומטית שמזהה האם הבאג קיים במערכת. זה יעזור לנו אחרי שנתקן את הבאג לוודא שבאמת תוקן וגם נוכל לשמור את הבדיקה במערכת וכך לוודא שהבאג גם לא יחזור בעתיד.

בפוסט זה אני רוצה להראות עוד ייתרון של שיטת עבודה זו והוא השימוש בבדיקה כדי לזהות איפה בדיוק נכנס הבאג למערכת ואיזה שינוי הביא אותו. הפקודה בה נשתמש כדי לעשות את זה נקראת git bisect.

הפקודה git bisect עושה דבר מאוד פשוט: היא מקבלת מספר גירסא מההיסטוריה בה הכל עבד כמו שצריך ועוד מספר גירסא יחסית עדכני בה משהו שבור וסקריפט שיודע לבדוק אם גירסא מסוימת תקינה או שבורה. עכשיו לאט לאט היא תתחיל לבדוק באופן אוטומטי גירסאות עד שמוצאת את הגירסא הראשונה בה הקוד היה שבור. החיפוש של גיט בייסקט מתבצע בשיטת אריה במדבר כך שהוא ייקח רק מספר לוגריתמי של בדיקות.

בשביל להפעיל את git bisect נבצע את הצעדים הבאים:

  1. תחילה נפתח ענף חדש ובו נכתוב את הבדיקה שלנו שמזהה האם הבאג קיים במערכת.

  2. לאחר מכן נכתוב קובץ Shell Script שלוקח את קובץ הבדיקות מאותו ענף נפרד ומפעיל אותו כדי לזהות אם גירסא מסוימת מכילה את הבאג.

  3. לסיום נריץ את git bisect ונעביר לה את מספרי הגירסאות ואת הסקריפט והיא כבר תגיד לנו באיזה גירסא לראשונה התגלה הבאג.

2. דוגמא מהחיים

במאגר שכאן הכנסתי באג באחד הקומיטים:

https://github.com/ynonp/git-bisect-demo

כך נראה לוג הקומיטים מענף master:

5a0f315 (HEAD -> master, origin/master) add usage instructions to readme
15fa19c its a new ruby project
5034722 allow saying goodbye
ca75b9c add comments
edfa593 add fib function
41d70f1 add demo class and test
46c60f9 initial commit

בנוסף בניתי ענף בשם bugfix ובו קובץ בדיקות שמזהה את התקלה. מעבר לענף bugfix והפעלת הבדיקה נראים כך:

$ git checkout bugfix
Switched to branch 'bugfix'

$ rspec
.F

Failures:

  1) #fib should return 5 as the 5th element
     Failure/Error: expect(Utils.fib(5)).to eq(5)

       expected: 5
            got: 16

       (compared using ==)
     # ./spec/utils_spec.rb:6:in `block (2 levels) in <top (required)>'

Finished in 0.01639 seconds (files took 0.09638 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec/utils_spec.rb:5 # #fib should return 5 as the 5th element

הפרויקט כתוב בשפת Ruby וספריית הבדיקות נקראת rspec. הפעלת הפקודה rspec מריצה את כל הבדיקות ומדווחת על שגיאה באחת הבדיקות: הבדיקה ציפתה למצוא את המספר 5 אבל קיבלה במקומו 16.

זה מעולה כי עכשיו אפשר להשתמש בבדיקה כדי לוודא שתיקנו את הבאג. אבל אנחנו לא עוצרים שם. נכתוב קובץ סקריפט בשם test.sh שמבצע את הפעולות הבאות:

#!/bin/bash

git cherry-pick --no-commit bugfix || exit 128
rspec
status=$?

git reset --hard

exit $status

הקובץ לוקח מענף bugfix את הקומיט האחרון באמצעות cherry-pick (זה הקומיט עם הבדיקה החדשה). לאחר מכן מפעיל את הבדיקה ושומר את הסטטוס בצד. הפקודה git reset --hard מבטלת את שילוב הבדיקה ומחזירה אותנו לעץ נקי בתיקיית העבודה כדי ש bisect יוכל להמשיך לבדוק עוד גירסאות ובסוף הסקריפט יוצא עם הסטטוס של rspec.

כשמפעילים את הסקריפט הזה מתיקיית העבודה של גירסא תקינה מקבלים קוד יציאה 0, וכשמפעילים אותו מגירסא שבורה מקבלים קוד יציאה שונה מ-0 וזאת למרות שבכל הגירסאות השבורות הבדיקה לא היתה חלק מהמערכת. זאת החשיבות של שורת ה cherry-pick שלוקחת בדיקה ממקום אחר, משלבת אותה בגירסא ישנה של המערכת ורואה אם הבדיקה הצליחה.

נעבור להפעיל את bisect כדי למצוא איפה היתה השגיאה. הפקודה הבאה מתחילה את תהליך ה bisect:

$ git bisect start HEAD edfa593
Bisecting: 1 revision left to test after this (roughly 1 step)
[50347225e7dd7602f3ab8eb84f1e1a2fbf873e06] allow saying goodbye

במקרה שלנו אני יודע שבגירסא edfa593 הכל עבד כמו שצריך ואני יודע שבגירסת HEAD הנוכחית משהו שבור. אני לא יודע איפה באמצע זה נשבר. גיט אומר לי שהוא עובר עכשיו לגירסא מספר 503472 ואחריה תישאר לו כנראה עוד גירסא אחת לבדוק.

בתגובה אני יכול לבקש מגיט להמשיך לרוץ באופן אוטומטי עם הבדיקות באמצעות הסקריפט שכתבתי:

$ git bisect run ./test.sh

ופה כבר קיבלתי הרבה פלט:

running ./test.sh
.F

Failures:

  1) #fib should return 5 as the 5th element
     Failure/Error: expect(Utils.fib(5)).to eq(5)

       expected: 5
            got: 16

       (compared using ==)
     # ./spec/utils_spec.rb:6:in `block (2 levels) in <top (required)>'

Finished in 0.01763 seconds (files took 0.09565 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec/utils_spec.rb:5 # #fib should return 5 as the 5th element

HEAD is now at 5034722 allow saying goodbye
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[ca75b9cef8024b1b6f09f05b6afa111910f9b47e] add comments
running ./test.sh
.F

Failures:

  1) #fib should return 5 as the 5th element
     Failure/Error: expect(Utils.fib(5)).to eq(5)

       expected: 5
            got: 16

       (compared using ==)
     # ./spec/utils_spec.rb:6:in `block (2 levels) in <top (required)>'

Finished in 0.01775 seconds (files took 0.09638 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec/utils_spec.rb:5 # #fib should return 5 as the 5th element

HEAD is now at ca75b9c add comments
ca75b9cef8024b1b6f09f05b6afa111910f9b47e is the first bad commit
commit ca75b9cef8024b1b6f09f05b6afa111910f9b47e
Author: ynonp <ynonperek@gmail.com>
Date:   Sat Sep 15 20:56:10 2018 +0300

    add comments

:100644 100644 a5cead8d4ebfdb3948c746ff20938d88e62ec281 b871f7944d50511cd065f252f2d5e442cd0539c1 M  utils.rb
bisect run success

אבל מה שמעניין אותי זה הסוף: גיט זיהה שהגירסא הראשונה בה הבדיקה נכשלה היא גירסא ca759c והוא מציג לי את זה ואת הודעת הקומיט שלה. על הדרך הוא גם העביר אותי לאותה גירסא שבורה כדי שאוכל לחקור מה קרה כאן. אני יכול להשתמש במתג p של פקודת לוג כדי לקבל יותר פרטים:

$ git log -p -1
commit ca75b9cef8024b1b6f09f05b6afa111910f9b47e (HEAD, refs/bisect/bad)
Author: ynonp <ynonperek@gmail.com>
Date:   Sat Sep 15 20:56:10 2018 +0300

    add comments

diff --git a/utils.rb b/utils.rb
index a5cead8..b871f79 100644
--- a/utils.rb
+++ b/utils.rb
@@ -2,7 +2,9 @@ module Utils
   def self.fib(n)
     x, y = 0, 1
     n.times do
-      x, y = y, x + y
+      # calculating fib
+      x = y
+      y = x + y
     end
     x
   end

ואני יכול לראות את השינויים בגירסא ולהבין מהם מה היה הבאג. במקרה שלנו מדובר על פונקציה שצריכה לחשב איבר מסידרת פיבונאצ'י אבל מישהו שינה קוד שעובד לקוד ארוך יותר שלא עובד.

3. אהבתם? בואו לוובינר על אוטומציה בגיט

בשבועיים הקרובים אני מתכנן לכתוב פה עוד קצת על אוטומציה בגיט לקראת וובינר שאעביר בנושא בסוף שבוע הבא. אם אתם בעניין ורוצים לחזק את המיומנות שלכם בגיט, בסקריפטים ובשילוב בין השניים עכשיו יהיה זמן טוב להירשם בקישור הבא:

https://www.tocode.co.il/workshops/46

נתראה בוובינר, ינון