MyHDL中文手冊(八)—— 單元測試
單元測試 unit test
介紹
現代數字硬體設計流程中的許多方面都可以看作是一種特殊的軟體開發。從這個角度來看,軟體設計技術的進步很自然也能應用於硬體設計。
一種值得注意的軟體設計方法是極限程式設計(XP)。這是一套引人入勝的技術和指導方針,似乎常常與傳統智慧背道而馳。在其他情況下,XP似乎只是強調常識,它並不總是符合通常的做法。例如,如果我們想擁有良好的軟體開發所需的新鮮思維,XP強調正常工作周的重要性。
提出一個關於極限程式設計的教程不是我的意圖,也沒有資格。相反,在本節中,我將強調一個我認為與硬體設計非常相關的XP概念:單元測試的重要性和方法。
單元測試的重要性
單元測試是極限程式設計的基石之一。其他XP概念,如程式碼的集體所有權和持續改進,只有通過單元測試才能實現。此外,XP強調編寫單元測試應該是自動化的,應該測試每個類中的所有內容,並且應該始終完美地執行。
我認為這些概念直接適用於硬體設計。此外,單元測試是管模擬時間的一種方法。例如,在不經常發生的事件上執行非常緩慢的狀態機可能無法在系統級別進行驗證,即使在最快的模擬器上也是如此。另一方面,即使在速度最慢的模擬器上,在單元測試中也很容易對其進行詳盡的驗證。
顯然,單元測試具有令人信服的優勢。另一方面,如果我們需要測試所有的東西,我們必須編寫大量的單元測試。因此,建立、管理和執行它們應該是簡單而愉快的。因此,XP強調需要一個支援這些任務的單元測試框架。在本章中,我們將探討如何使用標準Python庫中的unittest模組為硬體設計建立單元測試。
單元測試開發
在本節中,我們將非正式地探討單元測試技術在硬體設計中的應用。我們將通過一個(小的)示例:測試二進位制到格雷編碼器(如位索引bit indexing一節中所介紹的)來實現這一點。
定義需求
我們首先定義需求。對於格雷編碼器,我們希望輸出符合格雷碼的特性。讓我們將程式碼定義為一個碼字列表,其中一個碼字是位字串。n階碼有2*n個碼字。
眾所周知,格雷碼的特點是:
格雷碼中的連續碼字應該在一位中有所不同。
這就夠了嗎?不完全是:例如,假設一個實現返回每個二進位制輸入的LSB。這將符合要求,但顯然不是我們想要的。此外,我們不希望格雷碼字的位寬度超過二進位制碼字的位寬。
n階格雷碼中的每個碼字必須在同一階二進位制碼中精確出現一次。
把要求寫下來,我們就可以繼續了。
首先寫測試
XP世界中一個引人入勝的指導方針是首先編寫單元測試。也就是說,在實現某項內容之前,首先編寫將對其進行驗證的測試。這似乎違背了我們的自然傾向,也違背了我們的一般做法。許多工程師喜歡先實現,然後再考慮驗證。
但是如果你考慮一下,首先處理驗證是很有意義的。驗證只涉及到需求–所以您的想法還沒有被實現的細節弄得雜亂無章。單元測試是對需求的可執行描述,因此可以更好地理解它們,並且非常清楚需要做什麼。因此,執行工作應該更加順利。也許最重要的是,當您完成實現時,測試是可用的,並且任何人都可以隨時執行該測試來驗證更改。
Python有一個標準的unittest模組,可以方便地編寫、管理和執行單元測試。使用unittest,通過建立從unittest.TestCase繼承的類來編寫測試用例。單個測試是由該類的方法建立的:所有test開頭的方法名稱都被視為測試用例的測試。
我們將為Gray程式碼屬性定義一個測試用例,然後為每個需求編寫一個測試。測試用例類的概要如下:
import unittest
class TestGrayCodeProperties(unittest.TestCase):
def testSingleBitChange(self):
"""Check that only one bit changes in successive codewords."""
....
def testUniqueCodeWords(self):
"""Check that all codewords occur exactly once."""
....
每種方法都將是一個測試單個需求的小型測試平臺。為了編寫測試,我們不需要實現格雷編碼器,但我們需要設計的介面。我們可以通過一個假設的實現來指定這一點,如下所示:
from myhdl import block
@block
def bin2gray(B, G):
# DUMMY PLACEHOLDER
""" Gray encoder.
B -- binary input
G -- Gray encoded output
"""
pass
對於第一個需求,我們將測試所有連續的輸入數字,並對每一個輸入的當前輸出和前一個輸出進行比較,我們將檢查差異是否正好是一個位。對於第二個需求,我們將測試所有輸入數字,並將結果放在一個列表中。這一要求意味著如果我們對結果列表進行排序,我們應該得到一個數字範圍。對於這兩種要求,我們將測試所有格雷碼,直到某一階最大寬度max_Width。測試程式碼如下所示:
import unittest
from myhdl import Simulation, Signal, delay, intbv, bin
from bin2gray import bin2gray
MAX_WIDTH = 11
class TestGrayCodeProperties(unittest.TestCase):
def testSingleBitChange(self):
"""Check that only one bit changes in successive codewords."""
def test(B, G):
w = len(B)
G_Z = Signal(intbv(0)[w:])
B.next = intbv(0)
yield delay(10)
for i in range(1, 2**w):
G_Z.next = G
B.next = intbv(i)
yield delay(10)
diffcode = bin(G ^ G_Z)
self.assertEqual(diffcode.count('1'), 1)
self.runTests(test)
def testUniqueCodeWords(self):
"""Check that all codewords occur exactly once."""
def test(B, G):
w = len(B)
actual = []
for i in range(2**w):
B.next = intbv(i)
yield delay(10)
actual.append(int(G))
actual.sort()
expected = list(range(2**w))
self.assertEqual(actual, expected)
self.runTests(test)
def runTests(self, test):
"""Helper method to run the actual tests."""
for w in range(1, MAX_WIDTH):
B = Signal(intbv(0)[w:])
G = Signal(intbv(0)[w:])
dut = bin2gray(B, G)
check = test(B, G)
sim = Simulation(dut, check)
sim.run(quiet=1)
if __name__ == '__main__':
unittest.main(verbosity=2)
請注意實際的檢查是如何由一個由unittest.TestCase類定義的self.assertEqual方法執行的。此外,我們還在一個單獨的方法runTest中分離出所有Gray碼執行測試的因素。
測試驅動的實現
寫好測試後,我們從實現開始。為了便於說明,我們將特意編寫一些不正確的實現,以瞭解測試的行為。
執行使用unittest框架定義的測試的最簡單方法是在測試模組的末尾呼叫它的main方法:
unittest.main()
讓我們使用前面顯示的假Gray編碼器執行測試:
% python test_gray_properties.py
testSingleBitChange (__main__.TestGrayCodeProperties)
Check that only one bit changes in successive codewords. ... ERROR
testUniqueCodeWords (__main__.TestGrayCodeProperties)
Check that all codewords occur exactly once. ... ERROR
不出所料,這完全失敗了。讓我們嘗試一個不正確的實現,將輸入的LSB放在輸出中:
from myhdl import block, always_comb
@block
def bin2gray(B, G):
# INCORRECT IMPLEMENTATION
""" Gray encoder.
B -- binary input
G -- Gray encoded output
"""
@always_comb
def logic():
G.next = B[0]
return logic
執行測試會產生:
python test_gray_properties.py
testSingleBitChange (__main__.TestGrayCodeProperties)
Check that only one bit changes in successive codewords. ... ok
testUniqueCodeWords (__main__.TestGrayCodeProperties)
Check that all codewords occur exactly once. ... FAIL
======================================================================
FAIL: testUniqueCodeWords (__main__.TestGrayCodeProperties)
Check that all codewords occur exactly once.
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_gray_properties.py", line 42, in testUniqueCodeWords
self.runTests(test)
File "test_gray_properties.py", line 53, in runTests
sim.run(quiet=1)
File "/home/jand/dev/myhdl/myhdl/_Simulation.py", line 154, in run
waiter.next(waiters, actives, exc)
File "/home/jand/dev/myhdl/myhdl/_Waiter.py", line 127, in next
clause = next(self.generator)
File "test_gray_properties.py", line 40, in test
self.assertEqual(actual, expected)
AssertionError: Lists differ: [0, 0, 1, 1] != [0, 1, 2, 3]
First differing element 1:
0
1
- [0, 0, 1, 1]
+ [0, 1, 2, 3]
----------------------------------------------------------------------
Ran 2 tests in 0.083s
FAILED (failures=1)
現在,測試像預期的那樣通過了第一個需求,但是沒有通過第二個需求。在測試反饋之後,顯示了一個完整的回溯,可以幫助除錯測試輸出。
最後,我們使用正確的實現:
from myhdl import block, always_comb
@block
def bin2gray(B, G):
""" Gray encoder.
B -- binary input
G -- Gray encoded output
"""
@always_comb
def logic():
G.next = (B>>1) ^ B
return logic
這次通過了。
在這裡插入程式碼片$ python test_gray_properties.py
testSingleBitChange (__main__.TestGrayCodeProperties)
Check that only one bit changes in successive codewords. ... ok
testUniqueCodeWords (__main__.TestGrayCodeProperties)
Check that all codewords occur exactly once. ... ok
----------------------------------------------------------------------
Ran 2 tests in 0.476s
OK
額外要求
在上一節中,我們集中討論了格雷碼的一般要求。可以在不指定實際程式碼的情況下指定這些程式碼。很容易看出,有幾個程式碼滿足這些要求。在良好的XP風格中,我們只測試了需求,僅此而已。
可能需要更多的控制。例如,該要求可能是對特定程式碼的要求,而不是對一般屬性的遵從性。作為一個例子,我們將展示如何測試原始的格雷碼,這是一個滿足上一節要求的特定例項。在這個特定的例子中,這個測試實際上比前一個測試更容易。
我們將原始的n階格雷碼錶示為Ln。一些例子:
L1 = ['0', '1']
L2 = ['00', '01', '11', '10']
L3 = ['000', '001', '011', '010', '110', '111', '101', 100']
可以通過遞迴演算法指定這些程式碼,如下所示:
L1=[‘0’,‘1’]。
LN+1可以從Ln中得到如下結果。建立一個新程式碼Ln0,方法是在Ln的所有程式碼字前面加上“0”。建立另一個新程式碼LN1,方法是在Ln的所有程式碼字前面加上“1”,然後反轉它們的順序。Ln+1是Ln0和LN1的級聯。
Python以其優雅的演算法描述而聞名,這是一個很好的例子。我們可以用Python編寫演算法,如下所示:
def nextLn(Ln):
""" Return Gray code Ln+1, given Ln. """
Ln0 = ['0' + codeword for codeword in Ln]
Ln1 = ['1' + codeword for codeword in Ln]
Ln1.reverse()
return Ln0 + Ln1
程式碼[‘0’+用於.的程式碼字]叫做列表推導式。它是描述由for迴圈中的短計算構建的列表的一種簡潔方法。
現在的要求是輸出程式碼與預期的程式碼Ln匹配。我們使用nextLn函式來計算預期的結果。新的測試用例程式碼如下所示:
import unittest
from myhdl import Simulation, Signal, delay, intbv, bin
from bin2gray import bin2gray
from next_gray_code import nextLn
MAX_WIDTH = 11
class TestOriginalGrayCode(unittest.TestCase):
def testOriginalGrayCode(self):
"""Check that the code is an original Gray code."""
Rn = []
def stimulus(B, G, n):
for i in range(2**n):
B.next = intbv(i)
yield delay(10)
Rn.append(bin(G, width=n))
Ln = ['0', '1'] # n == 1
for w in range(2, MAX_WIDTH):
Ln = nextLn(Ln)
del Rn[:]
B = Signal(intbv(0)[w:])
G = Signal(intbv(0)[w:])
dut = bin2gray(B, G)
stim = stimulus(B, G, w)
sim = Simulation(dut, stim)
sim.run(quiet=1)
self.assertEqual(Ln, Rn)
if __name__ == '__main__':
unittest.main(verbosity=2)
實際上,我們的實現顯然是一個原始的格雷碼:
$ python test_gray_original.py
testOriginalGrayCode (__main__.TestOriginalGrayCode)
Check that the code is an original Gray code. ... ok
----------------------------------------------------------------------
Ran 1 test in 0.269s
OK