Skip to content

Commit

Permalink
Add example support to schemas to enable unittests (#272)
Browse files Browse the repository at this point in the history
Add example field to register and functions
Created a unit test template that can create unit tests for template
Update validator to optionally run unit tests on a template
Add examples to BMP280 peripheral
Update documentation templates with examples

Fixes #271
  • Loading branch information
Fleker authored May 22, 2021
1 parent e68af67 commit 88897ab
Show file tree
Hide file tree
Showing 11 changed files with 417 additions and 69 deletions.
29 changes: 29 additions & 0 deletions cyanobyte-spec/cyanobyte.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,10 @@
"W",
"n"
]
},
"example": {
"description": "Example values of this register.",
"$ref": "#/definitions/Examples"
}
},
"required": [
Expand Down Expand Up @@ -356,6 +360,10 @@
"type": "object",
"additionalProperties": false,
"properties": {
"example": {
"description": "Example return values of this function.",
"$ref": "#/definitions/Examples"
},
"input": {
"description": "Variables that are required before running computation.",
"type": "object",
Expand Down Expand Up @@ -429,6 +437,27 @@
"float64"
]
},
"Examples": {
"description": "Potential values, good and bad, that may be the outcome of this.",
"type": "object",
"properties": {
"valid": {
"description": "Non-exhaustive list of valid example values.",
"type": "array",
"items": {
"type": "number"
}
},
"errors": {
"description": "Map where key is a custom error label and value is that error result.",
"additionalProperties": true,
"type": "object"
}
},
"required": [
"valid"
]
},
"Command": {
"type": "object",
"description": "A command to be used in Pseduo-YAML operations.",
Expand Down
12 changes: 9 additions & 3 deletions cyanobyte-templates/datasheet.tex
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,18 @@ \section{Register Description}

\section{Technical specification}
\centering
\begin{tabular}{lcrr}
\begin{tabular}{lcrrr}
\toprule
& Register Name & Register Address & Register Length \\
& Register Name & Register Address & Register Length & Example \\
\midrule
{% for key,register in registers|dictsort %}
{{key | regex_replace('_', '\_')}} & {{register.title}} & {{register.address}} & {{register.length}} \\
{{key | regex_replace('_', '\_')}} & {{register.title}} & {{register.address}} & {{register.length}}
{%- if 'example' in register -%}
& {{ register.example.valid[0] }}
{%- else -%}
&
{%- endif -%}
\\
{% endfor %}
\bottomrule
\end{tabular}
Expand Down
3 changes: 3 additions & 0 deletions cyanobyte-templates/doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ description: >
{% if register.signed %}
* Signed value
{% endif %}
{% if 'example' in register %}
* Example value: `{{ register.example.valid[0] }}`
{% endif %}

{{ register.description }}
{% endfor %}
Expand Down
125 changes: 125 additions & 0 deletions cyanobyte-templates/python-unittest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
{% import 'macros.jinja2' as utils %}
{% import 'base.python.jinja2' as py %}
{% set template = namespace(enum=false, sign=false, math=false, struct=false, time=false) %}
{{ utils.pad_string('# ', utils.license(info.copyright.name, info.copyright.date, info.license.name)) -}}
#
# Auto-generated file for {{ info.title }} v{{ info.version }}.
# Generated from {{ fileName }} using Cyanobyte Codegen v{{ version }}

import unittest

{# Copy logic macro from `raspberrypi.py` #}
{% macro logic(logicalSteps, function) -%}

{% for step in logicalSteps %}
{% for key in step.keys() %}
{% if step[key] is mapping and 'rawRead' in step[key] %}
{% set bytes = (step[key].rawRead / 8) | round(1, 'ceil') | int %}
{{key}} = 0
{% for n in range(bytes) %}
{# Read each byte one at a time from device #}
_datum = self.bus.read_byte(self.device_address)
{{key}} = {{key}} << 8 | _datum
{% endfor %}
{% break %}
{% endif %}
{# Check if assignment is a send-op #}
{% if key == '$cmdWrite' %}
{% if 'value' in step[key] %}
self.set_{{step[key].register[12:].lower()}}({{step[key].value}})
{% else %}
self.set_{{step[key].register[12:].lower()}}()
{% endif %}
{% break %}
{%- endif %}
{% if key == '$delay' %}
time.sleep({{ step[key].for / 1000 }})
{{ logic(step[key].after, function) }}
{% break %}
{%- endif %}
{# Check if assignment op #}
{% if step[key] is string and step[key][0:1] == "=" %}
{{key | camel_to_snake}} {{step[key]}}
{%- endif %}
{# Check if assignment is a send-op #}
{% if key == 'send' %}
self.set_{{function.register[12:].lower()}}({{step[key]}})
{%- endif %}
{# Check if assignment is register read op #}
{% if step[key] is string and step[key][:12] == '#/registers/' %}
{{key | camel_to_snake}} = self.get_{{step[key][12:].lower()}}()
{%- endif %}
{# Check if assignment is function call op #}
{% if step[key] is string and step[key][:12] == '#/functions/' %}
{{key | camel_to_snake}} = self.{{step[key].lower() | regex_replace('#/functions/(?P<function>.+)/(?P<compute>.+)', '\\g<function>_\\g<compute>')}}()
{%- endif %}
{# If the value is a list, then this is a logical setter #}
{% if step[key] is iterable and step[key] is not string %}
{{key | camel_to_snake}} = {{py.recursiveAssignLogic(step[key][0], step[key][0].keys()) -}}
{%- endif %}

{% endfor %}
{%- endfor %}
{%- endmacro %}

{{ py.importStdLibs(fields, functions, i2c, template) -}}

class Test{{info.title}}(unittest.TestCase):
"""
Test class to validate behavior for {{info.title}}.
"""
{% for r in registers %}
def get_{{r.lower()}}(self):
{% if 'example' in registers[r] %}
return {{registers[r].example.valid[0]}} # Hardcoded example
{% else %}
# registers.{{r}}.example does not exist
# This will be okay unless we try to call it.
raise ValueError("Example does not exist for register {{r}}.")
{% endif %}

def set_{{r.lower()}}(self, val):
# Ignore.
return

{% endfor %}

{% for f in functions %}
{% for c in functions[f].computed %}
{% set compute = functions[f].computed[c] %}
{% if 'example' in compute %}
def {{f.lower()}}_{{c.lower()}}(self):
return {{compute.example.valid[0]}} # Hardcoded example

def test_{{c.lower()}}(self):
# Execute actual code
{# Read `value` if applicable #}
{% if 'input' in compute %}
{%- for vkey,variable in compute.input|dictsort %}
{% if vkey == 'value' %}
# Read value of register into a variable
value = self.get_{{function.register[12:].lower()}}()
{% endif %}
{% endfor -%}
{% endif %}
{# Handle the logic #}
{{ logic(compute.logic, function) -}}
{# Return if applicable #}
{# Return a tuple #}
{% if 'return' in compute and compute.return is not string %}
returnValue = [{% for returnValue in compute.return %}{{ returnValue | camel_to_snake }}{{ ", " if not loop.last }}{% endfor %}]
{# Return a plain value #}
{% elif compute.return is string %}
{# See if we need to massage the data type #}
{% if compute.output == 'int16' %}
# Convert from a unsigned short to a signed short
{{compute.return}} = struct.unpack("h", struct.pack("H", {{compute.return}}))[0]
{% endif %}
returnValue = {{compute.return}}
expectedValue = {{functions[f].computed[c].example.valid[0]}}
{% endif %}
self.assertEqual(returnValue, expectedValue)

{% endif %}
{% endfor %}
{% endfor %}
3 changes: 3 additions & 0 deletions cyanobyte-templates/webpage.html
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ <h3>
{% if register.signed %}
<br><em>Signed value</em>
{% endif %}
{% if 'example' in register %}
<br><em>Example value: <code>{{ register.example.valid[0] }}</code></em>
{% endif %}
</h3>
</td>
{% if key in template.registersToFields %}
Expand Down
1 change: 1 addition & 0 deletions cyanobyte.egg-info/SOURCES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ cyanobyte-templates/mcu.c
cyanobyte-templates/mcu.h
cyanobyte-templates/mcu.jinja2
cyanobyte-templates/micropython.py
cyanobyte-templates/python-unittest.py
cyanobyte-templates/raspberrypi-spi-emboss.py
cyanobyte-templates/raspberrypi-spi-register.py
cyanobyte-templates/raspberrypi.py
Expand Down
55 changes: 52 additions & 3 deletions cyanobyte/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import sys
import json
import click
import os
import os.path as path
import yaml
from yaml.constructor import ConstructorError
try:
Expand Down Expand Up @@ -73,17 +75,64 @@ def cyanobyte_validate(input_files):
print(e.err)


def unittest(input_files):
# Load the python-unittest template file
template = "cyanobyte-templates/python-unittest.py"
try:
import pkg_resources
template = pkg_resources.resource_filename('cyanobyte-templates', 'python-unittest.py')
except:pass

if _DEBUG:
print('cyanobyte-codegen \
-c \
-o ./tmp/ \
-t ' + template + ' ' + ' \
'.join(input_files))
os.system('cyanobyte-codegen \
-c \
-o ./tmp/ \
-t ' + template + ' ' + ' \
'.join(input_files))
for i in input_files:
# Now execute each unittest
file = i.replace('.yaml', '.py')
file = path.basename(file)
# NOTE: Won't work for different package names
if _DEBUG:
print('python3 -m unittest tmp/com/cyanobyte/' + file)
os.system('python3 -m unittest tmp/com/cyanobyte/' + file)

@click.command()
@click.option("-d", "--debug", "debug", default=False)
@click.option("--validate-schema", "flag_schema", is_flag=True)
@click.option("--unit-test", "flag_unittest", is_flag=True)
@click.argument("input_files", type=click.Path(exists=True), nargs=-1)
def click_validate(input_files):
def click_validate(input_files, flag_schema, flag_unittest, debug=False):
"""
Main command line entrypoint
Args:
input_files: A list of CyanoByte documents to validate.
"""
cyanobyte_validate(input_files)

#pylint: disable=global-statement
global _DEBUG
_DEBUG = debug

run_schema = True
run_unittest = True
# Default all modes to true unless there's a flag
if flag_schema or flag_unittest:
# Then we only allow a few modes
if not flag_schema:
run_schema = False
if not flag_unittest:
run_unittest = False

if run_schema:
cyanobyte_validate(input_files)
if run_unittest:
unittest(input_files)

if __name__ == "__main__":
click_validate(sys.argv[1:])
Loading

0 comments on commit 88897ab

Please sign in to comment.