diff --git a/ci/ci-install.bash b/ci/ci-install.bash index b268794ec..b3ae2e06d 100755 --- a/ci/ci-install.bash +++ b/ci/ci-install.bash @@ -40,12 +40,32 @@ if [ "$CI_OS_NAME" = "linux" ]; then echo "path-exclude /usr/share/info/*" | sudo tee -a /etc/dpkg/dpkg.cfg.d/01_nodoc fi -install-vcddiff() { - TMP_DIR="$(mktemp -d)" - git clone https://github.com/veripool/vcddiff "$TMP_DIR" - git -C "${TMP_DIR}" checkout 4db0d84a27e8f148b127e916fc71d650837955c5 - "$MAKE" -C "${TMP_DIR}" - sudo cp "${TMP_DIR}/vcddiff" /usr/local/bin +install-wavediff() { + source ci/docker/buildenv/wavetools.conf + local _base_url="https://github.com/hudson-trading/wavetools/releases/download/${WAVETOOLS_VERSION}" + local _platform + if [ "$CI_OS_NAME" = "linux" ]; then + _platform="linux-x86_64" + elif [ "$CI_OS_NAME" = "osx" ]; then + _platform="macos-arm64" + elif [ "$CI_OS_NAME" = "windows" ]; then + _platform="windows-x86_64" + else + echo "WARNING: No wavetools binary available for CI_OS_NAME=$CI_OS_NAME, skipping" + return 0 + fi + local _tmpdir + _tmpdir=$(mktemp -d) + local _archive="wavetools-${WAVETOOLS_VERSION}-${_platform}" + if [ "$CI_OS_NAME" = "windows" ]; then + wget -q -O "${_tmpdir}/${_archive}.zip" "${_base_url}/${_archive}.zip" + unzip -o "${_tmpdir}/${_archive}.zip" -d "${_tmpdir}" + else + wget -q -O "${_tmpdir}/${_archive}.tar.gz" "${_base_url}/${_archive}.tar.gz" + tar -xzf "${_tmpdir}/${_archive}.tar.gz" -C "${_tmpdir}" + fi + sudo cp "${_tmpdir}/${_archive}/wavediff" /usr/local/bin/wavediff + rm -rf "${_tmpdir}" } if [ "$CI_BUILD_STAGE_NAME" = "build" ]; then @@ -120,7 +140,7 @@ elif [ "$CI_BUILD_STAGE_NAME" = "test" ]; then fatal "Unknown CI_OS_NAME: '$CI_OS_NAME'" fi # Common installs - install-vcddiff + install-wavediff # Workaround -fsanitize=address crash sudo sysctl -w vm.mmap_rnd_bits=28 else diff --git a/ci/docker/buildenv/Dockerfile b/ci/docker/buildenv/Dockerfile index d205c08e2..27a58073a 100644 --- a/ci/docker/buildenv/Dockerfile +++ b/ci/docker/buildenv/Dockerfile @@ -53,10 +53,14 @@ RUN apt-get update \ WORKDIR /tmp -RUN git clone https://github.com/veripool/vcddiff.git && \ - make -C vcddiff && \ - cp -p vcddiff/vcddiff /usr/local/bin/vcddiff && \ - rm -rf vcddiff +COPY wavetools.conf /tmp/wavetools.conf +ARG WGET_EXTRA_ARGS= +RUN . /tmp/wavetools.conf && \ + wget -q ${WGET_EXTRA_ARGS} -O /tmp/wavetools.tar.gz \ + "https://github.com/hudson-trading/wavetools/releases/download/${WAVETOOLS_VERSION}/wavetools-${WAVETOOLS_VERSION}-linux-x86_64.tar.gz" && \ + tar -xzf /tmp/wavetools.tar.gz -C /tmp && \ + cp -p /tmp/wavetools-${WAVETOOLS_VERSION}-linux-x86_64/wavediff /usr/local/bin/wavediff && \ + rm -rf /tmp/wavetools.tar.gz /tmp/wavetools-* /tmp/wavetools.conf COPY build.sh /tmp/build.sh diff --git a/ci/docker/buildenv/wavetools.conf b/ci/docker/buildenv/wavetools.conf new file mode 100644 index 000000000..d327112c5 --- /dev/null +++ b/ci/docker/buildenv/wavetools.conf @@ -0,0 +1 @@ +WAVETOOLS_VERSION=v0.1.2 diff --git a/docs/internals.rst b/docs/internals.rst index 02f6ed0c0..519a52a94 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -1568,8 +1568,8 @@ For all tests to pass, you must install the following packages: - SystemC to compile the SystemC outputs, see https://systemc.org -- vcddiff to find differences in VCD outputs. See the readme at - https://github.com/veripool/vcddiff +- wavediff to find differences in waveform outputs. See the readme at + https://github.com/hudson-trading/wavetools - Cmake for build paths that use it. diff --git a/docs/spelling.txt b/docs/spelling.txt index 80b6c176f..36db92d3a 100644 --- a/docs/spelling.txt +++ b/docs/spelling.txt @@ -1260,6 +1260,7 @@ vpm vpp warmup wavealloca +wavediff waveforms whitespace widthed diff --git a/src/V3ProtectLib.cpp b/src/V3ProtectLib.cpp index c7be84580..49aa38f56 100644 --- a/src/V3ProtectLib.cpp +++ b/src/V3ProtectLib.cpp @@ -221,7 +221,7 @@ class ProtectVisitor final : public VNVisitor { + "_protectlib_final(chandle handle__V);\n\n"); // Local variables - // Avoid tracing handle, as it is not a stable value, so breaks vcddiff + // Avoid tracing handle, as it is not a stable value, so breaks wavediff // Likewise other internals aren't interesting to the user txtp->add("// verilator tracing_off\n"); diff --git a/test_regress/driver.py b/test_regress/driver.py index 9489e1c8f..11d9a2a4b 100755 --- a/test_regress/driver.py +++ b/test_regress/driver.py @@ -7,7 +7,6 @@ import collections import ctypes import glob import hashlib -import json import logging import multiprocessing import os @@ -2511,104 +2510,19 @@ class VlTest: print("%Warning: HARNESS_UPDATE_GOLDEN set: cp " + fn1 + " " + fn2, file=sys.stderr) shutil.copy(fn1, fn2) - def vcd_identical(self, fn1: str, fn2: str, ignore_attr: bool = False) -> None: - """Test if two VCD files have logically-identical contents""" - # vcddiff to check transitions, if installed - cmd = "vcddiff --help" - out = test.run_capture(cmd, check=True) - cmd = 'vcddiff ' + fn1 + ' ' + fn2 - out = test.run_capture(cmd, check=True) - if out != "": - cmd = 'vcddiff ' + fn2 + " " + fn1 # Reversed arguments - out = VtOs.run_capture(cmd, check=False) - if out != "": - print(out) - self.copy_if_golden(fn1, fn2) - self.error("VCD miscompares " + fn2 + " " + fn1) - - # vcddiff doesn't check module and variable scope, so check that - # Also provides backup if vcddiff not installed - h1 = self._vcd_read(fn1) - h2 = self._vcd_read(fn2) - if ignore_attr: - h1 = {k: v for k, v in h1.items() if "$attr" not in v} - h2 = {k: v for k, v in h2.items() if "$attr" not in v} - a = json.dumps(h1, sort_keys=True, indent=1) - b = json.dumps(h2, sort_keys=True, indent=1) - if a != b: + def vcd_identical(self, fn1: str, fn2: str) -> None: + """Test if two VCD/FST files have logically-identical contents""" + cmd = VtOs.getenv_def('WAVEDIFF', 'wavediff') + ' --epsilon 0.0000001 ' + fn1 + ' ' + fn2 + proc = subprocess.run([cmd], capture_output=True, text=True, shell=True, check=False) + if proc.returncode: + print(proc.stderr) + print(proc.stdout) self.copy_if_golden(fn1, fn2) - self.error("VCD hier miscompares " + fn1 + " " + fn2 + "\nGOT=" + a + "\nEXP=" + b + - "\n") + self.error("VCD miscompares " + fn1 + " " + fn2) - def fst2vcd(self, fn1: str, fn2: str) -> None: - cmd = "fst2vcd -h" - out = VtOs.run_capture(cmd, check=False) - if out == "" or not re.search(r'Usage:', out): - self.skip("No fst2vcd installed") - return - - cmd = 'fst2vcd -e -f "' + fn1 + '" -o "' + fn2 + '"' - print("\t " + cmd + "\n") # Always print to help debug race cases - out = VtOs.run_capture(cmd, check=False) - print(out) - - # Post-process file and fix up to match upcoming fst2vcd output - # also reindent for readability - - # Slurp whole file - with open(fn2, 'r', encoding='latin-1') as fd: - lines = fd.readlines() - - # Process line by line - new_lines = [] - fixup_array_scope = False - indent = "" - for line in lines: - line = line.strip() - # Change "$attrbegin class" to "$attrbegin pack" - if match := re.match(r'^(\$attrbegin\s+)class(.*)', line): - line = indent + match.group(1) + "pack" + match.group(2) - # Check for "$attrbegin array" - elif re.search(r'^\$attrbegin\s+array', line): - line = indent + line - fixup_array_scope = True - # Check for "$scope" - elif match := re.match(r'(\$scope\s)(\S+)(.*)', line.lstrip('\r\n')): - if not indent: - indent = " " - # Fix up array scope - if (match.group(2) == "module") and fixup_array_scope: - line = indent + match.group(1) + "sv_array" + match.group(3) - fixup_array_scope = False - else: - line = indent + line - indent += " " - # Check for "$upscope" - elif re.search(r'^\$upscope', line): - indent = indent[0:-1] - line = indent + line - if len(indent) == 1: - indent = "" - # Just reindent - else: - line = indent + line - new_lines.append(line + "\n") - - # Write back to file - with open(fn2, 'w', encoding='latin-1') as fd: - fd.writelines(new_lines) - - def fst_identical(self, fn1: str, fn2: str, ignore_attr: bool = False) -> None: + def fst_identical(self, fn1: str, fn2: str) -> None: """Test if two FST files have logically-identical contents""" - if fn1.endswith(".fst"): - tmp = fn1 + ".vcd" - self.fst2vcd(fn1, tmp) - fn1 = tmp - if fn2.endswith(".fst"): - tmp = fn2 + ".vcd" - self.fst2vcd(fn2, tmp) - fn2 = tmp - self.vcd_identical(fn1, fn2, ignore_attr) + self.vcd_identical(fn1, fn2) def saif_identical(self, fn1: str, fn2: str) -> None: """Test if two SAIF files have logically-identical contents""" @@ -2621,96 +2535,17 @@ class VlTest: self.copy_if_golden(fn1, fn2) self.error("SAIF files miscompare") - def trace_identical(self, traceFn: str, goldenFn: str, ignore_attr: bool = False) -> None: + def trace_identical(self, traceFn: str, goldenFn: str) -> None: match traceFn.rpartition(".")[-1]: case "vcd": - self.vcd_identical(traceFn, goldenFn, ignore_attr) + self.vcd_identical(traceFn, goldenFn) case "fst": - self.fst_identical(traceFn, goldenFn, ignore_attr) + self.fst_identical(traceFn, goldenFn) case "saif": self.saif_identical(traceFn, goldenFn) case _: self.error("Unknown trace file format " + traceFn) - @staticmethod - def _vcd_parse_header(filename: str, root_scope: 'str | None' = None) -> 'tuple[dict, dict]': - """Parse VCD header into hierarchy data and signal-code mapping. - - Returns (hier_data, var_codes) where: - hier_data: dict used by _vcd_read for hierarchy comparison - (scope paths -> scope description, var paths -> $var prefix, - attr paths -> $attrbegin text) - var_codes: dict mapping 'scope.signal' -> VCD code identifier - """ - hier_data: dict = {} - var_codes: dict = {} - hier_stack = [root_scope] if root_scope else [] - attr: list = [] - with open(filename, 'r', encoding='latin-1') as fh: - for line in fh: - m_scope = re.search(r'\$scope\s+(\S*)\s+(\S+)', line) - m_var = re.search(r'(\$var\s+\S+\s+\d+\s+)(\S+)\s+(.+)\s+\$end', line) - m_attr = re.search(r'(\$attrbegin .* \$end)', line) - if m_scope: - scope_type = m_scope.group(1) - name = m_scope.group(2) - hier_stack.append(name) - scope = '.'.join(hier_stack) - hier_data[scope] = scope_type + " " + name - if attr: - hier_data[scope + "#"] = " ".join(attr) - attr = [] - elif m_var: - scope = '.'.join(hier_stack) - decl_prefix = m_var.group(1) - code = m_var.group(2) - var_name = m_var.group(3) - hier_data[scope + "." + var_name] = decl_prefix - if attr: - hier_data[scope + "." + var_name + "#"] = " ".join(attr) - attr = [] - bare_name = var_name.split()[0] - var_codes[scope + "." + bare_name] = code - elif m_attr: - attr.append(m_attr.group(1)) - elif re.search(r'\$enddefinitions', line): - break - n = len(re.findall(r'\$upscope', line)) - if n: - for _i in range(0, n): - hier_stack.pop() - return hier_data, var_codes - - def _vcd_read(self, filename: str) -> dict: - data, _ = self._vcd_parse_header(filename, root_scope="TOP") - return data - - def vcd_extract_codes(self, filename: str) -> dict: - _, codes = self._vcd_parse_header(filename) - return codes - - def vcd_check_aliased(self, codes: dict, sig_a: str, sig_b: str) -> None: - code_a = codes.get(sig_a) - code_b = codes.get(sig_b) - if code_a is None: - self.error(f"Signal '{sig_a}' not found in VCD") - if code_b is None: - self.error(f"Signal '{sig_b}' not found in VCD") - if code_a != code_b: - self.error(f"Expected '{sig_a}' (code {code_a}) to alias " - f"'{sig_b}' (code {code_b})") - - def vcd_check_not_aliased(self, codes: dict, sig_a: str, sig_b: str) -> None: - code_a = codes.get(sig_a) - code_b = codes.get(sig_b) - if code_a is None: - self.error(f"Signal '{sig_a}' not found in VCD") - if code_b is None: - self.error(f"Signal '{sig_b}' not found in VCD") - if code_a == code_b: - self.error(f"Expected '{sig_a}' and '{sig_b}' to have different codes, " - f"both have code {code_a}") - def inline_checks(self) -> None: covfn = self.coverage_filename contents = self.file_contents(covfn) diff --git a/test_regress/t/t_dist_copyright.py b/test_regress/t/t_dist_copyright.py index 53ee776c6..e9aab9c58 100755 --- a/test_regress/t/t_dist_copyright.py +++ b/test_regress/t/t_dist_copyright.py @@ -26,6 +26,7 @@ EXEMPT_FILES_LIST = """ REUSE.toml ci/ci-win-compile.ps1 ci/ci-win-test.ps1 + ci/docker/buildenv/wavetools.conf ci/codecov docs/CONTRIBUTORS docs/_static diff --git a/test_regress/t/trace_hier_block_common.py b/test_regress/t/trace_hier_block_common.py index c92175c61..e634b5e28 100644 --- a/test_regress/t/trace_hier_block_common.py +++ b/test_regress/t/trace_hier_block_common.py @@ -93,8 +93,7 @@ def run(test, *, verilator_flags2=()): if "enddefinitions" in la: break - # The two models must match ignoring enum attributes which can differ - test.trace_identical(trace_hier, trace_nonh, ignore_attr=True) + test.trace_identical(trace_hier, trace_nonh) # The hierarchical must match the reference test.trace_identical(trace_hier, test.golden_filename) diff --git a/test_regress/t/trace_lib_common.py b/test_regress/t/trace_lib_common.py index 785cf38cb..dc99a1867 100644 --- a/test_regress/t/trace_lib_common.py +++ b/test_regress/t/trace_lib_common.py @@ -107,8 +107,7 @@ def run(test, *, verilator_flags2=()): if "enddefinitions" in la: break - # The two models must match ignoring enum attributes which can differ - test.trace_identical(trace_libs, trace_nonl, ignore_attr=True) + test.trace_identical(trace_libs, trace_nonl) # The --lib-create must match the reference test.trace_identical(trace_libs, test.golden_filename) diff --git a/test_regress/t/trace_struct_alias_common.py b/test_regress/t/trace_struct_alias_common.py index e5e418f68..df38a1266 100644 --- a/test_regress/t/trace_struct_alias_common.py +++ b/test_regress/t/trace_struct_alias_common.py @@ -7,6 +7,55 @@ # SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0 +def _vcd_extract_codes(filename): + """Parse VCD header and return dict mapping 'scope.signal' -> code.""" + codes = {} + scope_stack = [] + with open(filename, 'r', encoding='latin-1') as fh: + for line in fh: + line = line.strip() + if line.startswith("$scope"): + parts = line.split() + # $scope $end + scope_stack.append(parts[2]) + elif line.startswith("$var"): + parts = line.split() + # $var ... $end + code = parts[3] + name = parts[4] + full = '.'.join(scope_stack) + '.' + name + codes[full] = code + elif line.startswith("$upscope"): + scope_stack.pop() + elif line.startswith("$enddefinitions"): + break + return codes + + +def _check_aliased(test, codes, sig_a, sig_b): + code_a = codes.get(sig_a) + code_b = codes.get(sig_b) + if code_a is None: + test.error(f"Signal '{sig_a}' not found in VCD") + if code_b is None: + test.error(f"Signal '{sig_b}' not found in VCD") + if code_a != code_b: + test.error(f"Expected '{sig_a}' (code {code_a}) to alias " + f"'{sig_b}' (code {code_b})") + + +def _check_not_aliased(test, codes, sig_a, sig_b): + code_a = codes.get(sig_a) + code_b = codes.get(sig_b) + if code_a is None: + test.error(f"Signal '{sig_a}' not found in VCD") + if code_b is None: + test.error(f"Signal '{sig_b}' not found in VCD") + if code_a == code_b: + test.error(f"Expected '{sig_a}' and '{sig_b}' to have different codes, " + f"both have code {code_a}") + + def run(test): (fmt, ) = test.parse_name(r"t_trace_struct_alias_([a-z]+)") @@ -20,16 +69,16 @@ def run(test): test.execute() if fmt == "vcd": - codes = test.vcd_extract_codes(test.trace_filename) + codes = _vcd_extract_codes(test.trace_filename) - test.vcd_check_not_aliased(codes, "top.t.s1.a", "top.t.s2.a") + _check_not_aliased(test, codes, "top.t.s1.a", "top.t.s2.a") - test.vcd_check_aliased(codes, "top.t.s3.a", "top.t.alias_of_s3a") + _check_aliased(test, codes, "top.t.s3.a", "top.t.alias_of_s3a") - test.vcd_check_aliased(codes, "top.t.s4.a", "top.t.s5.a") - test.vcd_check_aliased(codes, "top.t.s4.b", "top.t.s5.b") + _check_aliased(test, codes, "top.t.s4.a", "top.t.s5.a") + _check_aliased(test, codes, "top.t.s4.b", "top.t.s5.b") - test.vcd_check_aliased(codes, "top.t.s6.a", "top.t.source_val") + _check_aliased(test, codes, "top.t.s6.a", "top.t.source_val") test.trace_identical(test.trace_filename, test.golden_filename) diff --git a/test_regress/t/trace_struct_array_multi_inst_common.py b/test_regress/t/trace_struct_array_multi_inst_common.py index e22d1f83c..937d399ff 100644 --- a/test_regress/t/trace_struct_array_multi_inst_common.py +++ b/test_regress/t/trace_struct_array_multi_inst_common.py @@ -74,10 +74,8 @@ def run(test): test.trace_identical(test.trace_filename, test.golden_filename) - if fmt == "fst": - _check_empty_scopes_vcd(test, test.trace_filename + ".vcd") - elif fmt == "vcd": - _check_empty_scopes_vcd(test, test.trace_filename) + if fmt in ("fst", "vcd"): + _check_empty_scopes_vcd(test, test.golden_filename) elif fmt == "saif": _check_empty_scopes_saif(test, test.trace_filename)