123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442 |
- #!/usr/bin/env python3
- """
- Older Graphviz regression test suite that has been encapsulated
- TODO:
- Report differences with shared version and with new output.
- """
- import hashlib
- import io
- import platform
- import re
- import shutil
- import subprocess
- import sys
- from pathlib import Path
- from typing import List
- import pytest
- # Test specifications
- GRAPHDIR = Path(__file__).parent / "graphs"
- # Directory of input graphs and data
- OUTDIR = Path("ndata") # Directory for test output
- OUTHTML = Path("nhtml") # Directory for html test report
- class Case:
- """
- test case struct
- """
- def __init__(
- self,
- name: str,
- input: Path,
- algorithm: str,
- format: str,
- flags: List[str],
- ): # pylint: disable=too-many-arguments
- self.name = name
- self.input = input
- self.algorithm = algorithm
- self.format = format
- self.flags = flags[:]
- TESTS: List[Case] = [
- Case("shapes", Path("shapes.gv"), "dot", "gv", []),
- Case("shapes", Path("shapes.gv"), "dot", "ps", []),
- Case("crazy", Path("crazy.gv"), "dot", "png", []),
- Case("crazy", Path("crazy.gv"), "dot", "ps", []),
- Case("arrows", Path("arrows.gv"), "dot", "gv", []),
- Case("arrows", Path("arrows.gv"), "dot", "ps", []),
- Case("arrowsize", Path("arrowsize.gv"), "dot", "png", []),
- Case("center", Path("center.gv"), "dot", "ps", []),
- Case("center", Path("center.gv"), "dot", "png", ["-Gmargin=1"]),
- # color encodings
- # multiple edge colors
- Case("color", Path("color.gv"), "dot", "png", []),
- Case("color", Path("color.gv"), "dot", "png", ["-Gbgcolor=lightblue"]),
- Case("decorate", Path("decorate.gv"), "dot", "png", []),
- Case("record", Path("record.gv"), "dot", "gv", []),
- Case("record", Path("record.gv"), "dot", "ps", []),
- Case("html", Path("html.gv"), "dot", "gv", []),
- Case("html", Path("html.gv"), "dot", "ps", []),
- Case("html2", Path("html2.gv"), "dot", "gv", []),
- Case("html2", Path("html2.gv"), "dot", "ps", []),
- Case("html2", Path("html2.gv"), "dot", "svg", []),
- Case("pslib", Path("pslib.gv"), "dot", "ps", ["-lgraphs/sdl.ps"]),
- Case("user_shapes", Path("user_shapes.gv"), "dot", "ps", []),
- # dot png - doesn't work: Warning: No loadimage plugin for "gif:cairo"
- Case("user_shapes", Path("user_shapes.gv"), "dot", "png:gd", []),
- # bug - the epsf version has problems
- Case(
- "ps_user_shapes",
- Path("ps_user_shapes.gv"),
- "dot",
- "ps",
- ["-Nshapefile=graphs/dice.ps"],
- ),
- Case("colorscheme", Path("colorscheme.gv"), "dot", "ps", []),
- Case("colorscheme", Path("colorscheme.gv"), "dot", "png", []),
- Case("compound", Path("compound.gv"), "dot", "gv", []),
- Case("dir", Path("dir.gv"), "dot", "ps", []),
- Case("clusters", Path("clusters.gv"), "dot", "ps", []),
- Case("clusters", Path("clusters.gv"), "dot", "png", []),
- Case(
- "clustlabel",
- Path("clustlabel.gv"),
- "dot",
- "ps",
- ["-Glabelloc=t", "-Glabeljust=r"],
- ),
- Case(
- "clustlabel",
- Path("clustlabel.gv"),
- "dot",
- "ps",
- ["-Glabelloc=b", "-Glabeljust=r"],
- ),
- Case(
- "clustlabel",
- Path("clustlabel.gv"),
- "dot",
- "ps",
- ["-Glabelloc=t", "-Glabeljust=l"],
- ),
- Case(
- "clustlabel",
- Path("clustlabel.gv"),
- "dot",
- "ps",
- ["-Glabelloc=b", "-Glabeljust=l"],
- ),
- Case(
- "clustlabel",
- Path("clustlabel.gv"),
- "dot",
- "ps",
- ["-Glabelloc=t", "-Glabeljust=c"],
- ),
- Case(
- "clustlabel",
- Path("clustlabel.gv"),
- "dot",
- "ps",
- ["-Glabelloc=b", "-Glabeljust=c"],
- ),
- Case("clustlabel", Path("clustlabel.gv"), "dot", "ps", ["-Glabelloc=t"]),
- Case("clustlabel", Path("clustlabel.gv"), "dot", "ps", ["-Glabelloc=b"]),
- Case(
- "rootlabel",
- Path("rootlabel.gv"),
- "dot",
- "ps",
- ["-Glabelloc=t", "-Glabeljust=r"],
- ),
- Case(
- "rootlabel",
- Path("rootlabel.gv"),
- "dot",
- "ps",
- ["-Glabelloc=b", "-Glabeljust=r"],
- ),
- Case(
- "rootlabel",
- Path("rootlabel.gv"),
- "dot",
- "ps",
- ["-Glabelloc=t", "-Glabeljust=l"],
- ),
- Case(
- "rootlabel",
- Path("rootlabel.gv"),
- "dot",
- "ps",
- ["-Glabelloc=b", "-Glabeljust=l"],
- ),
- Case(
- "rootlabel",
- Path("rootlabel.gv"),
- "dot",
- "ps",
- ["-Glabelloc=t", "-Glabeljust=c"],
- ),
- Case(
- "rootlabel",
- Path("rootlabel.gv"),
- "dot",
- "ps",
- ["-Glabelloc=b", "-Glabeljust=c"],
- ),
- Case("rootlabel", Path("rootlabel.gv"), "dot", "ps", ["-Glabelloc=t"]),
- Case("rootlabel", Path("rootlabel.gv"), "dot", "ps", ["-Glabelloc=b"]),
- Case("layers", Path("layers.gv"), "dot", "ps", []),
- # check mode=hier
- Case("mode", Path("mode.gv"), "neato", "ps", ["-Gmode=KK"]),
- Case("mode", Path("mode.gv"), "neato", "ps", ["-Gmode=hier"]),
- Case("mode", Path("mode.gv"), "neato", "ps", ["-Gmode=hier", "-Glevelsgap=1"]),
- Case("model", Path("mode.gv"), "neato", "ps", ["-Gmodel=circuit"]),
- Case(
- "model", Path("mode.gv"), "neato", "ps", ["-Goverlap=false", "-Gmodel=subset"]
- ),
- # cairo versions have problems
- Case("nojustify", Path("nojustify.gv"), "dot", "png", []),
- Case("nojustify", Path("nojustify.gv"), "dot", "png:gd", []),
- Case("nojustify", Path("nojustify.gv"), "dot", "ps", []),
- Case("nojustify", Path("nojustify.gv"), "dot", "ps:cairo", []),
- # bug
- Case("ordering", Path("ordering.gv"), "dot", "gv", ["-Gordering=in"]),
- Case("ordering", Path("ordering.gv"), "dot", "gv", ["-Gordering=out"]),
- Case("overlap", Path("overlap.gv"), "neato", "gv", ["-Goverlap=false"]),
- Case("overlap", Path("overlap.gv"), "neato", "gv", ["-Goverlap=scale"]),
- Case("pack", Path("pack.gv"), "neato", "gv", []),
- Case("pack", Path("pack.gv"), "neato", "gv", ["-Gpack=20"]),
- Case("pack", Path("pack.gv"), "neato", "gv", ["-Gpackmode=graph"]),
- Case("page", Path("mode.gv"), "neato", "ps", ["-Gpage=8.5,11"]),
- Case("page", Path("mode.gv"), "neato", "ps", ["-Gpage=8.5,11", "-Gpagedir=TL"]),
- Case("page", Path("mode.gv"), "neato", "ps", ["-Gpage=8.5,11", "-Gpagedir=TR"]),
- # pencolor, fontcolor, fillcolor
- Case("colors", Path("colors.gv"), "dot", "ps", []),
- Case("polypoly", Path("polypoly.gv"), "dot", "ps", []),
- Case("polypoly", Path("polypoly.gv"), "dot", "png", []),
- Case("ports", Path("ports.gv"), "dot", "gv", []),
- Case("rotate", Path("crazy.gv"), "dot", "png", ["-Glandscape"]),
- Case("rotate", Path("crazy.gv"), "dot", "ps", ["-Glandscape"]),
- Case("rotate", Path("crazy.gv"), "dot", "png", ["-Grotate=90"]),
- Case("rotate", Path("crazy.gv"), "dot", "ps", ["-Grotate=90"]),
- Case("rankdir", Path("crazy.gv"), "dot", "gv", ["-Grankdir=LR"]),
- Case("rankdir", Path("crazy.gv"), "dot", "gv", ["-Grankdir=BT"]),
- Case("rankdir", Path("crazy.gv"), "dot", "gv", ["-Grankdir=RL"]),
- Case("url", Path("url.gv"), "dot", "ps2", []),
- Case("url", Path("url.gv"), "dot", "svg", ["-Gstylesheet=stylesheet"]),
- Case("url", Path("url.gv"), "dot", "imap", []),
- Case("url", Path("url.gv"), "dot", "cmapx", []),
- Case("url", Path("url.gv"), "dot", "imap_np", []),
- Case("url", Path("url.gv"), "dot", "cmapx_np", []),
- Case(
- "viewport", Path("viewport.gv"), "neato", "png", ["-Gviewport=300,300", "-n2"]
- ),
- Case("viewport", Path("viewport.gv"), "neato", "ps", ["-Gviewport=300,300", "-n2"]),
- Case(
- "viewport",
- Path("viewport.gv"),
- "neato",
- "png",
- ["-Gviewport=300,300,1,200,620", "-n2"],
- ),
- Case(
- "viewport",
- Path("viewport.gv"),
- "neato",
- "ps",
- ["-Gviewport=300,300,1,200,620", "-n2"],
- ),
- Case(
- "viewport",
- Path("viewport.gv"),
- "neato",
- "png",
- ["-Gviewport=300,300,2,200,620", "-n2"],
- ),
- Case(
- "viewport",
- Path("viewport.gv"),
- "neato",
- "ps",
- ["-Gviewport=300,300,2,200,620", "-n2"],
- ),
- Case("rowcolsep", Path("rowcolsep.gv"), "dot", "gv", ["-Gnodesep=0.5"]),
- Case("rowcolsep", Path("rowcolsep.gv"), "dot", "gv", ["-Granksep=1.5"]),
- Case("size", Path("mode.gv"), "neato", "ps", ["-Gsize=5,5"]),
- Case("size", Path("mode.gv"), "neato", "png", ["-Gsize=5,5"]),
- # size with !
- Case("size_ex", Path("root.gv"), "dot", "ps", ["-Gsize=6,6!"]),
- Case("size_ex", Path("root.gv"), "dot", "png", ["-Gsize=6,6!"]),
- Case("dotsplines", Path("size.gv"), "dot", "gv", ["-Gsplines=line"]),
- Case("dotsplines", Path("size.gv"), "dot", "gv", ["-Gsplines=polyline"]),
- Case(
- "neatosplines",
- Path("overlap.gv"),
- "neato",
- "gv",
- ["-Goverlap=false", "-Gsplines"],
- ),
- Case(
- "neatosplines",
- Path("overlap.gv"),
- "neato",
- "gv",
- ["-Goverlap=false", "-Gsplines=polyline"],
- ),
- Case("style", Path("style.gv"), "dot", "ps", []),
- Case("style", Path("style.gv"), "dot", "png", []),
- # edge clipping
- Case("edgeclip", Path("edgeclip.gv"), "dot", "gv", []),
- # edge weight
- Case("weight", Path("weight.gv"), "dot", "gv", []),
- Case("root", Path("root.gv"), "twopi", "gv", []),
- Case("cairo", Path("cairo.gv"), "dot", "ps:cairo", []),
- Case("cairo", Path("cairo.gv"), "dot", "png:cairo", []),
- Case("cairo", Path("cairo.gv"), "dot", "svg:cairo", []),
- Case("flatedge", Path("flatedge.gv"), "dot", "gv", []),
- Case("nestedclust", Path("nestedclust"), "dot", "gv", []),
- Case("rd_rules", Path("rd_rules.gv"), "dot", "png", []),
- Case("sq_rules", Path("sq_rules.gv"), "dot", "png", []),
- Case("fdp_clus", Path("fdp.gv"), "fdp", "png", []),
- Case("japanese", Path("japanese.gv"), "dot", "png", []),
- Case("russian", Path("russian.gv"), "dot", "png", []),
- Case("AvantGarde", Path("AvantGarde.gv"), "dot", "png", []),
- Case("AvantGarde", Path("AvantGarde.gv"), "dot", "ps", []),
- Case("Bookman", Path("Bookman.gv"), "dot", "png", []),
- Case("Bookman", Path("Bookman.gv"), "dot", "ps", []),
- Case("Helvetica", Path("Helvetica.gv"), "dot", "png", []),
- Case("Helvetica", Path("Helvetica.gv"), "dot", "ps", []),
- Case("NewCenturySchlbk", Path("NewCenturySchlbk.gv"), "dot", "png", []),
- Case("NewCenturySchlbk", Path("NewCenturySchlbk.gv"), "dot", "ps", []),
- Case("Palatino", Path("Palatino.gv"), "dot", "png", []),
- Case("Palatino", Path("Palatino.gv"), "dot", "ps", []),
- Case("Times", Path("Times.gv"), "dot", "png", []),
- Case("Times", Path("Times.gv"), "dot", "ps", []),
- Case("ZapfChancery", Path("ZapfChancery.gv"), "dot", "png", []),
- Case("ZapfChancery", Path("ZapfChancery.gv"), "dot", "ps", []),
- Case("ZapfDingbats", Path("ZapfDingbats.gv"), "dot", "png", []),
- Case("ZapfDingbats", Path("ZapfDingbats.gv"), "dot", "ps", []),
- Case("xlabels", Path("xlabels.gv"), "dot", "png", []),
- Case("xlabels", Path("xlabels.gv"), "neato", "png", []),
- Case("sides", Path("sides.gv"), "dot", "ps", []),
- ]
- def doDiff(OUTFILE, testname, fmt):
- """
- Compare old and new output and report if different.
- Args: testname index fmt
- """
- FILE1 = OUTDIR / OUTFILE
- FILE2 = REFDIR / OUTFILE
- F = fmt.split(":")[0]
- if F in ["ps", "ps2"]:
- with open(FILE1, "rt", encoding="latin-1") as src:
- dst1 = io.StringIO()
- done_setup = False
- for line in src:
- if done_setup:
- dst1.write(line)
- else:
- done_setup = re.match(r"%%End.*Setup", line) is not None
- with open(FILE2, "rt", encoding="latin-1") as src:
- dst2 = io.StringIO()
- done_setup = False
- for line in src:
- if done_setup:
- dst2.write(line)
- else:
- done_setup = re.match(r"%%End.*Setup", line) is not None
- returncode = 0 if dst1.getvalue() == dst2.getvalue() else -1
- elif F == "svg":
- with open(FILE1, "rt", encoding="utf-8") as f:
- a = re.sub(r"^<!--.*-->$", "", f.read(), flags=re.MULTILINE)
- with open(FILE2, "rt", encoding="utf-8") as f:
- b = re.sub(r"^<!--.*-->$", "", f.read(), flags=re.MULTILINE)
- returncode = 0 if a.strip() == b.strip() else -1
- elif F == "png":
- OUTHTML.mkdir(exist_ok=True)
- returncode = subprocess.call(
- ["diffimg", FILE1, FILE2, OUTHTML / f"dif_{OUTFILE}"],
- )
- if returncode != 0:
- with open(OUTHTML / "index.html", "at", encoding="utf-8") as fd:
- fd.write("<p>\n")
- shutil.copyfile(FILE2, OUTHTML / f"old_{OUTFILE}")
- fd.write(f'<img src="old_{OUTFILE}" width="192" height="192">\n')
- shutil.copyfile(FILE1, OUTHTML / f"new_{OUTFILE}")
- fd.write(f'<img src="new_{OUTFILE}" width="192" height="192">\n')
- fd.write(f'<img src="dif_{OUTFILE}" width="192" height="192">\n')
- else:
- (OUTHTML / f"dif_{OUTFILE}").unlink()
- else:
- with open(FILE2, "rt", encoding="utf-8") as a:
- with open(FILE1, "rt", encoding="utf-8") as b:
- returncode = 0 if a.read().strip() == b.read().strip() else -1
- if returncode != 0:
- # FIXME: pytest.fail when all tests pass on all platforms
- print(f"Test {testname}: == Failed == {OUTFILE}", file=sys.stderr)
- def genOutname(name, alg, fmt, flags: List[str]):
- """
- Generate output file name given 4 parameters.
- testname layout format flags
- If format ends in :*, remove this, change the colons to underscores,
- and append to basename
- """
- fmt_split = fmt.split(":")
- if len(fmt_split) >= 2:
- F = fmt_split[0]
- XFMT = f'_{"_".join(fmt_split[1:])}'
- else:
- F = fmt
- XFMT = ""
- suffix = hashlib.sha256()
- for flag in flags:
- suffix.update(f"{flag} ".encode("utf-8"))
- OUTFILE = f"{name}_{alg}{XFMT}_{suffix.hexdigest()}.{F}"
- return OUTFILE
- @pytest.mark.parametrize(
- "name,input,algorithm,format,flags",
- ((c.name, c.input, c.algorithm, c.format, c.flags) for c in TESTS),
- )
- def test_graph(name: str, input: Path, algorithm: str, format: str, flags: List[str]):
- """
- Run a single test.
- """
- if input.suffix != ".gv":
- pytest.skip(f"Unknown graph spec, test {name} - ignoring")
- INFILE = GRAPHDIR / input
- OUTFILE = genOutname(name, algorithm, format, flags)
- OUTDIR.mkdir(exist_ok=True)
- OUTPATH = OUTDIR / OUTFILE
- testcmd = ["dot", f"-K{algorithm}", f"-T{format}"] + flags + ["-o", OUTPATH, INFILE]
- # FIXME: Remove when https://gitlab.com/graphviz/graphviz/-/issues/1790 is
- # fixed
- if platform.system() == "Windows" and name == "ps_user_shapes":
- pytest.skip(
- f"Skipping test {name}: using PostScript shapefile "
- "because it fails with Windows builds (#1790)"
- )
- RVAL = subprocess.call(testcmd, stderr=subprocess.STDOUT)
- if RVAL != 0 or not OUTPATH.exists():
- pytest.fail(
- f'Test {name}: == Layout failed ==\n {" ".join(str(a) for a in testcmd)}'
- )
- elif (REFDIR / OUTFILE).exists():
- doDiff(OUTFILE, name, format)
- else:
- sys.stderr.write(
- f"Test {name}: == No file {REFDIR}/{OUTFILE} for comparison ==\n"
- )
- # Set REFDIR
- if platform.system() == "Linux":
- REFDIR = Path("linux.x86")
- elif platform.system() == "Darwin":
- REFDIR = Path("macosx")
- elif platform.system() == "Windows":
- REFDIR = Path("nshare")
- else:
- print(f'Unrecognized system "{platform.system()}"', file=sys.stderr)
- REFDIR = Path("nshare")
|