masato-ka's diary

日々思ったこととか、やったことの備忘録。

Subversionのコミットログからバグを予測する!

元ネタは数日前にみつけたこれです。
http://www.publickey1.jp/blog/11/post_193.html
Googleではソースコードの修正履歴をもとに今後バグが発生しそうなソースを探しているという記事でした。基本的なアイディアは、「よくコミットされてるソースはたくさん修正してるから、バグもたくさん含まれているよね」ってとこでしょうか?
任意のソースコードに対して、コミットした日時を評価関数で計算して、コミット回数分の積をスコアとするというものです。面白そうだったのでPythonで簡単なスクリプトを作って普段使っているSubversionリポジトリに対して計算してみました。

svn lookコマンドに「-q」「-v」オプションを付けた結果をファイルに保存しデータを作成しました。

$svn look -q -v [repository path] > svn_log_result.log

プログラムは主に以下の動作をします。
まず最初に、上記のログファイルからパスの部分を抽出し、計算対象となるパスを抽出しリスト化します。次に再度ログファイルを先頭から精査して、パスをキー値、コミット時間のリストをバリューとしたディクショナリを作成します。最後に、作成したリストとバリューを使い各パスごとのスコアを計算しました。計算式は上記のブログで紹介しているものと同じです。

実際に作成したPythonスクリプトを以下に掲載します。1時間くらいで書いたものなので問題もありそうですが、とりあえず動きました。
以下にそのソースをのせてます。バグのコミットかどうか関係なく計算します。また、ソース以外のファイルが含まれていてもカウントしてしまいます。
prediction_bug.py

import time
from math import exp
import re
import sys
def main():
    
    first_time = 0.0
    file_path=sys.argv[1]
    file_p = open(file_path)
    file_line = file_p.readlines()
    file_p.close()

    #[path1,path2,path3,path4]
    # A: Add new file or directory
    # D: Delete file or directory
    # M: Modify file or directory
    # R: Replace file or directory
    # Make path list
    paths=set([line.strip()[2:] for line in file_line if line.strip()[0] in ['A','M','R','D']])

    # Make dictionary key=path value=[commit_time1,commit_time2,commit_time3.....] 
    data_reg = re.compile(u'[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}')    
    revision_time={}
    time=''
    for line in file_line:        
        if line.strip()[0] == 'r':
            match = data_reg.search(line.strip())
            if not match is None:
                time = match.group(0)             
        if line.strip()[0] in ['A','M','R','D']:
            if revision_time.has_key(line.strip()[2:]):
                #Update dictuonary
                revision_time[line.strip()[2:]].append((int(time[0:4]),int(time[5:7]),int(time[8:10]),int(time[11:13]),int(time[14:16]),int(time[17:19])))
            else:
                #Append new path
                times=[(int(time[0:4]),int(time[5:7]),int(time[8:10]),int(time[11:13]),int(time[14:16]),int(time[17:19]))]
                revision_time.update({line.strip()[2:]:times})
    
   # calc score    
    for path in paths:
        score=0.0
        commit_date = revision_time[path]
        if len(commit_date) > 1:
            first=commit_date[0]
            for date in commit_date:
                lt = lapse_time(first,date)
                score += bug_score(lt)
            print '%s,%f' % (path,score) 
    

def lapse_time((f_year, f_month, f_day, f_h, f_m, f_s),(b_year, b_month, b_day, b_h, b_m, b_s)):
    first_time = time.mktime((f_year,f_month,f_day,f_h,f_m,f_s)+(0,0,0))
    bug_time = time.mktime((b_year,b_month,b_day,b_h,b_m,b_s)+(0,0,0))
    now_time = time.time()
    delta_fix_time=float(now_time-bug_time)
    delta_alive_time=float(now_time-first_time)
    return float(delta_fix_time)/float(delta_alive_time)
    
def bug_score(lt):
    return 1.000/(1.000+exp(-12.000*lt+12.000))

if __name__ == '__main__':
    main()

以下の様なコマンドで実行して下さい。

$python prediction_bug.py svn.log > result.csv

結果をexcelなどで開き、スコアの降順に並べたところ、確かに普段修正が多いファイルが上位に来ました。当たり前と言えば当たり前なんでしょうが、普段感覚的にしか感じない修正の回数などを可視化できて面白いと思いました。バグが落ち着いて来るとスコアが下がるはずなので、そのあたりも見てみると面白いかもしれません。
コードの複雑度などのメトリクスもありますが、実際に修正が多いファイルを見つけるということは、実際に使われている or バグが発見されやすいコードを発見するのに役立つと思います。また、簡単に計測ができてしまうのも魅力的かもしれません。また、この手法ならソースコードだけでなく設計書などのドキュメントも計測できそう!という感じもします。
リファクタリングの目安などに利用すると面白いと思います。もう少しコードをきれいにしてバグりそうなソースを発見するのに利用してみたいです。

せっかく構成管理のツールを利用しているなら、こういったリポジトリの情報を分析してみると言うのも良いかもしれません。プロジェクトの改善につながる情報が落ちているかも。定量的な分析ツールはSubversionではStatSVNというツールがあるのですが、GitとかMarcurialはそういうものがあるのかな。。。個人的には集合知的な手法でリポジトリを分析すると面白いのではと思いますがなにが?という疑問符も。