JenkinsでUVMのテスト結果を集計する
記事の順番で言えば、次はUVMの受信編になるのですが・・・話をすっとばしてJenkinsとの連動を考えてみます! 受信編は気が乗れば書きますw
僕がUVMを学んでいる目的は、HDLのテストケースを効率的に記述することです。となると、テストケースを集計する仕組みがあった方がよく、そこで目星をつけたのがJenkinsです。同様のことを考えている先行例として、Qiitaの以下の記事が参考になります。
qiita.com
上の記事では、UVMのxml_report_serverクラスを継承して、Jenkinsが扱いやすい形式のxmlファイルを作成しているようです(雰囲気、未完で終わっているのかな・・・?)。
ただ個人的には、あんまり好みな方法じゃないです。Jenkins都合のために、System Verilogのコードを書きたくないのでw System Veirlogの記述は最小限に留めて、こういうことはPythonに任せましょう!
Pythonであればコーディングは簡単ですし、解説記事もたくさんあります。UVMがバージョンアップしても柔軟に対応できます。
想定するシステムのイメージは以下になります。
僕が実際に構築したシステムの紹介です。簡単のため一つのパソコンで完結させています。
OS:Windows10
バージョン管理システム:Git、確かGit for Windowsをインストール
JenkinsからGitリポジトリへのアクセス:http、Apacheサーバで実現
HDLシミュレータ:Modelsim Intel FPGA Starter edition
Python:Anaconda3のPython
補足1:Gitリポジトリはローカルに作っています。アクセスは一応httpにしていますが、外部に公開しないならfileアクセスでオッケー。
補足2:AnacondaのPythonはWindowsアップデートの影響で、pythonという名前で実行するには絶対パス指定が必要です。僕はシンボリックリンクを張って、python_slという名前で使っています。
UVM側の準備
テスト実行はUVMでやるのですから、テストレポートを出力するまでがUVMの仕事です。レポート用マクロ(uvm_info、uvm_warningなど)の機能を使い、テストに対応したレポートのみを外部ファイルに出力します。コードにするとこんな感じ。
class xxx_scoreboard extends uvm_subscriber #(xxx_item); `uvm_component_utils(xxx_scoreboard); bit test1_passed; function new(string name = "xxx_scoreboard", uvm_component parent); super.new(name, parent); endfunction: new extern function void write(xxx_item t); extern function void reportTest(); endclass: xxx_scoreboar function void xxx_scoreboard::write(xxx_item t); //省略 endfunction: write function void xxx_scoreboard::reportTest(); if(test1_passed) `uvm_info("TEST", "Testcase1 Success", UVM_LOW); else `uvm_warning("TEST","Testcase1 Fail"); //省略 endfunction: reportTest
class xxx_test extends uvm_test; `uvm_component_utils(xxx_test) xxx_env env; uvm_factory factory; int print_file; function new(string name = "xxx_test", uvm_component parent = null); super.new(name, parent); endfunction : new virtual function void build_phase(uvm_phase phase); super.build_phase(phase); env = xxx_env::type_id::create("env", this); endfunction : build_phase virtual function void run_phase(uvm_phase phase); //テストケース実行、省略 endfunction : run_phase function void end_of_elaboration_phase(uvm_phase phase); print_file = $fopen("report/test1.txt"); set_report_id_file_hier("TEST", print_file); set_report_severity_action_hier(UVM_INFO, UVM_DISPLAY | UVM_LOG); set_report_severity_action_hier(UVM_WARNING, UVM_DISPLAY | UVM_LOG); //dump_report_state(); endfunction : end_of_elaboration_phase endclass : xxx_test
xxx_scoreboad.svhはテスト結果を判定するクラス、xxx_test.svhはテストケースを実行する最上位クラスです。
xxx_test.svhのend_of_elaboration_phaseにおいて、IDが"TEST"のuvm_infoとuvm_warningを外部ファイル"report/test1.txt"に出力するように設定しています。他のテストケースの出力ファイルも、reportフォルダ以下に保存することを想定しています。
xxx_test.svhでは、テストが成功したときはuvm_info、失敗したときはuvm_warningをコールします。引数のIDには"TEST"を指定し、メッセージの一番最初にTestcase名を書いています。
これはあくまで僕が勝手に決めたルールなので、UVMの公式ではないです。Jenkinsには、このルールに従ってテスト結果を集計させます。
シミュレーションの実行後、出力ファイルは下みたいになります。特に驚きもなく、UVMのレポート出力がそのまま入るだけです。
UVM_INFO ..\src\xxx_scoreboard.svh(50) @ 7500: uvm_test_top.env.sb [TEST] Testcase1 Success UVM_WARNING ..\src\xxx_scoreboard.svh(56) @ 7500: uvm_test_top.env.sb [TEST] Testcase2 Fail
Pythonで変換スクリプト作成
Jenkinsにはデフォルトで、JUnitと呼ばれるテストフォーマットの結果を読み込み、グラフやリストとして表示する機能があります。
僕はJUnitのことはよくわからないです・・・。
とはいえ、UVMのテスト結果をJUnitのフォーマットに変換すれば、JenkinsでUVMのテスト結果を集計できます!以下のスクリプトで、先ほどのファイルを変換します。
import os import re from lxml import etree as ET REPORT_PATH = './report/' OUTPUT_FILE = './result.xml' class TestInfo: def __init__(self, fail, classname, name, msg): self.fail = fail self.classname = classname self.name = name self.msg = msg class ReportInfo: def __init__(self): self.name = '' self.cnt_test = 0 self.cnt_fail = 0 self.test_info = [] def isNewMsg(text): patterns = ["UVM_INFO", "UVM_WARNING"] for pattern in patterns: if pattern in text: return True return False def searchId(line): match = re.search(r'\[.+\]', line) id_bracket = match.group() return id_bracket[1:len(id_bracket)-1] def takeFirstWord(line): match = re.search(r'\[.+\]', line) end = match.end() msg = line[end:] return msg.split()[0] def removeExtension(word): return word[:len(word)-4] def reconstructMsg(line): words = line.split() msg = '' for word in words: if msg != '': msg += ' ' msg += word return msg def analysisReport(report): report_info = ReportInfo() report_info.name = removeExtension(report) with open(REPORT_PATH + report) as rpt: nxt_line = rpt.readline() test_info = TestInfo(False, "dummy", "dummy", "dummy") while nxt_line: line = nxt_line nxt_line = rpt.readline() if len(line) < 2: #skip a blank line continue if isNewMsg(line): #create a new test class report_info.cnt_test += 1 classname = report_info.name + '.' + searchId(line) name = takeFirstWord(line) msg = reconstructMsg(line) test_info = TestInfo(False, classname, name, msg) if 'UVM_WARNING' in line: test_info.fail = True report_info.cnt_fail += 1 report_info.test_info.append(test_info) else: #add more msg to the previous test class msg = reconstructMsg(line) test_info.msg += "\n" + msg return report_info def makeXml(): testsuites = ET.Element('testsuites') for report in os.listdir(REPORT_PATH): report_info = analysisReport(report) testsuite = ET.SubElement(testsuites, 'testsuite', {'name' : report_info.name, 'tests': str(report_info.cnt_test), 'failures': str(report_info.cnt_fail) }) for test_info in report_info.test_info: testcase = ET.SubElement(testsuite, 'testcase', {'classname' : test_info.classname, 'name' : test_info.name}) if test_info.fail == True: ET.SubElement(testcase, 'failure', {'message' : test_info.msg}) system_out = ET.SubElement(testcase, 'system-out') system_out.text = test_info.msg tree = ET.ElementTree(testsuites) fl = OUTPUT_FILE tree.write(fl, pretty_print=True, xml_declaration=True, encoding='utf-8') if __name__ == '__main__': makeXml()
変換結果はこんな感じになります。
<?xml version='1.0' encoding='UTF-8'?> <testsuites> <testsuite name="test1" tests="2" failures="1"> <testcase classname="test1.TEST" name="Testcase1"> <system-out>UVM_INFO ..\src\xxx_scoreboard.svh(50) @ 0: uvm_test_top.env.sb@@seq [TEST] Testcase1 Success</system-out> </testcase> <testcase classname="test1.TEST" name="Testcase2"> <failure message="UVM_WARNING ..\src\xxx_scoreboard.svh(56) @ 0: uvm_test_top.env.sb@@seq [TEST] Testcase2 Fail"/> <system-out>UVM_WARNING ..\src\xxx_scoreboard.svh(56) @ 0: uvm_test_top.env.sb@@seq [TEST] Testcase2 Fail</system-out> </testcase> </testsuite> </testsuites>
Jenkinsの設定
ここまで完成すれば、後はJenkinsの設定をするだけです。
簡単のため、新規ジョブ作成からフリースタイル・プロジェクトのビルドを選択します。プロジェクト名は適当にJenkins_Testとします。プロジェクト名にはスペースを入れないこと、同様にJENKINS_HOMEのパスにもスペースを入れないことを推奨します。変な文字が入ると、特にModelsimあたりが正常に動きません。僕の環境だとデフォルトのJENKINS_HOMEは「C:\Program Files (x86)\Jenkins」ですが、わざわざ「D:\jenkins」に移しています(CがSSD、DがHDDなのでストレージの節約の目的もある)。
プロジェクトの設定に移ります。ソースコード管理はGitでこんな感じ。httpでlocalhostを参照するくらいなら、file://を使った方が素直な気もしますw
ビルドでは、Windowsバッチコマンドの実行を選択。run.batの中でUVMのシミュレーション、上記pythonのスクリプトを実行します。run.batの中身は面倒なので省略。
ビルド後の処理では、JUnitテスト結果の集計を追加。テスト結果XMLに、pythonで変換したxmlへのパスを指定します。
これで設定は完了。ビルドすると、プロジェクトのトップ画面でテスト結果の推移を見ることができます。
テスト結果の詳細は、テスト階層を下に辿ることで見ることができます。