Skip to content

Commit

Permalink
programmer: add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Rogdham committed Oct 28, 2017
1 parent 119476f commit 29b2e68
Show file tree
Hide file tree
Showing 6 changed files with 358 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ synlog.tcl
*bak
gerbers/
build/
*.pyc
8 changes: 8 additions & 0 deletions programmer/.coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[run]
branch = True
include = tinyfpgab.py

[report]
exclude_lines =
def __repr__
if __name__ == .__main__.:
5 changes: 5 additions & 0 deletions programmer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.cache
.coverage
.tox
__pycache__
htmlcov
21 changes: 20 additions & 1 deletion programmer/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
The GUI has been moved to the [TinyFPGA Programmer Application repo](https://github.com/tinyfpga/TinyFPGA-Programmer-Application). However, the `tinyfpgab.py` script can be used on its own with a command-line interface:
# Programmer

## GUI

The GUI has been moved to the [TinyFPGA Programmer Application repo][gui-repo].
However, the `tinyfpgab.py` script can be used on its own with a command-line
interface:

[gui-repo]: https://github.com/tinyfpga/TinyFPGA-Programmer-Application


## CLI

```
> python tinyfpgab.py --help
Expand All @@ -15,6 +26,7 @@ optional arguments:
```

You can list valid ports with the `--list` option:

```
> python tinyfpgab.py --list
Expand All @@ -41,3 +53,10 @@ You can use the `--com` option to specify a specific port. If you don't specify
Success!
```


## Testing

The tests can be run with [tox](https://tox.readthedocs.io/): just run the `tox` command.

The code coverage will be generated as HTML pages in the `htmlcov` directory.
312 changes: 312 additions & 0 deletions programmer/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
import os
import pytest
import shutil
import string
import tempfile
from tinyfpgab import TinyFPGAB

DATA = 'Thequickbrownfoxjumpsoverthelazydog'
DATA_4096 = ''.join(a + b + c
for a in string.uppercase
for b in string.lowercase
for c in string.digits)[:4096]


class FakeSerial(object):

def __init__(self, read_data=''):
self.written = [] # str instance if data written, True if flush call
self.read_data = read_data

def write(self, data):
assert isinstance(data, str)
self.written.append(data)

def read(self, read_len):
assert len(self.read_data) >= read_len
data = self.read_data[:read_len]
self.read_data = self.read_data[read_len:]
return data

def flush(self):
self.written.append(True)

def assert_written(self, parts):
# insert True after each write (for flush calls)
expected = []
for part in parts:
expected.append(part)
expected.append(True)
# test
assert self.written == expected


@pytest.fixture(scope='module')
def bitstream_dir():
# file content
hexdata = '''
54 68
65 71 75 69 63 6b 62 72
6f 77 6e 66 6f 78 6a 75 6d 70 73 6f 76 65 72 74 68 65 6c 61 7a 79 64
6f 67
'''.strip() + '\n\n'
assert hexdata.replace(' ', '').replace('\n', '').decode('hex') == DATA
# create temporary directory
tmpdir = tempfile.mkdtemp()
# create the files
with open(os.path.join(tmpdir, 'bitstream.hex'), 'w') as f:
f.write(hexdata)
with open(os.path.join(tmpdir, 'bitstream.bin'), 'wb') as f:
f.write(DATA)
# give the temporary directory to test cases needing it
yield tmpdir
# delete temporary directory
shutil.rmtree(tmpdir)


@pytest.mark.parametrize('method, serial_out, serial_in, expected', [
('sleep', '0101000000b9', '', None),
('wake', '0101000000ab', '', None),
('read_id', '01010004009f', '1f8401', '1f8401'),
('read_sts', '010100020005', '01', '01'),
('write_enable', '010100000006', '', None),
('write_disable', '010100000004', '', None),
('boot', '00', '', None),
])
def test_simple_cmds(method, serial_out, serial_in, expected):
# prepare
serial = FakeSerial(serial_in.decode('hex'))
fpga = TinyFPGAB(serial)
if expected is not None:
expected = [ord(c) for c in expected.decode('hex')]
# run
output = getattr(fpga, method)()
# check
assert output == expected
assert not serial.read_data
serial.assert_written([serial_out.decode('hex')])


@pytest.mark.parametrize('success_after', range(5))
def test_is_bootloader_active(success_after):
# prepare
calls = []
fpga = TinyFPGAB(None)
read_id = [[0x42, 0x13, 0x13]] * success_after + [[0x1f, 0x84, 0x01]]
# patching methods
fpga.wake = lambda *a: calls.append(('wake', a))
fpga.read = lambda *a: calls.append(('read', a))
fpga.read_id = lambda *a: calls.append(('read_id', a)) or read_id.pop(0)
# run
output = fpga.is_bootloader_active()
# check
assert output == (success_after < 3)
expected_calls = [
('wake', ()),
('read', (0, 16)),
('wake', ()),
('read_id', ()),
] * min(success_after + 1, 3)
assert calls == expected_calls


@pytest.mark.parametrize('length, serial_outs', [
# reading 16 bytes at a time, so testing various lengths
(5, ['01050006000b12345600']),
(16, ['01050011000b12345600']),
(17, ['01050011000b12345600',
'01050002000b12346600']),
(35, ['01050011000b12345600',
'01050011000b12346600',
'01050004000b12347600']),
])
def test_read(length, serial_outs):
# prepare
serial = FakeSerial(DATA[:length])
fpga = TinyFPGAB(serial)
expected = [ord(c) for c in DATA[:length]]
# run
output = fpga.read(0x123456, length)
# check
assert output == expected
assert not serial.read_data
serial.assert_written([d.decode('hex') for d in serial_outs])


def test_wait_while_busy():
# prepare
serial = FakeSerial('0101010100'.decode('hex'))
fpga = TinyFPGAB(serial)
# run
assert fpga.wait_while_busy() is None
# check
assert not serial.read_data
serial.assert_written(['010100020005'.decode('hex')] * 5)


@pytest.mark.parametrize('offset, length, block_len, serial_outs', [
# well aligned, one block
(0x123000, 0x1000, 0x1000, ['010400000020123000']),
(0x120000, 0x8000, 0x8000, ['010400000052120000']),
(0x120000, 0x10000, 0x10000, ['0104000000d8120000']),
# well aligned, several blocks
(0x123000, 0x2000, 0x1000, ['010400000020123000',
'010400000020124000']),
(0x123000, 0x8000, 0x1000, ['010400000020123000',
'010400000020124000',
'010400000020125000',
'010400000020126000',
'010400000020127000',
'010400000020128000',
'010400000020129000',
'01040000002012a000']),
# within the block, erase start of block
(0x123000, 0xff0, 0x1000, ['010400000020123000']),
# within the block, erase end of block
(0x123010, 0xff0, 0x1000, ['010400000020123000']),
# within the block, erase middle of block
(0x123008, 0xff0, 0x1000, ['010400000020123000']),
# several block, not aligned
(0x123010, 0x2fe0, 0x1000, ['010400000020123000',
'010400000020124000',
'010400000020125000'])
])
def test_erase(offset, length, block_len, serial_outs):
# prepare
calls = []
serial = FakeSerial()
fpga = TinyFPGAB(serial)
data_4096 = [ord(c) for c in DATA_4096]
fpga.write_enable = lambda: None # patch write_enable
fpga.wait_while_busy = lambda: None # patch wait_while_busy
fpga.read = lambda *a: calls.append(('read', a)) \
or data_4096[a[0] % 4096:][:a[1]]
fpga.write = lambda *a: calls.append(('write', a))
# run
assert fpga.erase(offset, length) is None
# check
assert not serial.read_data
serial.assert_written([d.decode('hex') for d in serial_outs])
expected_calls = []
restore_first_block = None
if offset % block_len > 0: # restore start of first block
restore_offset = offset & ~(block_len - 1)
restore_len = offset % block_len
expected_calls.append((
'read', (restore_offset, restore_len)))
expected_calls.append((
'write', (restore_offset, data_4096[:restore_len])))
restore_first_block = restore_offset
if (offset + length) % block_len > 0: # restore end of last block
restore_offset = offset + length
restore_len = block_len - (restore_offset % block_len)
read_call = ('read', (restore_offset, restore_len))
if restore_first_block == ((offset + length) & ~(block_len - 1)):
# restore before and after erase in same block
expected_calls.insert(1, read_call) # 2nd read before 1st write
else:
expected_calls.append(read_call) # 2nd read after 1st write
expected_calls.append((
'write', (restore_offset, data_4096[restore_offset % block_len:])))
assert calls == expected_calls


@pytest.mark.parametrize('offset, length, serial_outs', [
# witting 16 bytes at a time, so testing various length
(0x123400, 5, ['0109000000021234005468657175']),
(0x123400, 16, ['011400000002123400546865717569636b62726f776e666f78']),
(0x123400, 17, ['011400000002123400546865717569636b62726f776e666f78',
'0105000000021234106a']),
(0x123400, 35, ['011400000002123400546865717569636b62726f776e666f78',
'0114000000021234106a756d70736f7665727468656c617a79',
'010700000002123420646f67']),
# FIXME: for some reason, there is a special case to align on 256 bytes
(0x1234fd, 5, ['0107000000021234fd546865',
'0106000000021235007175']),
])
def test_write(offset, length, serial_outs):
# prepare
serial = FakeSerial()
fpga = TinyFPGAB(serial)
fpga.write_enable = lambda: None # patch write_enable
fpga.wait_while_busy = lambda: None # patch wait_while_busy
data = [ord(c) for c in DATA[:length]]
# run
assert fpga.write(offset, data) is None
# check
assert not serial.read_data
serial.assert_written([d.decode('hex') for d in serial_outs])


@pytest.mark.parametrize('success', [True, False])
def test_program(success):
# prepare
calls = []
fpga = TinyFPGAB(None, lambda *a: calls.append(('progress', a)))
bitstream = [ord(c) for c in DATA]
# patching methods
fpga.erase = lambda *a: calls.append(('erase', a))
fpga.write = lambda *a: calls.append(('write', a))
if success:
fpga.read = lambda *a: calls.append(('read', a)) or bitstream
else:
fpga.read = lambda *a: calls.append(('read', a)) or [84, 13, 37]
# run
output = fpga.program(0x123456, bitstream)
# check
assert output == success
assert calls == [
('progress', ('Erasing designated flash pages', )),
('erase', (0x123456, 35)),
('progress', ('Writing bitstream', )),
('write', (0x123456, bitstream)),
('progress', ('Verifying bitstream', )),
('read', (0x123456, 35)),
('progress', ('Success!' if success else 'Verification Failed!', )),
]


@pytest.mark.parametrize('ext', ['hex', 'bin', 'unknown'])
def test_slurp(bitstream_dir, ext):
# prepare
fpga = TinyFPGAB(None)
filename = os.path.join(bitstream_dir, 'bitstream.{}'.format(ext))
expected = (0x30000, [ord(c) for c in DATA])
if ext == 'unknown':
expected = None # FIXME: maybe the code should raise ValueError?
# run
output = fpga.slurp(filename)
# check
assert output == expected


@pytest.mark.parametrize('success', [True, False])
def test_program_bitstream(success):
# prepare
calls = []
fpga = TinyFPGAB(None, lambda *a: calls.append(('progress', a)))
bitstream = [ord(c) for c in DATA]
# patching methods
fpga.wake = lambda *a: calls.append(('wake', a))
fpga.read_sts = lambda *a: calls.append(('read_sts', a))
fpga.read = lambda *a: calls.append(('read', a))
if success:
fpga.program = lambda *a: calls.append(('program', a)) or True
else:
fpga.program = lambda *a: calls.append(('program', a)) or False
fpga.boot = lambda *a: calls.append(('boot', a))
# run
output = fpga.program_bitstream(0x123456, bitstream)
# check
assert output == success
expected_calls = [
('progress', ('Waking up SPI flash', )),
('wake', ()),
('read_sts', ()),
('read', (0, 16)),
('progress', ('35 bytes to program', )),
('program', (0x123456, bitstream)),
]
if success:
expected_calls.append(('boot', ()))
assert calls == expected_calls
12 changes: 12 additions & 0 deletions programmer/tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[tox]
skipsdist = True
envlist = py27

[testenv]
deps =
pytest
pytest-pep8
pytest-cov
commands=
py.test --pep8 --cov --cov-report term-missing test.py
py27: coverage html

0 comments on commit 29b2e68

Please sign in to comment.