diff --git a/.gitignore b/.gitignore
index 97033368..9652cf77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,7 +22,6 @@ __pycache__
# Unit test / coverage reports
.coverage
-.tox
# Translations
*.mo
@@ -46,9 +45,6 @@ docs/_build
# There must be a very good reason for someone to add a verilog file to the repo
.v
-# ipynb
-ipynb-examples/.ipynb_checkpoints
-
# VS Code
.vscode
@@ -56,5 +52,6 @@ ipynb-examples/.ipynb_checkpoints
pyvenv.cfg
# Jupyter
+.ipynb_checkpoints
.jupyterlite.doit.db
_output
diff --git a/docs/export.rst b/docs/export.rst
index 99ffb22e..35fa67d7 100644
--- a/docs/export.rst
+++ b/docs/export.rst
@@ -28,5 +28,5 @@ Outputting for Visualization
.. autofunction:: pyrtl.output_to_svg
.. autofunction:: pyrtl.block_to_graphviz_string
.. autofunction:: pyrtl.block_to_svg
-.. autofunction:: pyrtl.trace_to_html
+.. autofunction:: pyrtl.trace_to_json
.. autofunction:: pyrtl.net_graph
diff --git a/examples/Makefile b/examples/Makefile
index 71b0c703..ae614623 100644
--- a/examples/Makefile
+++ b/examples/Makefile
@@ -1,5 +1,5 @@
PYTHON=uv run
-PY_FILES=$(wildcard *.py)
+PY_FILES=$(wildcard example*.py) introduction-to-hardware.py
IPYNB_FILES=$(addprefix ../ipynb-examples/, $(PY_FILES:.py=.ipynb))
all: $(IPYNB_FILES)
diff --git a/examples/example8-verilog.py b/examples/example8-verilog.py
index 1aca987d..d5838a78 100644
--- a/examples/example8-verilog.py
+++ b/examples/example8-verilog.py
@@ -67,9 +67,7 @@
{"x": random.randrange(2), "y": random.randrange(2), "cin": random.randrange(2)}
)
# Only display the `Input` and `Output` `WireVectors` for clarity.
-input_vectors = pyrtl.working_block().wirevector_subset(pyrtl.Input)
-output_vectors = pyrtl.working_block().wirevector_subset(pyrtl.Output)
-sim.tracer.render_trace(trace_list=[*input_vectors, *output_vectors], symbol_len=2)
+sim.tracer.render_trace(trace_list=["x", "y", "cin", "sum", "cout"], symbol_len=2)
# ## Exporting to Verilog
#
diff --git a/ipynb-examples/example8-verilog.ipynb b/ipynb-examples/example8-verilog.ipynb
index 5745a90e..3e566613 100644
--- a/ipynb-examples/example8-verilog.ipynb
+++ b/ipynb-examples/example8-verilog.ipynb
@@ -149,9 +149,7 @@
},
"outputs": [],
"source": [
- "input_vectors = pyrtl.working_block().wirevector_subset(pyrtl.Input)\n",
- "output_vectors = pyrtl.working_block().wirevector_subset(pyrtl.Output)\n",
- "sim.tracer.render_trace(trace_list=[*input_vectors, *output_vectors], symbol_len=2)\n"
+ "sim.tracer.render_trace(trace_list=[\"x\", \"y\", \"cin\", \"sum\", \"cout\"], symbol_len=2)\n"
]
},
{
diff --git a/ipynb-examples/renderer-demo.ipynb b/ipynb-examples/renderer-demo.ipynb
deleted file mode 100644
index aeae46b6..00000000
--- a/ipynb-examples/renderer-demo.ipynb
+++ /dev/null
@@ -1,173 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- " # Render traces with various `WaveRenderer` options.\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- " Run this demo to see which options work well in your terminal.\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
- "source": [
- "%pip install pyrtl\n",
- "\n",
- "import pyrtl\n",
- "\n",
- "\n",
- "pyrtl.reset_working_block()\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- " Make some clock dividers and counters.\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
- "source": [
- "def make_clock(period: int):\n",
- " \"\"\"Make a clock signal that inverts every `period` cycles.\"\"\"\n",
- " assert period > 0\n",
- "\n",
- " # Build a chain of registers.\n",
- " first_reg = pyrtl.Register(bitwidth=1, name=f\"clock_0_{period}\", reset_value=1)\n",
- " last_reg = first_reg\n",
- " for offset in range(1, period):\n",
- " reg = pyrtl.Register(bitwidth=1, name=f\"clock_{offset}_{period}\")\n",
- " reg.next <<= last_reg\n",
- " last_reg = reg\n",
- "\n",
- " # The first register's input is the inverse of the last register's output.\n",
- " first_reg.next <<= ~last_reg\n",
- " return last_reg\n",
- "\n",
- "\n",
- "def make_counter(period: int, bitwidth: int = 2):\n",
- " \"\"\"Make a counter that increments every `period` cycles.\"\"\"\n",
- " assert period > 0\n",
- "\n",
- " # Build a chain of registers.\n",
- " first_reg = pyrtl.Register(bitwidth=bitwidth, name=f\"counter_0_{period}\")\n",
- " last_reg = first_reg\n",
- " for offset in range(1, period):\n",
- " reg = pyrtl.Register(bitwidth=bitwidth, name=f\"counter_{offset}_{period}\")\n",
- " reg.next <<= last_reg\n",
- " last_reg = reg\n",
- "\n",
- " # The first register's input is the last register's output plus 1.\n",
- " first_reg.next <<= last_reg + pyrtl.Const(1)\n",
- " return last_reg\n",
- "\n",
- "\n",
- "make_clock(period=1)\n",
- "make_clock(period=2)\n",
- "make_counter(period=1)\n",
- "make_counter(period=2)\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- " Simulate 20 cycles.\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
- "source": [
- "sim = pyrtl.Simulation()\n",
- "sim.step_multiple(nsteps=20)\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- " Render the trace with a variety of rendering options.\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "collapsed": true
- },
- "outputs": [],
- "source": [
- "renderers = {\n",
- " \"powerline\": (\n",
- " pyrtl.simulation.PowerlineRendererConstants(),\n",
- " \"Requires a font with powerline glyphs\",\n",
- " ),\n",
- " \"utf-8\": (\n",
- " pyrtl.simulation.Utf8RendererConstants(),\n",
- " \"Unicode, default non-Windows renderer\",\n",
- " ),\n",
- " \"utf-8-alt\": (\n",
- " pyrtl.simulation.Utf8AltRendererConstants(),\n",
- " \"Unicode, alternate display option\",\n",
- " ),\n",
- " \"cp437\": (\n",
- " pyrtl.simulation.Cp437RendererConstants(),\n",
- " \"Code page 437 (8-bit ASCII), default Windows renderer\",\n",
- " ),\n",
- " \"ascii\": (pyrtl.simulation.AsciiRendererConstants(), \"Basic 7-bit ASCII renderer\"),\n",
- "}\n",
- "\n",
- "for name, (constants, notes) in renderers.items():\n",
- " print(f\"# {notes}\")\n",
- " print(f\"export PYRTL_RENDERER={name}\\n\")\n",
- " sim.tracer.render_trace(\n",
- " renderer=pyrtl.simulation.WaveRenderer(constants), repr_func=int\n",
- " )\n",
- " print()\n"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.6.4"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
\ No newline at end of file
diff --git a/pyrtl/__init__.py b/pyrtl/__init__.py
index ecb6a2b2..923d95ad 100644
--- a/pyrtl/__init__.py
+++ b/pyrtl/__init__.py
@@ -95,7 +95,7 @@
output_to_svg,
block_to_graphviz_string,
block_to_svg,
- trace_to_html,
+ trace_to_json,
net_graph,
)
@@ -236,7 +236,7 @@
"output_to_svg",
"block_to_graphviz_string",
"block_to_svg",
- "trace_to_html",
+ "trace_to_json",
"net_graph",
# importexport
"input_from_verilog",
diff --git a/pyrtl/simulation.py b/pyrtl/simulation.py
index 36687c38..65749d80 100644
--- a/pyrtl/simulation.py
+++ b/pyrtl/simulation.py
@@ -1030,6 +1030,37 @@ def make_split(source, split_length, split_start_bit, split_res_start_bit):
#
+def val_to_str(
+ value: int,
+ wire: WireVector,
+ repr_func: Callable[[int], str],
+ repr_per_name: dict[str, Callable[[int], str]],
+) -> str:
+ """Return a string representing 'value'.
+
+ :param value: The value to convert to string.
+ :param wire: Wire that produced this value.
+ :param repr_func: function to use for representing the current_val; examples are
+ 'hex', 'oct', 'bin', 'str' (for decimal), or the function returned by
+ :func:`enum_name`. Defaults to 'hex'.
+ :param repr_per_name: Map from signal name to a function that takes in the signal's
+ value and returns a user-defined representation. If a signal name is not found
+ in the map, the argument `repr_func` will be used instead.
+
+ :return: a string representing 'value'.
+ """
+ func = repr_per_name.get(wire.name)
+ if func is None:
+ if isinstance(wire, Register) and wire.State is not None:
+ func = enum_name(wire.State)
+ else:
+ func = repr_func
+
+ if func is val_to_signed_integer:
+ return str(val_to_signed_integer(value=value, bitwidth=wire.bitwidth))
+ return str(func(value))
+
+
class WaveRenderer:
"""Render a SimulationTrace to the terminal.
@@ -1077,39 +1108,6 @@ def render_ruler_segment(
# Pad major_tick out to segment_size.
return major_tick.ljust(cycle_len * segment_size)
- def val_to_str(
- self,
- value: int,
- wire: WireVector,
- repr_func: Callable[[int], str],
- repr_per_name: dict[str, Callable[[int], str]],
- ) -> str:
- """Return a string representing 'value'.
-
- :param value: The value to convert to string.
- :param wire: Wire that produced this value.
- :param repr_func: function to use for representing the current_val; examples are
- 'hex', 'oct', 'bin', 'str' (for decimal), or the function returned by
- :func:`enum_name`. Defaults to 'hex'.
- :param repr_per_name: Map from signal name to a function that takes in the
- signal's value and returns a user-defined representation. If a signal name
- is not found in the map, the argument `repr_func` will be used instead.
-
- :return: a string representing 'value'.
- """
- f = repr_per_name.get(wire.name)
-
- def invoke_f(f, value):
- if f is val_to_signed_integer:
- return str(val_to_signed_integer(value=value, bitwidth=wire.bitwidth))
- return str(f(value))
-
- if f is not None:
- return invoke_f(f, value)
- if isinstance(wire, Register) and wire.State is not None:
- return invoke_f(enum_name(wire.State), value)
- return invoke_f(repr_func, value)
-
def render_val(
self,
w: WireVector,
@@ -1181,7 +1179,7 @@ def render_val(
out += self.constants._bus_start
# Display the current non-zero value.
out += (
- self.val_to_str(current_val, w, repr_func, repr_per_name)
+ val_to_str(current_val, w, repr_func, repr_per_name)
.rstrip("L")
.ljust(symbol_len)[:symbol_len]
)
@@ -1768,15 +1766,23 @@ def render_trace(
display,
)
- from pyrtl.visualization import trace_to_html
+ from pyrtl.visualization import trace_to_json
display(
HTML(
- trace_to_html(self, trace_list=trace_list, sortkey=_trace_sort_key)
+ '\n"
),
HTML("""
-
-
+
+
"""),
# Wait for WaveDrom to load, polling every 100ms.
Javascript("""
@@ -1881,9 +1887,7 @@ def formatted_trace_line(wire, trace):
trace = self.trace[trace_name]
current_symbol_len = max(
len(
- renderer.val_to_str(
- v, self._wires[trace_name], repr_func, repr_per_name
- )
+ val_to_str(v, self._wires[trace_name], repr_func, repr_per_name)
)
for v in trace
)
diff --git a/pyrtl/visualization.py b/pyrtl/visualization.py
index 4286cac9..991edfe9 100644
--- a/pyrtl/visualization.py
+++ b/pyrtl/visualization.py
@@ -8,6 +8,7 @@
from __future__ import annotations
import collections
+import json
from collections.abc import Callable
from typing import TYPE_CHECKING
@@ -560,17 +561,42 @@ def block_to_svg(
# | | | | | |___
-def trace_to_html(
+def trace_to_json(
simtrace: SimulationTrace,
trace_list: list[str] | None = None,
sortkey=None,
repr_func: Callable[[int], str] = hex,
repr_per_name: dict[str, Callable[[int], str]] | None = None,
) -> str:
- """Return a HTML block showing the trace.
+ """Return a `WaveJSON `_
+ representation of a :class:`.SimulationTrace`.
- :param simtrace: A trace to render in HTML.
- :param trace_list: (optional) A list of wires to display.
+ The returned WaveJSON is compatible with `WaveDrom
+ `_; see the `WaveDrom README
+ `_ for various
+ ways to visualize WaveJSON.
+
+ .. doctest only::
+
+ >>> import pyrtl
+ >>> pyrtl.reset_working_block()
+
+ Example::
+
+ >>> counter = pyrtl.Register(name="counter", bitwidth=2)
+ >>> counter.next <<= counter + 1
+
+ >>> sim = pyrtl.Simulation()
+ >>> sim.step_multiple(nsteps=4)
+
+ >>> print(pyrtl.trace_to_json(sim.tracer, repr_func=str))
+ {"signal": [{"name": "counter", "wave": "====", "data": ["0", "1", "2", "3"]}], "config": {"hscale": 1}, "head": {"tick": 0}}
+
+ Try pasting the WaveJSON output from the example above in the `WaveDrom editor
+ `_.
+
+ :param simtrace: A trace to convert to WaveJSON.
+ :param trace_list: (optional) A list of wires to include in the WaveJSON.
:param sortkey: (optional) The key with which to sort the ``trace_list``.
:param repr_func: Function to use for representing each value in the trace. Examples
include :func:`hex`, :func:`oct`, :func:`bin`, and :class:`str` (for decimal),
@@ -580,16 +606,12 @@ def trace_to_html(
value and returns a user-defined representation. If a signal name is not found
in the map, the argument ``repr_func`` will be used instead.
- :return: An HTML block showing the trace.
- """
-
- from pyrtl.simulation import SimulationTrace, _trace_sort_key
+ :return: WaveJSON representing ``simtrace``.
+ """ # noqa: E501
+ from pyrtl.simulation import _trace_sort_key, val_to_str
if repr_per_name is None:
repr_per_name = {}
- if not isinstance(simtrace, SimulationTrace):
- msg = "first arguement must be of type SimulationTrace"
- raise PyrtlError(msg)
trace = simtrace.trace
if sortkey is None:
@@ -598,54 +620,41 @@ def trace_to_html(
if trace_list is None:
trace_list = sorted(trace, key=sortkey)
- wave_template = """\
-
-"""
+ signals = []
+ # Length of the longest value string, in characters.
+ max_value_length = 1
+ for signal_name in trace_list:
+ # Make a WaveDrom WaveLane for ``signal_name``.
+ wave_list = []
+ data_list = []
+ last_value = None
+
+ wire = simtrace._wires[signal_name]
+ for value in trace[signal_name]:
+ if last_value == value:
+ wave_list.append(".")
+ continue
+
+ if len(wire) == 1:
+ # int() to convert True/False to 0/1.
+ wave_list.append(str(int(value)))
+ else:
+ wave_list.append("=")
+ data = val_to_str(value, wire, repr_func, repr_per_name)
+ data_list.append(data)
+ max_value_length = max(max_value_length, len(data))
- vallens = [] # For determining longest value length
+ last_value = value
- def extract(w):
- wavelist = []
- datalist = []
- last = None
+ signal = {"name": signal_name, "wave": "".join(wave_list)}
+ if len(data_list) > 0:
+ signal["data"] = data_list
+ signals.append(signal)
- for value in trace[w]:
- if last == value:
- wavelist.append(".")
- else:
- f = repr_per_name.get(w)
- if f is not None:
- wavelist.append("=")
- datalist.append(str(f(value)))
- elif len(simtrace._wires[w]) == 1:
- # int() to convert True/False to 0/1
- wavelist.append(str(int(value)))
- else:
- wavelist.append("=")
- datalist.append(str(repr_func(value)))
-
- last = value
-
- wavestring = "".join(wavelist)
- datastring = ", ".join([f'"{data}"' for data in datalist])
- if repr_per_name.get(w) is None and len(simtrace._wires[w]) == 1:
- vallens.append(1) # all are the same length
- return bool_signal_template % (w, wavestring)
- vallens.extend([len(data) for data in datalist])
- return int_signal_template % (w, wavestring, datastring)
-
- bool_signal_template = ' { name: "%s", wave: "%s" },'
- int_signal_template = ' { name: "%s", wave: "%s", data: [%s] },'
- signals = [extract(w) for w in trace_list]
- all_signals = "\n".join(signals)
- maxvallen = max(vallens)
- scale = (maxvallen // 5) + 1
- return wave_template % (all_signals, scale)
- # print(wave)
+ # WaveDrom doesn't automatically scale cycle width to fit `data`, so we use `hscale`
+ # to manually adjust cycle width. `hscale: 1` fits about three characters of `data`
+ # in a cycle. `hscale` must be an integer.
+ hscale = max_value_length // 4 + 1
+
+ wave_json = {"signal": signals, "config": {"hscale": hscale}, "head": {"tick": 0}}
+ return json.dumps(wave_json)
diff --git a/tests/test_simulation.py b/tests/test_simulation.py
index 681f11ff..2205bc79 100644
--- a/tests/test_simulation.py
+++ b/tests/test_simulation.py
@@ -330,7 +330,7 @@ class Foo(enum.IntEnum):
D = 3
i = pyrtl.Input(4, "i")
- state = pyrtl.Register(max(Foo).bit_length(), name="state")
+ state = pyrtl.Register(name="state", State=Foo)
o = pyrtl.Output(name="o")
o <<= state
@@ -350,7 +350,6 @@ class Foo(enum.IntEnum):
sim.tracer.render_trace(
file=buff,
renderer=self.renderer,
- repr_per_name={"state": pyrtl.enum_name(Foo)},
)
expected = (
" |0 |1 |2 |3 |4 \n"
diff --git a/tests/test_visualization.py b/tests/test_visualization.py
index 2c3a9782..c67a18c3 100644
--- a/tests/test_visualization.py
+++ b/tests/test_visualization.py
@@ -1,3 +1,4 @@
+import doctest
import enum
import io
import random
@@ -5,6 +6,16 @@
import pyrtl
+
+class TestDocTests(unittest.TestCase):
+ """Test documentation examples."""
+
+ def test_doctests(self):
+ failures, tests = doctest.testmod(m=pyrtl.visualization)
+ self.assertGreater(tests, 0)
+ self.assertEqual(failures, 0)
+
+
graphviz_string_detailed = """\
digraph g {
graph [splines="spline", outputorder="edgesfirst"];
@@ -348,51 +359,41 @@ def test_one_bit_adder_matches_expected(self):
}
)
- pyrtl.trace_to_html(sim.tracer) # tests if it compiles or not
+ pyrtl.trace_to_json(sim.tracer) # tests if it compiles or not
- def test_trace_to_html(self):
+ def test_trace_to_json(self):
i = pyrtl.Input(1, "i")
o = pyrtl.Output(2, "o")
o <<= i + 1
sim = pyrtl.Simulation()
sim.step_multiple({"i": "0100110"})
- htmlstring = pyrtl.trace_to_html(sim.tracer)
+ json_string = pyrtl.trace_to_json(sim.tracer)
expected = (
- '\n"
+ '{"signal": ['
+ '{"name": "i", "wave": "010.1.0"}, '
+ '{"name": "o", "wave": "===.=.=", "data": ["0x1", "0x2", "0x1", "0x2", "0x1"]}], ' # noqa: E501
+ '"config": {"hscale": 1}, "head": {"tick": 0}}'
)
- self.assertEqual(htmlstring, expected)
+ self.assertEqual(json_string, expected)
- def test_trace_to_html_repr_func(self):
+ def test_trace_to_json_repr_func(self):
i = pyrtl.Input(1, "i")
o = pyrtl.Output(2, "o")
o <<= i + 1
sim = pyrtl.Simulation()
sim.step_multiple({"i": "0100110"})
- htmlstring = pyrtl.trace_to_html(sim.tracer, repr_func=bin)
+ json_string = pyrtl.trace_to_json(sim.tracer, repr_func=bin)
expected = (
- '\n"
+ '{"signal": ['
+ '{"name": "i", "wave": "010.1.0"}, '
+ '{"name": "o", "wave": "===.=.=", "data": ["0b1", "0b10", "0b1", "0b10", "0b1"]}], ' # noqa: E501
+ '"config": {"hscale": 2}, "head": {"tick": 0}}'
)
- self.assertEqual(htmlstring, expected)
+ self.assertEqual(json_string, expected)
- def test_trace_to_html_repr_per_name(self):
+ def test_trace_to_json_repr_per_name(self):
class Foo(enum.IntEnum):
A = 0
B = 1
@@ -400,7 +401,7 @@ class Foo(enum.IntEnum):
D = 3
i = pyrtl.Input(4, "i")
- state = pyrtl.Register(max(Foo).bit_length(), name="state")
+ state = pyrtl.Register(name="state", State=Foo)
o = pyrtl.Output(name="o")
o <<= state
@@ -417,30 +418,23 @@ class Foo(enum.IntEnum):
sim = pyrtl.Simulation()
sim.step_multiple({"i": [1, 2, 4, 8, 0]})
- htmlstring = pyrtl.trace_to_html(
- sim.tracer, repr_per_name={"state": pyrtl.enum_name(Foo)}
- )
+ json_string = pyrtl.trace_to_json(sim.tracer)
expected = (
- '\n"
+ '{"signal": ['
+ '{"name": "i", "wave": "=====", "data": ["0x1", "0x2", "0x4", "0x8", "0x0"]}, ' # noqa: E501
+ '{"name": "o", "wave": "=.===", "data": ["0x0", "0x1", "0x2", "0x3"]}, '
+ '{"name": "state", "wave": "=.===", "data": ["A", "B", "C", "D"]}], '
+ '"config": {"hscale": 1}, "head": {"tick": 0}}'
)
- self.assertEqual(htmlstring, expected)
+ self.assertEqual(json_string, expected)
- def test_trace_to_html_repr_per_name_enum_is_bool(self):
+ def test_trace_to_json_repr_per_name_enum_is_bool(self):
class Foo(enum.IntEnum):
A = 0
B = 1
i = pyrtl.Input(2, "i")
- state = pyrtl.Register(max(Foo).bit_length(), name="state")
+ state = pyrtl.Register(name="state", State=Foo)
o = pyrtl.Output(name="o")
o <<= state
@@ -453,22 +447,15 @@ class Foo(enum.IntEnum):
sim = pyrtl.Simulation()
sim.step_multiple({"i": [1, 2, 1, 2, 2]})
- htmlstring = pyrtl.trace_to_html(
- sim.tracer, repr_per_name={"state": pyrtl.enum_name(Foo)}
- )
+ json_string = pyrtl.trace_to_json(sim.tracer)
expected = (
- '\n"
+ '{"signal": ['
+ '{"name": "i", "wave": "====.", "data": ["0x1", "0x2", "0x1", "0x2"]}, '
+ '{"name": "o", "wave": "0.101"}, '
+ '{"name": "state", "wave": "0.101"}], '
+ '"config": {"hscale": 1}, "head": {"tick": 0}}'
)
- self.assertEqual(htmlstring, expected)
+ self.assertEqual(json_string, expected)
if __name__ == "__main__":