らんらん技術日記

日々の学習メモに

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がバージョンアップしても柔軟に対応できます。
想定するシステムのイメージは以下になります。

f:id:yukirunrun:20200412000436p:plain

僕が実際に構築したシステムの紹介です。簡単のため一つのパソコンで完結させています。
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のPythonWindowsアップデートの影響で、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
f:id:yukirunrun:20200412145554p:plain

ビルドでは、Windowsバッチコマンドの実行を選択。run.batの中でUVMのシミュレーション、上記pythonスクリプトを実行します。run.batの中身は面倒なので省略。
f:id:yukirunrun:20200412150743p:plain

ビルド後の処理では、JUnitテスト結果の集計を追加。テスト結果XMLに、pythonで変換したxmlへのパスを指定します。
f:id:yukirunrun:20200412151136p:plain

これで設定は完了。ビルドすると、プロジェクトのトップ画面でテスト結果の推移を見ることができます。
f:id:yukirunrun:20200412151302p:plain

テスト結果の詳細は、テスト階層を下に辿ることで見ることができます。
f:id:yukirunrun:20200412152002p:plain
f:id:yukirunrun:20200412152015p:plain


まとめ

UVMでのテスト結果をJenkinisで集計できるようにしました。
集計の方法は、
①UVMレポートへの自分流のルール設定
Pythonの変換スクリプト
によってある程度カスタマイズできます。ここで紹介したのは一例ということで、使用方法に合わせて変更くださいな。