ソースコード同士の類似性評価ツールを作ってみた

Q: あなたはテストエンジニアです、ここにテストしなくてはならないソースが大量にあります。
しかし、よくみるとコピペメソッドによる類似コードが多数あり、ロジックを共有してるコードが大量にあります。従って、全てのコードをテストする必要はなさそうです。
そこで、ここで類似コードに関しては、代表的ないくつかをテストするという抽出テストにしたいと思いました、ここで、どうやって選別しますか?


A:類似したコードを群として束ね、類似性の高いコードはテストを割愛します。
類似性検出方法は下記の手段を用います。


A B C D のコードがあった時。

ひとつめは

  1. Aのコードを2回足しあわせたものを作ります。 A+A
  2. 次にそれを圧縮します。 zip(A+A)
  3. そして、それをファイルサイズで割った値を算出します。 zip(A+A)/size(A+A)=f(AA)

ふたつめは

  1. AとBのコードを足しあわせたファイルを作ります。 A+B
  2. 次にそれを圧縮します。 zip(A+B)
  3. そして、それをファイルサイズで割った値を算出します。 zip(A+B)/size(A+B)=f(AB)

ここで
f(AB)/f(AA)の値が1に近いとBはAとはあまり違わないものであるといえます。
もしも
f(AB)/f(AA)>1 の場合、1より大きい程、BはAとは異なったものであるといえます。

ここでzip()はrunlength方を使用していると想定しています。
runlength方は、ファイル内で同一パターンを見つけると、前出と同じ、といった表記に置換して圧縮する一般的な圧縮手法です。

動かし方
ソースコードの置いてあるディレクトリーの元の部分でこのスクリプトを動かすと、その下にある
コードの全てに対して、コード間の距離を評価します。

課題
実はすごく遅いです。当たり前、毎回圧縮しているのだから、ファイルIOをもっと減らしてマルチスレッドに
しないと実用的にはならない予感がします。
本来テスト対象コードが多すぎるから絞り込みたいという動機があるので、
処理速度がもっと速くないと使えないでしょう。

// ソースコード間の違いを評価するツール
List codes=[]
// ここでは 拡張子がjavaのコードを再帰的に収集します。
new File(".").eachFileRecurse { file -> 
	if(file.isFile() && file.name.endsWith("java")) { 
		name = file.getPath() //+file.getName()
		codes.add(file.getPath())
	}
}
println "検査対象コード"
println codes
println "ソース間距離: (100より大きいほど、二つのファイルは異なっている。"

num = 1
for (x in (0..codes.size-1)) {
	for (y in (0..codes.size-1)) {
		if (1 /* x!=y*/) {
			print "["+num++ +"]"+codes.getAt(y)+" "+codes.getAt(x)+"\t"
			isUnique(codes.getAt(x), codes.getAt(y))
		}
	}		
}

def isUnique(code1, code2) {
	File output0 = new File("doubled.txt")
	output0.write("")
	new File(code1).eachLine { line1 ->
		//println line1
		output0.append(line1)
	}
	new File(code1).eachLine { line1 ->
		//println line1
		output0.append(line1)
	}

	File output = new File("merged.txt")
	output.write("")
	new File(code1).eachLine { line1 ->
		//println line1
		output.append(line1)
	}
	new File(code2).eachLine { line2 ->
		//println line2
		output.append(line2)
	}

	proc = "cmd /c gzip -f doubled.txt".execute()
	proc.waitFor()

	proc = "cmd /c gzip -f merged.txt".execute()
	proc.waitFor()

	def zaa = new File("doubled.txt.gz").size() 
	def zab = new File("merged.txt.gz").size() 
	def a   = new File(code1).size()
	def b   = new File(code2).size()
	println " 類似性 ="+( (zab/(a+b))/(zaa/(a+a)))*100
}

実行イメージは例えば以下のようになります。
抽出方法とか、計算の効率化とか、もう少し改善したいです。

                                                                                                                                    • -

検査対象コード
[.\a.java, .\b.java, .\SOURCE1\a.java, .\SOURCE1\b.java]
ソース間距離: (100より大きいほど、二つのファイルは異なっている。
[1].\a.java .\a.java 類似性 =99.9551166900
[2].\b.java .\a.java 類似性 =37.2159692800
[3].\SOURCE1\a.java .\a.java 類似性 =192.5814740900
[4].\SOURCE1\b.java .\a.java 類似性 =103.5254833100
[5].\a.java .\b.java 類似性 =152.3449010600
[6].\b.java .\b.java 類似性 =99.9632623700
[7].\SOURCE1\a.java .\b.java 類似性 =178.8919630900
[8].\SOURCE1\b.java .\b.java 類似性 =279.0446589500
[9].\a.java .\SOURCE1\a.java 類似性 =27.5365924800
[10].\b.java .\SOURCE1\a.java 類似性 =6.2432073200
[11].\SOURCE1\a.java .\SOURCE1\a.java 類似性 =97.5609756100
[12].\SOURCE1\b.java .\SOURCE1\a.java 類似性 =12.7184763200
[13].\a.java .\SOURCE1\b.java 類似性 =133.4013067100
[14].\b.java .\SOURCE1\b.java 類似性 =87.9460498600
[15].\SOURCE1\a.java .\SOURCE1\b.java 類似性 =115.114421600
[16].\SOURCE1\b.java .\SOURCE1\b.java 類似性 =99.9902913200