リブート最初の記事を考えていたが、タイムリーなネタとして。
システム開発の品質測定でソースのステップ数が必要な場合がある
システム開発の仕事をしていると、品質計測を行う場面があるのではないでしょうか。
そんな時に、未だに一つの指標として見られているのが所謂「ステップ数(≒ソースの行数)」というやつです。
機能の複雑性がプログラムソースコードの規模(行数)と一定の相関がありそうだというのは何となく分かるところです。
ステップ数の測定はツールを使用する
そんなステップ数ですが、いちいちテキストエディタで広げて行数を数える。。。なんてことをやっていると日が暮れてしまうので、一般的にはステップ数をカウントするツールを使ったりします。
ネットワークの制約なくなんでもあり。
ということであれば、Winodowsならこんな便利なツールも使えそうです。
私の環境では諸々の制約から、ステップ数とコメント数を収集するツールが活用されていました。
改良開発の場合、差分のソース規模が必要になるが…
新規開発であれば、必要なソースを取得してこのツールに打ち込んでやればそれで済むので簡単なのですが、問題は追加開発でのステップ数というやつです。
上記のclocの派生版を会社では利用していましたが、-diffオプションの出力結果はちょっと「?」な内容でして、使いこなせていないだけという可能性もありますが、原始的にソース差分だけのソース集合を作成して測定するのが簡単そうです。
個人利用であればネット上に無数に存在するツールからぴったりなものを使用すればよいのですが、ちょっと大きめの会社で利用するとなるといろいろな手続きをふまなければならないのでかなり面倒なことになると思います。
Python、便利すぎ
そこで、Pythonで差分を取得するツールを書いてみることにしました。
ディレクトリ処理
ディレクトリ構造を取得するだけであれば、os.walkを使えば行けそうです。
こんな感じでループ処理します。
存在しないディレクトリは宛先ディレクトリに作成してやります。
たったこんな数行のコードでディレクトリを辿った再帰処理まで実装されてしまうので、めちゃくちゃ楽ちんですね。
# top : 更新後保管場所
# out : 差分保管場所
for root, dirs, files in os.walk(top):
for dir in dirs:
#格納先ディレクトリ構造を生成
dirPath = os.path.join(root, dir)
outpath = dirPath.replace(top, outtop)
if os.path.isdir(outpath):
pass
else:
# ない場合にはフォルダを生成
os.makedirs(outpath)
ファイル単位の処理
そして、ファイル単位の処理は同じループ内でやってしまいます。
存在しないファイルはファイルコピーし、存在するものについては差分出力させます。
同一性は判定していませんが、差分処理で差分ゼロとして処理されればいいやと手抜きです。
# top : 更新後保管場所
# oldtop : 更新前保管場所
# out : 差分保管場所
for root, dirs, files in os.walk(top):
for dir in dirs:
#格納先ディレクトリ構造を生成
dirPath = os.path.join(root, dir)
outpath = dirPath.replace(top, outtop)
if os.path.isdir(outpath):
pass
else:
# ない場合にはフォルダを生成
os.makedirs(outpath)
for file in files:
newfileName = os.path.join(root, file)
print(f'filePath = {newfileName}')
oldfileName = newfileName.replace(top, oldtop)
if os.path.isfile(oldfileName):
#print(oldfilepath + " が存在")
#ここで、差分があるか確認してなければスキップ。あれば差分ファイルを生成(関数化)。
else:
print(oldfileName + " がない")
#ないものについては、ファイルコピーする
outfilepath = newfileName.replace(top, outtop)
shutil.copy2(newfileName, outfilepath)
たったこれだけ書くだけで、大体の構造ができあがってしまいます。
差分比較処理…文字コードの問題
上記の差分処理関数とした部分も出来上がり、テストしているとここで問題が。
会社で扱っているソースの文字コードが混在しているためにPythonがエラーを吐いてしまうというもの。
Pythonでは文字コード判定にはchardet(https://pypi.org/project/chardet/)という便利なライブラリが用意されていますが、会社環境ではなんとpipが利用不可で導入はえらくめんどくさい。
他のメンバーも利用できるようにしておきたいので、導入ハードルはできるだけ下げておきたいと思い、結構無理やりだがUTF8とcp932(所謂sjis)しかない前提で例外処理だけで乗り切ることにしました。
で、作ったソースがこんな感じ。
def diffout(new_filename , old_filename, out_dir):
### Differクラスでファイルを比較
file1 = open(old_filename)
encode = 'cp932'
try:
str1 = file1.readlines()
except:
#ダメだったら、ファイル閉じてUTFで読み直してリトライ
file1.close()
file1 = open(old_filename, encoding = 'utf-8')
str1 = file1.readlines()
encode = 'utf-8'
file2 = open(new_filename)
try:
str2 = file2.readlines()
except:
#ダメだったら、ファイル閉じてUTFで読み直してリトライ
file2.close()
file2 = open(new_filename, encoding = 'utf-8')
str2 = file2.readlines()
encode = 'utf-8'
output_diff = difflib.unified_diff(str1, str2)
# 得られたリストを加工
o=[]
pattern = re.compile('---|\+\+\+|\@\@|-|\s|}')
pattern2 = re.compile('^\+')
for item in output_diff:
# 文字列の判定
# 先頭"---","+++","@@"," "(先頭スペース),"-"はコピーしない
if re.match(pattern,item):
# 一致したので何もしない
# print(item + " 対象外行として判定")
pass
else:
# 先頭の"+"を除去して文字列をリストに追加する
item = re.sub(pattern2, '',item)
o.append(item)
# ファイル出力
outfilename = os.path.basename(new_filename)
outfilename = os.path.join(out_dir, outfilename)
with open(outfilename, 'w',encoding = encode) as f:
f.writelines(o)
f.close()
file1.close()
file2.close()
結構乱暴なソースだが、どうやら会社で使用OKのwinmargeの差分とだいたい同じ結果が得られるようになった。
winmargeで手作業すると数百あるソースを処理するのに1日仕事になりそうだったが、これ使えば数秒で終わる。
余った時間は分析などに費やすことができる。。。と教科書的な話で締めくくっておくとしよう。
ソースの公開
せっかく作ったので、こういうレアな環境で困っている方がいたら使ってもらえるようソースを置いてみました。
特定環境で動くように作ったので、利用される場合には目的を達成できるか十分に検証の上自己責任でご利用ください。
コメント