""" Graphviz regression tests The test cases in this file relate to previously observed bugs. A failure of one of these indicates that a past bug has been reintroduced. """ import dataclasses import io import json import math import os import platform import re import shlex import shutil import signal import stat import statistics import subprocess import sys import tempfile import textwrap import time import xml.etree.ElementTree as ET from pathlib import Path from typing import Iterator, List, Set import pexpect import pytest sys.path.append(os.path.dirname(__file__)) from gvtest import ( # pylint: disable=wrong-import-position ROOT, dot, gvpr, is_macos, is_mingw, is_rocky, is_rocky_8, is_static_build, remove_xtype_warnings, run_c, which, ) def is_ndebug_defined() -> bool: """ are assertions disabled in the Graphviz build under test? """ # the Windows release builds set NDEBUG if os.environ.get("configuration") == "Release": return True return False @pytest.mark.xfail( platform.system() == "Windows", reason="#56", strict=not is_ndebug_defined() ) # FIXME def test_14(): """ using ortho and twopi in combination should not cause an assertion failure https://gitlab.com/graphviz/graphviz/-/issues/14 """ # locate our associated test case in this directory input = Path(__file__).parent / "14.dot" assert input.exists(), "unexpectedly missing test case" # process it with Graphviz dot("svg", input) @pytest.mark.skipif(which("neato") is None, reason="neato not available") def test_42(): """ check for a former crash in neatogen https://gitlab.com/graphviz/graphviz/-/issues/42 """ # locate our associated test case in this directory input = Path(__file__).parent / "42.dot" assert input.exists(), "unexpectedly missing test case" # process it with Graphviz subprocess.check_call(["neato", "-n2", "-Tpng", input], stdout=subprocess.DEVNULL) def test_56(): """ parsing a particular graph should not cause a Trapezoid-table overflow assertion failure https://gitlab.com/graphviz/graphviz/-/issues/56 """ # locate our associated test case in this directory input = Path(__file__).parent / "56.dot" assert input.exists(), "unexpectedly missing test case" # process it with Graphviz dot("svg", input) def test_121(): """ test a graph that previously caused an assertion failure in `merge_chain` https://gitlab.com/graphviz/graphviz/-/issues/121 """ # locate our associated test case in this directory input = Path(__file__).parent / "121.dot" assert input.exists(), "unexpectedly missing test case" # process it with Graphviz dot("pdf", input) def test_131(): """ PIC back end should produce valid output https://gitlab.com/graphviz/graphviz/-/issues/131 """ # a basic graph src = "digraph { a -> b; c -> d; }" # ask Graphviz to process this to PIC pic = dot("pic", source=src) if which("gpic") is None: pytest.skip("GNU PIC not available") # ask GNU PIC to process the Graphviz output subprocess.run( ["gpic"], input=pic, stdout=subprocess.DEVNULL, check=True, universal_newlines=True, ) @pytest.mark.parametrize( "testcase", ( "144_no_ortho.dot", pytest.param( "144_ortho.dot", marks=pytest.mark.xfail(platform.system() == "Windows", reason="flaky"), ), ), ) # FIXME def test_144(testcase: str): """ using ortho should not result in head/tail confusion https://gitlab.com/graphviz/graphviz/-/issues/144 """ # locate our associated test cases in this directory input = Path(__file__).parent / testcase assert input.exists(), "unexpectedly missing test case" # process the non-ortho one into JSON out = dot("json", input) data = json.loads(out) # find the nodes “A”, “B” and “C” A = [x for x in data["objects"] if x["name"] == "A"][0] B = [x for x in data["objects"] if x["name"] == "B"][0] C = [x for x in data["objects"] if x["name"] == "C"][0] # find the straight A→B and the angular A→C edges straight_edge = [ x for x in data["edges"] if x["tail"] == A["_gvid"] and x["head"] == B["_gvid"] ][0] angular_edge = [ x for x in data["edges"] if x["tail"] == A["_gvid"] and x["head"] == C["_gvid"] ][0] # the A→B edge should have been routed vertically down straight_points = straight_edge["_draw_"][1]["points"] xs = [x for x, _ in straight_points] ys = [y for _, y in straight_points] assert all(x == xs[0] for x in xs), "A->B not routed vertically" assert ys == sorted(ys, reverse=True), "A->B is not routed down" # determine Graphviz’ idea of head and tail ends straight_head_point = straight_edge["_hdraw_"][3]["points"][0] straight_tail_point = straight_edge["_tdraw_"][3]["points"][0] assert straight_head_point[1] < straight_tail_point[1], "A->B head/tail confusion" # the A→C edge should have been routed in zigzag down and right angular_points = angular_edge["_draw_"][1]["points"] xs = [x for x, _ in angular_points] ys = [y for _, y in angular_points] assert xs == sorted(xs), "A->B is not routed down" assert ys == sorted(ys, reverse=True), "A->B is not routed right" # determine Graphviz’ idea of head and tail ends angular_head_point = angular_edge["_hdraw_"][3]["points"][0] angular_tail_point = angular_edge["_tdraw_"][3]["points"][0] assert angular_head_point[0] > angular_tail_point[0], "A->C head/tail confusion" def test_146(): """ dot should respect an alpha channel value of 0 when writing SVG https://gitlab.com/graphviz/graphviz/-/issues/146 """ # a graph using white text but with 0 alpha source = ( "graph {\n" ' n[style="filled", fontcolor="#FFFFFF00", label="hello world"];\n' "}" ) # ask Graphviz to process this svg = dot("svg", source=source) # the SVG should be setting opacity opacity = re.search(r'\bfill-opacity="(\d+(\.\d+)?)"', svg) assert opacity is not None, "transparency not set for alpha=0 color" # it should be zeroed assert ( float(opacity.group(1)) == 0 ), "alpha=0 color set to something non-transparent" def test_165(): """ dot should be able to produce properly escaped xdot output https://gitlab.com/graphviz/graphviz/-/issues/165 """ # locate our associated test case in this directory input = Path(__file__).parent / "165.dot" assert input.exists(), "unexpectedly missing test case" # ask Graphviz to translate it to xdot output = dot("xdot", input) # find the line containing the _ldraw_ attribute ldraw = re.search(r"^\s*_ldraw_\s*=(?P.*?)$", output, re.MULTILINE) assert ldraw is not None, "no _ldraw_ attribute in graph" # this should contain the label correctly escaped assert r"hello \\\" world" in ldraw.group("value"), "unexpected ldraw contents" def test_165_2(): """ variant of test_165() that checks a similar problem for edges https://gitlab.com/graphviz/graphviz/-/issues/165 """ # locate our associated test case in this directory input = Path(__file__).parent / "165_2.dot" assert input.exists(), "unexpectedly missing test case" # ask Graphviz to translate it to xdot output = dot("xdot", input) # find the lines containing _ldraw_ attributes ldraw = re.findall(r"^\s*_ldraw_\s*=(.*?)$", output, re.MULTILINE) assert ldraw is not None, "no _ldraw_ attributes in graph" # one of these should contain the label correctly escaped assert any(r"hello \\\" world" in l for l in ldraw), "unexpected ldraw contents" def test_165_3(): """ variant of test_165() that checks a similar problem for graph labels https://gitlab.com/graphviz/graphviz/-/issues/165 """ # locate our associated test case in this directory input = Path(__file__).parent / "165_3.dot" assert input.exists(), "unexpectedly missing test case" # ask Graphviz to translate it to xdot output = dot("xdot", input) # find the lines containing _ldraw_ attributes ldraw = re.findall(r"^\s*_ldraw_\s*=(.*?)$", output, re.MULTILINE) assert ldraw is not None, "no _ldraw_ attributes in graph" # one of these should contain the label correctly escaped assert any(r"hello \\\" world" in l for l in ldraw), "unexpected ldraw contents" def test_167(): """ using concentrate=true should not result in a segfault https://gitlab.com/graphviz/graphviz/-/issues/167 """ # locate our associated test case in this directory input = Path(__file__).parent / "167.dot" assert input.exists(), "unexpectedly missing test case" # process this with dot ret = subprocess.call(["dot", "-Tpdf", "-o", os.devnull, input]) # Graphviz should not have caused a segfault assert ret != -signal.SIGSEGV, "Graphviz segfaulted" def test_191(): """ a comma-separated list without quotes should cause a hard error, not a warning https://gitlab.com/graphviz/graphviz/-/issues/191 """ source = ( "graph {\n" ' "Trackable" [fontcolor=grey45,labelloc=c,fontname=Vera Sans, ' "DejaVu Sans, Liberation Sans, Arial, Helvetica, sans,shape=box," 'height=0.3,align=center,fontsize=10,style="setlinewidth(0.5)"];\n' "}" ) with subprocess.Popen( ["dot", "-Tdot"], stdin=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) as p: _, stderr = p.communicate(source) assert "syntax error" in stderr, "missing error message for unquoted list" assert p.returncode != 0, "syntax error was only a warning, not an error" def test_218(): """ out-of-spec font names should cause warnings in the core PS renderer https://gitlab.com/graphviz/graphviz/-/issues/218 """ # a graph using a font name with a space in it source = 'graph { a[fontname="PT Sans"]; }' # render it to PS warnings = subprocess.check_output( ["dot", "-Tps", "-o", os.devnull], stderr=subprocess.STDOUT, input=source, universal_newlines=True, ) assert warnings.strip() != "", "no warning issued for a font name containing space" @pytest.mark.parametrize("test_case", ("241_0.dot", "241_1.dot")) @pytest.mark.xfail( strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/241" ) def test_241(test_case: str): """ processing a graph with a `splines=…` setting should not causes warnings https://gitlab.com/graphviz/graphviz/-/issues/241 """ # locate our associated test case in this directory input = Path(__file__).parent / test_case assert input.exists(), "unexpectedly missing test case" proc = subprocess.run( ["dot", "-Tsvg", "-o", os.devnull, input], stderr=subprocess.PIPE, universal_newlines=True, check=True, ) assert ( "Something is probably seriously wrong" not in proc.stderr ), "splines setting caused warnings" def test_358(): """ setting xdot version to 1.7 should enable font characteristics https://gitlab.com/graphviz/graphviz/-/issues/358 """ # locate our associated test case in this directory input = Path(__file__).parent / "358.dot" assert input.exists(), "unexpectedly missing test case" # process this with dot xdot = dot("xdot", input) for i in range(6): m = re.search(f"\\bt {1 << i}\\b", xdot) assert m is not None, f"font characteristic {1 << i} not enabled in xdot 1.7" @pytest.mark.parametrize("attribute", ("samehead", "sametail")) def test_452(attribute: str): """ more than 5 unique `samehead` and `sametail` values should be usable https://gitlab.com/graphviz/graphviz/-/issues/452 """ # a graph using more than 5 of the same attribute with the same node on one # side of each edge graph = io.StringIO() graph.write("digraph {\n") for i in range(6): if attribute == "samehead": graph.write(f" m{i} -> n1") else: graph.write(f" n1 -> m{i}") graph.write(f'[{attribute}="foo{i}"];\n') graph.write("}\n") # process this with dot dot("svg", source=graph.getvalue()) def test_510(): """ HSV colors should also support an alpha channel https://gitlab.com/graphviz/graphviz/-/issues/510 """ # a graph with a turquoise, partially transparent node source = 'digraph { a [color="0.482 0.714 0.878 0.5"]; }' # process this with dot svg = dot("svg", source=source) # see if we can locate an opacity adjustment m = re.search(r'\bstroke-opacity="(?P\d*.\d*)"', svg) assert m is not None, "no stroke-opacity set; alpha channel ignored?" # it should be something in-between transparent and opaque opacity = float(m.group("opacity")) assert opacity > 0, "node set transparent; misinterpreted alpha channel?" assert opacity < 1, "node set opaque; misinterpreted alpha channel?" @pytest.mark.skipif( which("gv2gxl") is None or which("gxl2gv") is None, reason="GXL tools not available" ) def test_517(): """ round tripping a graph through gv2gxl should not lose HTML labels https://gitlab.com/graphviz/graphviz/-/issues/517 """ # our test case input input = ( "digraph{\n" " A[label=<
(A)
>]\n" ' B[label="
(B)
"]\n' "}" ) # translate it to GXL gv2gxl = which("gv2gxl") gxl = subprocess.check_output([gv2gxl], input=input, universal_newlines=True) # translate this back to Dot gxl2gv = which("gxl2gv") dot_output = subprocess.check_output([gxl2gv], input=gxl, universal_newlines=True) # the result should have both expected labels somewhere assert ( "label=<
(A)
>" in dot_output ), "HTML label missing" assert ( 'label="
(B)
"' in dot_output ), "regular label missing" def test_793(): """ Graphviz should not crash when using VRML output with a non-writable current directory https://gitlab.com/graphviz/graphviz/-/issues/793 """ # create a non-writable directory with tempfile.TemporaryDirectory() as tmp: t = Path(tmp) t.chmod(t.stat().st_mode & ~stat.S_IWRITE) # ask the VRML back end to handle a simple graph, using the above as the # current working directory with subprocess.Popen(["dot", "-Tvrml", "-o", os.devnull], cwd=t) as p: p.communicate("digraph { a -> b; }") # Graphviz should not have caused a segfault assert p.returncode != -signal.SIGSEGV, "Graphviz segfaulted" def test_797(): """ “&;” should not be considered an XML escape sequence https://gitlab.com/graphviz/graphviz/-/issues/797 """ # some input containing the invalid escape input = 'digraph tree {\n"1" [shape="box", label="& &;", URL="a"];\n}' # process this with the client-side imagemap back end output = dot("cmapx", source=input) # the escape sequences should have been preserved assert "& &" in output @pytest.mark.xfail( strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/813" ) def test_813(): """ nodes with multiple peripheries should still have a stable rendering https://gitlab.com/graphviz/graphviz/-/issues/813 """ # locate our associated test case in this directory input = Path(__file__).parent / "813.dot" assert input.exists(), "unexpectedly mising test case" # render this to dot reference = dot("dot", input) # run it through multiple passes iterated = reference for _ in range(4): iterated = dot("dot", source=iterated) assert ( reference == iterated ), "rendering of shapes with multiple peripheries is unstable" def test_827(): """ Graphviz should not crash when processing the b15.gv example https://gitlab.com/graphviz/graphviz/-/issues/827 """ b15gv = Path(__file__).parent / "graphs/b15.gv" assert b15gv.exists(), "missing test case file" dot("svg", b15gv) def test_925(): """ spaces should be handled correctly in UTF-8-containing labels in record shapes https://gitlab.com/graphviz/graphviz/-/issues/925 """ # locate our associated test case in this directory input = Path(__file__).parent / "925.dot" assert input.exists(), "unexpectedly mising test case" # process this with dot svg = dot("svg", input) # The output should include the correctly spaced UTF-8 label. Note that these # are not ASCII capital As in this string, but rather UTF-8 Cyrillic Capital # Letter As. assert "ААА ААА ААА" in svg, "incorrect spacing in UTF-8 label" @pytest.mark.parametrize("testcase", ("1213-1.dot", "1213-2.dot")) @pytest.mark.xfail( strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/1213" ) def test_1213(testcase: str): """ clustering should not trigger “trouble in init_rank” errors https://gitlab.com/graphviz/graphviz/-/issues/1213 """ # locate our associated test case in this directory input = Path(__file__).parent / testcase assert input.exists(), "unexpectedly mising test case" # process this with dot dot("png", input) def test_1221(): """ assigning a node to two clusters with newrank should not cause a crash https://gitlab.com/graphviz/graphviz/-/issues/1221 """ # locate our associated test case in this directory input = Path(__file__).parent / "1221.dot" assert input.exists(), "unexpectedly missing test case" # process this with dot dot("svg", input) @pytest.mark.skipif(which("gv2gml") is None, reason="gv2gml not available") def test_1276(): """ quotes within a label should be escaped in translation to GML https://gitlab.com/graphviz/graphviz/-/issues/1276 """ # DOT input containing a label with quotes src = 'digraph test {\n x[label=<"Label">];\n}' # process this to GML gml = subprocess.check_output(["gv2gml"], input=src, universal_newlines=True) # the unescaped label should not appear in the output assert '""Label""' not in gml, "quotes not escaped in label" # the escaped label should appear in the output assert ( '""Label""' in gml or '""Label""' in gml ), "escaped label not found in GML output" def test_1308(): """ processing a minimized graph found by Google Autofuzz should not crash https://gitlab.com/graphviz/graphviz/-/issues/1308 """ # locate our associated test case in this directory input = Path(__file__).parent / "1308.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz dot("svg", input) def test_1308_1(): """ processing a malformed graph found by Google Autofuzz should not crash https://gitlab.com/graphviz/graphviz/-/issues/1308 """ # locate our associated test case in this directory input = Path(__file__).parent / "1308_1.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz ret = subprocess.call(["dot", "-Tsvg", "-o", os.devnull, input]) assert ret in (0, 1), "Graphviz crashed when processing malformed input" assert ret == 1, "Graphviz did not reject malformed input" def test_1314(): """ test that a large font size that produces an overflow in Pango is rejected https://gitlab.com/graphviz/graphviz/-/issues/1314 """ # locate our associated test case in this directory input = Path(__file__).parent / "1314.dot" assert input.exists(), "unexpectedly missing test case" # ask Graphviz to process it, which should fail try: dot("svg", input) except subprocess.CalledProcessError: return # the execution did not fail as expected pytest.fail("dot incorrectly exited with success") def test_1318(): """ processing a large number in a comment should not trigger integer overflow https://gitlab.com/graphviz/graphviz/-/issues/1318 """ # sample input consisting of a large number in a comment source = "#8828066547613302784" # processing this should succeed dot("svg", source=source) @pytest.mark.xfail( strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/1328" ) def test_1328(): """ a node with conflicting rank constraints should not cause a crash https://gitlab.com/graphviz/graphviz/-/issues/1328 """ # locate our associated test case in this directory input = Path(__file__).parent / "1328.dot" assert input.exists(), "unexpectedly missing test case" proc = subprocess.run( ["dot", "-Tsvg", "-o", os.devnull, input], stderr=subprocess.PIPE, universal_newlines=True, ) assert proc.returncode in (0, 1), "multiple rank constraints caused a crash" assert ( "trouble in init_rank" not in proc.stderr ), "multiple rank constraints caused ranking failure" def test_1332(): """ Triangulation calculation on the associated example should succeed. A prior change that was intended to increase accuracy resulted in the example in this test now failing some triangulation calculations. It is not clear whether the outcome before or after is correct, but this test ensures that the older behavior users are accustomed to is preserved. """ # locate our associated test case in this directory input = Path(__file__).parent / "1332.dot" assert input.exists(), "unexpectedly missing test case" # process it with Graphviz warnings = subprocess.check_output( ["dot", "-Tpdf", "-o", os.devnull, input], stderr=subprocess.STDOUT, universal_newlines=True, ) # work around macOS warnings warnings = remove_xtype_warnings(warnings).strip() # no warnings should have been printed assert ( warnings == "" ), "warnings were printed when processing graph involving triangulation" def test_1408(): """ parsing particular ortho layouts should not cause an assertion failure https://gitlab.com/graphviz/graphviz/-/issues/1408 """ # locate our associated test case in this directory input = Path(__file__).parent / "1408.dot" assert input.exists(), "unexpectedly missing test case" # process it with Graphviz dot("svg", input) def test_1411(): """ parsing strings containing newlines should not disrupt line number tracking https://gitlab.com/graphviz/graphviz/-/issues/1411 """ # locate our associated test case in this directory input = Path(__file__).parent / "1411.dot" assert input.exists(), "unexpectedly missing test case" # process it with Graphviz (should fail) with subprocess.Popen( ["dot", "-Tsvg", "-o", os.devnull, input], stderr=subprocess.PIPE, universal_newlines=True, ) as p: _, output = p.communicate() assert p.returncode != 0, "Graphviz accepted broken input" assert ( "syntax error in line 17 near '\\'" in output ), "error message did not identify correct location" @pytest.mark.xfail( strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/1435" ) def test_1435(): """ triangulation paths should be findable on this graph https://gitlab.com/graphviz/graphviz/-/issues/1435 """ # locate our associated test case in this directory input = Path(__file__).parent / "1435.dot" assert input.exists(), "unexpectedly missing test case" # process it with Graphviz err = subprocess.check_output( ["dot", "-Tpng", "-o", os.devnull, input], stderr=subprocess.STDOUT, universal_newlines=True, ) assert err.strip() == "", "errors were printed" def test_1436(): """ test a segfault from https://gitlab.com/graphviz/graphviz/-/issues/1436 has not reappeared """ # locate our associated test case in this directory input = Path(__file__).parent / "1436.dot" assert input.exists(), "unexpectedly missing test case" # ask Graphviz to process it, which should generate a segfault if this bug # has been reintroduced dot("svg", input) def test_1444(): """ specifying 'headport' as an edge attribute should work regardless of what order attributes appear in https://gitlab.com/graphviz/graphviz/-/issues/1444 """ # locate the first of our associated tests input1 = Path(__file__).parent / "1444.dot" assert input1.exists(), "unexpectedly missing test case" # ask Graphviz to process it with subprocess.Popen( ["dot", "-Tsvg", input1], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) as p: stdout1, stderr = p.communicate() assert p.returncode == 0, "failed to process a headport edge" stderr = remove_xtype_warnings(stderr).strip() assert stderr == "", "emitted an error for a legal graph" # now locate our second variant, that simply has the attributes swapped input2 = Path(__file__).parent / "1444-2.dot" assert input2.exists(), "unexpectedly missing test case" # process it identically with subprocess.Popen( ["dot", "-Tsvg", input2], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) as p: stdout2, stderr = p.communicate() assert p.returncode == 0, "failed to process a headport edge" stderr = remove_xtype_warnings(stderr).strip() assert stderr == "", "emitted an error for a legal graph" assert stdout1 == stdout2, "swapping edge attributes altered the output graph" def test_1449(): """ using the SVG color scheme should not cause warnings https://gitlab.com/graphviz/graphviz/-/issues/1449 """ # start Graphviz with subprocess.Popen( ["dot", "-Tsvg", "-o", os.devnull], stdin=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) as p: # pass it some input that uses the SVG color scheme _, stderr = p.communicate('graph g { colorscheme="svg"; }') assert p.returncode == 0, "Graphviz exited with non-zero status" assert stderr.strip() == "", "SVG color scheme use caused warnings" @pytest.mark.xfail( strict=platform.system() != "Linux", reason="https://gitlab.com/graphviz/graphviz/-/issues/1453", ) def test_1453(): """ `splines=curved` should not result in segfaults https://gitlab.com/graphviz/graphviz/-/issues/1453 """ # locate our associated test case in this directory input = Path(__file__).parent / "1453.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz dot("svg", input) @pytest.mark.xfail( strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/1472" ) def test_1472(): """ processing a malformed graph found by Google Autofuzz should not crash https://gitlab.com/graphviz/graphviz/-/issues/1472 """ # locate our associated test case in this directory input = Path(__file__).parent / "1472.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz dot("svg", input) def test_1554(): """ small distances between nodes should not cause a crash in majorization https://gitlab.com/graphviz/graphviz/-/issues/1554 """ # locate our associated test case in this directory input = Path(__file__).parent / "1554.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz output = dot("svg", input) # the output should not have NaN values, indicating out of bounds computation assert ( re.search(r"\bnan\b", output, flags=re.IGNORECASE) is None ), "computation exceeded bounds" def test_1585(): """ clustering nodes should not reverse their horizontal layout https://gitlab.com/graphviz/graphviz/-/issues/1585 """ # locate our associated test cases in this directory no_cluster = Path(__file__).parent / "1585_0.dot" assert no_cluster.exists(), "unexpectedly missing test case" cluster = Path(__file__).parent / "1585_1.dot" assert cluster.exists(), "unexpectedly missing test case" def find_node_xs(svg_output: str) -> Iterator[float]: """ yield 3 floats representing the X positions of nodes b, c, d in the given graph """ # parse the SVG root = ET.fromstring(svg_output) # find `b` b = root.findall( ".//{http://www.w3.org/2000/svg}title[.='b']../{http://www.w3.org/2000/svg}ellipse" ) assert len(b) == 1, "could not find node 'b'" yield float(b[0].attrib["cx"]) # find `c` c = root.findall( ".//{http://www.w3.org/2000/svg}title[.='c']../{http://www.w3.org/2000/svg}ellipse" ) assert len(c) == 1, "could not find node 'c'" yield float(c[0].attrib["cx"]) # find `d` d = root.findall( ".//{http://www.w3.org/2000/svg}title[.='d']../{http://www.w3.org/2000/svg}ellipse" ) assert len(d) == 1, "could not find node 'd'" yield float(d[0].attrib["cx"]) # render the one without clusters and get its nodes’ X positions no_cluster_out = dot("svg", no_cluster) b, c, d = list(find_node_xs(no_cluster_out)) # confirm we got a left → right ordering assert b < c, "unexpected horizontal node ordering" assert c < d, "unexpected horizontal node ordering" # now try the same thing with the clustered graph cluster_out = dot("svg", cluster) b, c, d = list(find_node_xs(cluster_out)) assert b < c, "clustering altered nodes’ horizontal ordering" assert c < d, "clustering altered nodes’ horizontal ordering" @pytest.mark.skipif(which("gvpr") is None, reason="GVPR not available") def test_1594(): """ GVPR should give accurate line numbers in error messages https://gitlab.com/graphviz/graphviz/-/issues/1594 """ # locate our associated test case in this directory input = Path(__file__).parent / "1594.gvpr" # run GVPR with our (malformed) input program with subprocess.Popen( ["gvpr", "-f", input], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, universal_newlines=True, ) as p: _, stderr = p.communicate() assert p.returncode != 0, "GVPR did not reject malformed program" assert "line 3:" in stderr, "GVPR did not identify correct line of syntax error" @pytest.mark.parametrize("long,short", (("--help", "-?"), ("--version", "-V"))) def test_1618(long: str, short: str): """ Graphviz should understand `--help` and `--version` https://gitlab.com/graphviz/graphviz/-/issues/1618 """ # run Graphviz with the short form of the argument p1 = subprocess.run( ["dot", short], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True ) # run it with the long form of the argument p2 = subprocess.run( ["dot", long], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True ) # output from both should match assert ( p1.stdout == p2.stdout ), f"`dot {long}` wrote differing output than `dot {short}`" assert ( p1.stderr == p2.stderr ), f"`dot {long}` wrote differing output than `dot {short}`" @pytest.mark.parametrize( "test_case", ("1622_0.dot", "1622_1.dot", "1622_2.dot", "1622_3.dot") ) def test_1622(test_case: str): """ Narrow HTML table cells should not cause assertion failures https://gitlab.com/graphviz/graphviz/-/issues/1622 """ # locate our associated test case in this directory input = Path(__file__).parent / test_case assert input.exists(), "unexpectedly missing test case" # process it with Graphviz dot("png:cairo:cairo", input) def test_1624(): """ record shapes should be usable https://gitlab.com/graphviz/graphviz/-/issues/1624 """ # locate our associated test case in this directory input = Path(__file__).parent / "1624.dot" assert input.exists(), "unexpectedly missing test case" # process it with Graphviz dot("svg", input) def test_1644(): """ neato results should be deterministic https://gitlab.com/graphviz/graphviz/-/issues/1644 """ # get our baseline reference input = Path(__file__).parent / "1644.dot" assert input.exists(), "unexpectedly missing test case" ref = subprocess.check_output(["neato", input], universal_newlines=True) # now repeat this, expecting it not to change for _ in range(20): out = subprocess.check_output(["neato", input], universal_newlines=True) assert ref == out, "repeated rendering changed output" def test_1658(): """ the graph associated with this test case should not crash Graphviz https://gitlab.com/graphviz/graphviz/-/issues/1658 """ # locate our associated test case in this directory input = Path(__file__).parent / "1658.dot" assert input.exists(), "unexpectedly missing test case" # process it with Graphviz dot("png", input) def test_1676(): """ https://gitlab.com/graphviz/graphviz/-/issues/1676 """ # locate our associated test case in this directory input = Path(__file__).parent / "1676.dot" assert input.exists(), "unexpectedly missing test case" # run Graphviz with this input ret = subprocess.call(["dot", "-Tsvg", "-o", os.devnull, input]) # this malformed input should not have caused Graphviz to crash assert ret != -signal.SIGSEGV, "Graphviz segfaulted" def test_1724(): """ passing malformed node and newrank should not cause segfaults https://gitlab.com/graphviz/graphviz/-/issues/1724 """ # locate our associated test case in this directory input = Path(__file__).parent / "1724.dot" assert input.exists(), "unexpectedly missing test case" # run Graphviz with this input ret = subprocess.call(["dot", "-Tsvg", "-o", os.devnull, input]) assert ret != -signal.SIGSEGV, "Graphviz segfaulted" @pytest.mark.skipif( is_static_build(), reason="dynamic libraries are unavailable to link against in static builds", ) def test_1767(): """ using the Pango plugin multiple times should produce consistent results https://gitlab.com/graphviz/graphviz/-/issues/1767 """ # FIXME: Remove skip when # https://gitlab.com/graphviz/graphviz/-/issues/1777 is fixed if os.getenv("build_system") == "msbuild": pytest.skip("Windows MSBuild release does not contain any header files (#1777)") # find co-located test source c_src = (Path(__file__).parent / "1767.c").resolve() assert c_src.exists(), "missing test case" # find our co-located dot input src = (Path(__file__).parent / "1767.dot").resolve() assert src.exists(), "missing test case" stdout, _ = run_c(c_src, [src], link=["cgraph", "gvc"]) assert stdout.splitlines() == [ "Loaded graph:clusters", "cluster_0 contains 5 nodes", "cluster_1 contains 1 nodes", "cluster_2 contains 3 nodes", "cluster_3 contains 3 nodes", "Loaded graph:clusters", "cluster_0 contains 5 nodes", "cluster_1 contains 1 nodes", "cluster_2 contains 3 nodes", "cluster_3 contains 3 nodes", ] @pytest.mark.skipif(which("gvpr") is None, reason="GVPR not available") @pytest.mark.skipif(platform.system() != "Windows", reason="only relevant on Windows") def test_1780(): """ GVPR should accept programs at absolute paths https://gitlab.com/graphviz/graphviz/-/issues/1780 """ # get absolute path to an arbitrary GVPR program clustg = Path(__file__).resolve().parent.parent / "cmd/gvpr/lib/clustg" # GVPR should not fail when given this path gvpr(clustg) def test_1783(): """ Graphviz should not segfault when passed large edge weights https://gitlab.com/graphviz/graphviz/-/issues/1783 """ # locate our associated test case in this directory input = Path(__file__).parent / "1783.dot" assert input.exists(), "unexpectedly missing test case" # run Graphviz with this input ret = subprocess.call(["dot", "-Tsvg", "-o", os.devnull, input]) assert ret != 0, "Graphviz accepted illegal edge weight" assert ret != -signal.SIGSEGV, "Graphviz segfaulted" @pytest.mark.skipif(which("gvedit") is None, reason="Gvedit not available") def test_1813(): """ gvedit -? should show usage https://gitlab.com/graphviz/graphviz/-/issues/1813 """ environ_copy = os.environ.copy() environ_copy.pop("DISPLAY", None) output = subprocess.check_output( ["gvedit", "-?"], env=environ_copy, universal_newlines=True ) assert "Usage" in output, "gvedit -? did not show usage" def test_1845(): """ rendering sequential graphs to PS should not segfault https://gitlab.com/graphviz/graphviz/-/issues/1845 """ # locate our associated test case in this directory input = Path(__file__).parent / "1845.dot" assert input.exists(), "unexpectedly missing test case" # generate a multipage PS file from this input dot("ps", input) @pytest.mark.xfail(strict=True) # FIXME def test_1856(): """ headports and tailports should be respected https://gitlab.com/graphviz/graphviz/-/issues/1856 """ # locate our associated test case in this directory input = Path(__file__).parent / "1856.dot" assert input.exists(), "unexpectedly missing test case" # process it into JSON out = dot("json", input) data = json.loads(out) # find the two nodes, “3” and “5” three = [x for x in data["objects"] if x["name"] == "3"][0] five = [x for x in data["objects"] if x["name"] == "5"][0] # find the edge from “3” to “5” edge = [ x for x in data["edges"] if x["tail"] == three["_gvid"] and x["head"] == five["_gvid"] ][0] # The edge should look something like: # # ┌─┐ # │3│ # └┬┘ # ┌────┘ # ┌┴┐ # │5│ # └─┘ # # but a bug causes port constraints to not be respected and the edge comes out # more like: # # ┌─┐ # │3│ # └┬┘ # │ # ┌─┐ │ # ├5̶┼───┘ # └─┘ # # So validate that the edge’s path does not dip below the top of the “5” node. top_of_five = max(y for _, y in five["_draw_"][1]["points"]) waypoints_y = [y for _, y in edge["_draw_"][1]["points"]] assert all(y >= top_of_five for y in waypoints_y), "edge dips below 5" @pytest.mark.skipif(which("fdp") is None, reason="fdp not available") def test_1865(): """ fdp should not read out of bounds when processing node names https://gitlab.com/graphviz/graphviz/-/issues/1865 Note, the crash this test tries to provoke may only occur when run under Address Sanitizer or Valgrind """ # locate our associated test case in this directory input = Path(__file__).parent / "1865.dot" assert input.exists(), "unexpectedly missing test case" # fdp should not crash when processing this file subprocess.check_call(["fdp", "-o", os.devnull, input]) @pytest.mark.skipif(which("gv2gml") is None, reason="gv2gml not available") @pytest.mark.skipif(which("gml2gv") is None, reason="gml2gv not available") @pytest.mark.parametrize( "penwidth", (pytest.param("1.0", id="penwidth=1.0"), pytest.param("1", id="pendwidth=1")), ) def test_1871(penwidth: str): """ round tripping something with either an integer or real `penwidth` through gv2gml→gml2gv should return the correct `penwidth` """ # a trivial graph input = f"graph {{ a [penwidth={penwidth}] }}" # pass it through gv2gml gv = subprocess.check_output(["gv2gml"], input=input, universal_newlines=True) # pass this through gml2gv gml = subprocess.check_output(["gml2gv"], input=gv, universal_newlines=True) # the result should have a `penwidth` of 1 has_1 = re.search(r"\bpenwidth\s*=\s*1[^\.]", gml) is not None has_1_0 = re.search(r"\bpenwidth\s*=\s*1\.0\b", gml) is not None assert ( has_1 or has_1_0 ), f"incorrect penwidth from round tripping through GML (output {gml})" @pytest.mark.skipif(which("fdp") is None, reason="fdp not available") def test_1876(): """ fdp should not rename nodes with internal names https://gitlab.com/graphviz/graphviz/-/issues/1876 """ # a trivial graph to provoke this issue input = "graph { a }" # process this with fdp try: output = subprocess.check_output(["fdp"], input=input, universal_newlines=True) except subprocess.CalledProcessError as e: raise RuntimeError("fdp failed to process trivial graph") from e # we should not see any internal names like "%3" assert "%" not in output, "internal name in fdp output" @pytest.mark.skipif(which("fdp") is None, reason="fdp not available") def test_1877(): """ fdp should not fail an assertion when processing cluster edges https://gitlab.com/graphviz/graphviz/-/issues/1877 """ # simple input with a cluster edge input = "graph {subgraph cluster_a {}; cluster_a -- b}" # fdp should be able to process this subprocess.run( ["fdp", "-o", os.devnull], input=input, check=True, universal_newlines=True ) def test_1880(): """ parsing a particular graph should not cause a Trapezoid-table overflow assertion failure https://gitlab.com/graphviz/graphviz/-/issues/1880 """ # locate our associated test case in this directory input = Path(__file__).parent / "1880.dot" assert input.exists(), "unexpectedly missing test case" # process it with Graphviz dot("png", input) @pytest.mark.skipif( os.getenv("build_system") == "msbuild", reason="Windows MSBuild release does not contain any header files (#1777)", ) @pytest.mark.xfail( strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/1887" ) def test_1887(): """ empty strings as labels should be propagated to dot output https://gitlab.com/graphviz/graphviz/-/issues/1887 """ # find co-located test source c_src = (Path(__file__).parent / "1887.c").resolve() assert c_src.exists(), "missing test case" # generate a graph and pass it through dot stdout, _ = run_c(c_src, link=["cgraph"]) assert ( re.search(r'label\s*=\s*""', stdout) is not None ), "empty label missing in output" def test_1898(): """ test a segfault from https://gitlab.com/graphviz/graphviz/-/issues/1898 has not reappeared """ # locate our associated test case in this directory input = Path(__file__).parent / "1898.dot" assert input.exists(), "unexpectedly missing test case" # ask Graphviz to process it, which should generate a segfault if this bug # has been reintroduced dot("svg", input) def test_1902(): """ test a segfault from https://gitlab.com/graphviz/graphviz/-/issues/1902 has not reappeared """ # locate our associated test case in this directory input = Path(__file__).parent / "1902.dot" assert input.exists(), "unexpectedly missing test case" # ask Graphviz to process it, which should generate a segfault if this bug # has been reintroduced dot("svg", input) # root directory of this checkout ROOT = Path(__file__).parent.parent.resolve() def test_1855(): """ SVGs should have a scale with sufficient precision https://gitlab.com/graphviz/graphviz/-/issues/1855 """ # locate our associated test case in this directory src = Path(__file__).parent / "1855.dot" assert src.exists(), "unexpectedly missing test case" # run it through Graphviz svg = dot("svg", src) # find the graph element root = ET.fromstring(svg) graph = root[0] assert graph.get("class") == "graph", "could not find graph element" # extract its `transform` attribute transform = graph.get("transform") # this should begin with a scale directive m = re.match(r"scale\((?P\d+(\.\d*)?) (?P\d+(\.\d*))\)", transform) assert m is not None, f"failed to find 'scale' in '{transform}'" x = m.group("x") y = m.group("y") # the scale should be somewhere in reasonable range of what is expected assert float(x) >= 0.32 and float(x) <= 0.34, "inaccurate x scale" assert float(y) >= 0.32 and float(y) <= 0.34, "inaccurate y scale" # two digits of precision are insufficient for this example, so require a # greater number of digits in both scale components assert len(x) > 4, "insufficient precision in x scale" assert len(y) > 4, "insufficient precision in y scale" @pytest.mark.parametrize("variant", [1, 2]) @pytest.mark.skipif(which("gml2gv") is None, reason="gml2gv not available") def test_1869(variant: int): """ gml2gv should be able to parse the style, outlineStyle, width and outlineWidth GML attributes and map them to the DOT attributes style and penwidth respectively https://gitlab.com/graphviz/graphviz/-/issues/1869 """ # locate our associated test case in this directory input = Path(__file__).parent / f"1869-{variant}.gml" assert input.exists(), "unexpectedly missing test case" # ask gml2gv to translate it to DOT output = subprocess.check_output(["gml2gv", input], universal_newlines=True) assert "style=dashed" in output, "style=dashed not found in DOT output" assert "penwidth=2" in output, "penwidth=2 not found in DOT output" def test_1879(): """https://gitlab.com/graphviz/graphviz/-/issues/1879""" # locate our associated test case in this directory input = Path(__file__).parent / "1879.dot" assert input.exists(), "unexpectedly missing test case" # process it with DOT stdout = subprocess.check_output( ["dot", "-Tsvg", "-o", os.devnull, input], cwd=Path(__file__).parent, stderr=subprocess.STDOUT, universal_newlines=True, ) # check we did not trigger an assertion failure assert re.search(r"\bAssertion\b.*\bfailed\b", stdout) is None def test_1879_2(): """ another variant of lhead/ltail + compound https://gitlab.com/graphviz/graphviz/-/issues/1879 """ # locate our associated test case in this directory input = Path(__file__).parent / "1879-2.dot" assert input.exists(), "unexpectedly missing test case" # process it with DOT subprocess.check_call(["dot", "-Gmargin=0", "-Tpng", "-o", os.devnull, input]) def test_1893(): """ an HTML label containing just a ] should work https://gitlab.com/graphviz/graphviz/-/issues/1893 """ # a graph containing a node with an HTML label with a ] in a table cell input = "digraph { 0 [label=<
]
>] }" # ask Graphviz to process this dot("svg", source=input) # we should be able to do the same with an escaped ] input = "digraph { 0 [label=<
]
>] }" dot("svg", source=input) def test_1906(): """ graphs that generate large rectangles should be accepted https://gitlab.com/graphviz/graphviz/-/issues/1906 """ # one of the rtest graphs is sufficient to provoke this input = Path(__file__).parent / "graphs/root.gv" assert input.exists(), "unexpectedly missing test case" # use Circo to translate it to DOT subprocess.check_call(["dot", "-Kcirco", "-Tgv", "-o", os.devnull, input]) @pytest.mark.skipif(which("twopi") is None, reason="twopi not available") def test_1907(): """ SVG edges should have title elements that match their names https://gitlab.com/graphviz/graphviz/-/issues/1907 """ # a trivial graph to provoke this issue input = "digraph { A -> B -> C }" # generate an SVG from this input with twopi output = subprocess.check_output( ["twopi", "-Tsvg"], input=input, universal_newlines=True ) assert "A->B" in output, "element title not found in SVG" @pytest.mark.skipif(which("gvpr") is None, reason="gvpr not available") def test_1909(): """ GVPR should not output internal names https://gitlab.com/graphviz/graphviz/-/issues/1909 """ # locate our associated test case in this directory prog = Path(__file__).parent / "1909.gvpr" graph = Path(__file__).parent / "1909.dot" # run GVPR with the given input output = subprocess.check_output( ["gvpr", "-c", "-f", prog, graph], universal_newlines=True ) # we should have produced this graph without names like "%2" in it assert output == "// begin\n" "digraph bug {\n" " a -> b;\n" " b -> c;\n" "}\n" @pytest.mark.skipif( is_static_build(), reason="dynamic libraries are unavailable to link against in static builds", ) def test_1910(): """ Repeatedly using agmemread() should have consistent results https://gitlab.com/graphviz/graphviz/-/issues/1910 """ # FIXME: Remove skip when # https://gitlab.com/graphviz/graphviz/-/issues/1777 is fixed if os.getenv("build_system") == "msbuild": pytest.skip("Windows MSBuild release does not contain any header files (#1777)") # find co-located test source c_src = (Path(__file__).parent / "1910.c").resolve() assert c_src.exists(), "missing test case" # run the test _, _ = run_c(c_src, link=["cgraph", "gvc"]) def test_1913(): """ ALIGN attributes in
tags should be parsed correctly https://gitlab.com/graphviz/graphviz/-/issues/1913 """ # a template of a trivial graph using an ALIGN attribute graph = ( "digraph {{\n" ' table1[label=<
hello world' '
>];\n' "}}" ) def run(input): """ run Dot with the given input and return its exit status and stderr """ with subprocess.Popen( ["dot", "-Tsvg", "-o", os.devnull], stdin=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) as p: _, stderr = p.communicate(input) return p.returncode, remove_xtype_warnings(stderr) # Graphviz should accept all legal values for this attribute for align in ("left", "right", "center"): input = align ret, stderr = run(graph.format(input)) assert ret == 0 assert stderr.strip() == "" # these attributes should also be valid when title cased input = f"{align[0].upper()}{align[1:]}" ret, stderr = run(graph.format(input)) assert ret == 0 assert stderr.strip() == "" # similarly, they should be valid when upper cased input = align.upper() ret, stderr = run(graph.format(input)) assert ret == 0 assert stderr.strip() == "" # various invalid things that have the same prefix or suffix as a valid # alignment should be rejected for align in ("lamp", "deft", "round", "might", "circle", "venter"): input = align _, stderr = run(graph.format(input)) assert f"Warning: Illegal value {input} for ALIGN - ignored" in stderr # these attributes should also fail when title cased input = f"{align[0].upper()}{align[1:]}" _, stderr = run(graph.format(input)) assert f"Warning: Illegal value {input} for ALIGN - ignored" in stderr # similarly, they should fail when upper cased input = align.upper() _, stderr = run(graph.format(input)) assert f"Warning: Illegal value {input} for ALIGN - ignored" in stderr def test_1931(): """ New lines within strings should not be discarded during parsing """ # a graph with \n inside of strings graph = ( "graph {\n" ' node1 [label="line 1\n' "line 2\n" '"];\n' ' node2 [label="line 3\n' 'line 4"];\n' " node1 -- node2\n" ' node2 -- "line 5\n' 'line 6"\n' "}" ) # ask Graphviz to process this to dot output xdot = dot("xdot", source=graph) # all new lines in strings should have been preserved assert "line 1\nline 2\n" in xdot assert "line 3\nline 4" in xdot assert "line 5\nline 6" in xdot @pytest.mark.xfail( strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/1939" ) def test_1939(): """ clustering should not cause “trouble in init_rank” errors https://gitlab.com/graphviz/graphviz/-/issues/1939 """ # locate our associated test case in this directory input = Path(__file__).parent / "1939.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz dot("svg", input) @pytest.mark.xfail() # FIXME def test_1949(): """ rankdir=LR + compound=true should not lead to an assertion failure https://gitlab.com/graphviz/graphviz/-/issues/1949 """ # locate our associated test case in this directory input = Path(__file__).parent / "1949.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz dot("png", input) @pytest.mark.skipif(which("edgepaint") is None, reason="edgepaint not available") def test_1971(): """ edgepaint should reject invalid command line options https://gitlab.com/graphviz/graphviz/-/issues/1971 """ # a basic graph that edgepaint can process input = ( "digraph {\n" ' graph [bb="0,0,54,108"];\n' ' node [label="\\N"];\n' " a [height=0.5,\n" ' pos="27,90",\n' " width=0.75];\n" " b [height=0.5,\n" ' pos="27,18",\n' " width=0.75];\n" ' a -> b [pos="e,27,36.104 27,71.697 27,63.983 27,54.712 27,46.112"];\n' "}" ) # run edgepaint with an invalid option, `-rabbit`, that happens to have the # same first character as valid options args = ["edgepaint", "-rabbit"] with subprocess.Popen(args, stdin=subprocess.PIPE, universal_newlines=True) as p: p.communicate(input) assert p.returncode != 0, "edgepaint incorrectly accepted '-rabbit'" def test_1990(): """ using ortho and circo in combination should not cause an assertion failure https://gitlab.com/graphviz/graphviz/-/issues/14 """ # locate our associated test case in this directory input = Path(__file__).parent / "1990.dot" assert input.exists(), "unexpectedly missing test case" # process it with Graphviz subprocess.check_call(["circo", "-Tsvg", "-o", os.devnull, input]) @pytest.mark.skipif( is_static_build(), reason="dynamic libraries are unavailable to link against in static builds", ) def test_2057(): """ gvToolTred should be usable by user code https://gitlab.com/graphviz/graphviz/-/issues/2057 """ # FIXME: Remove skip when # https://gitlab.com/graphviz/graphviz/-/issues/1777 is fixed if os.getenv("build_system") == "msbuild": pytest.skip("Windows MSBuild release does not contain any header files (#1777)") # find co-located test source c_src = (Path(__file__).parent / "2057.c").resolve() assert c_src.exists(), "missing test case" # run the test _, _ = run_c(c_src, link=["gvc"]) def test_2078(): """ Incorrectly using the "layout" attribute on a subgraph should result in a sensible error. https://gitlab.com/graphviz/graphviz/-/issues/2078 """ # our sample graph that incorrectly uses layout input = "graph {\n subgraph {\n layout=osage\n }\n}" # run it through Graphviz with subprocess.Popen( ["dot", "-Tcanon", "-o", os.devnull], stdin=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) as p: _, stderr = p.communicate(input) assert p.returncode != 0, "layout on subgraph was incorrectly accepted" assert ( "layout attribute is invalid except on the root graph" in stderr ), "expected warning not found" # a graph that correctly uses layout input = "graph {\n layout=osage\n subgraph {\n }\n}" # ensure this one does not trigger warnings with subprocess.Popen( ["dot", "-Tcanon", "-o", os.devnull], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) as p: stdout, stderr = p.communicate(input) assert p.returncode == 0, "correct layout use was rejected" assert stdout.strip() == "", "unexpected output" assert ( "layout attribute is invalid except on the root graph" not in stderr ), "incorrect warning output" def test_2082(): """ Check a bug in inside_polygon has not been reintroduced. https://gitlab.com/graphviz/graphviz/-/issues/2082 """ # locate our associated test case in this directory input = Path(__file__).parent / "2082.dot" assert input.exists(), "unexpectedly missing test case" # ask Graphviz to process it, which should generate an assertion failure if # this bug has been reintroduced dot("png", input) def test_2087(): """ spline routing should be aware of and ignore concentrated edges https://gitlab.com/graphviz/graphviz/-/issues/2087 """ # locate our associated test case in this directory input = Path(__file__).parent / "2087.dot" assert input.exists(), "unexpectedly missing test case" # process it with Graphviz warnings = subprocess.check_output( ["dot", "-Tpng", "-o", os.devnull, input], stderr=subprocess.STDOUT, universal_newlines=True, ) # work around macOS warnings warnings = remove_xtype_warnings(warnings).strip() # no warnings should have been printed assert ( warnings == "" ), "warnings were printed when processing concentrated duplicate edges" @pytest.mark.xfail(strict=True) @pytest.mark.parametrize("html_like_first", (False, True)) def test_2089(html_like_first: bool): # FIXME """ HTML-like and non-HTML-like strings should peacefully coexist https://gitlab.com/graphviz/graphviz/-/issues/2089 """ # a graph using an HTML-like string and a non-HTML-like string if html_like_first: graph = 'graph {\n a[label=];\n b[label="foo"];\n}' else: graph = 'graph {\n a[label="foo"];\n b[label=];\n}' # normalize the graph canonical = dot("dot", source=graph) assert "label=foo" in canonical, "non-HTML-like label not found" assert "label=" in canonical, "HTML-like label not found" @pytest.mark.xfail(strict=True) # FIXME def test_2089_2(): """ HTML-like and non-HTML-like strings should peacefully coexist https://gitlab.com/graphviz/graphviz/-/issues/2089 """ # find co-located test source c_src = (Path(__file__).parent / "2089.c").resolve() assert c_src.exists(), "missing test case" # run it _, _ = run_c(c_src, link=["cgraph"]) @pytest.mark.skipif(which("dot2gxl") is None, reason="dot2gxl not available") def test_2092(): """ an empty node ID should not cause a dot2gxl NULL pointer dereference https://gitlab.com/graphviz/graphviz/-/issues/2092 """ p = subprocess.run(["dot2gxl", "-d"], input='', universal_newlines=True) assert p.returncode != 0, "dot2gxl accepted invalid input" assert p.returncode == 1, "dot2gxl crashed" @pytest.mark.skipif(which("dot2gxl") is None, reason="dot2gxl not available") def test_2093(): """ dot2gxl should handle elements with no ID https://gitlab.com/graphviz/graphviz/-/issues/2093 """ with subprocess.Popen( ["dot2gxl", "-d"], stdin=subprocess.PIPE, universal_newlines=True ) as p: p.communicate('') assert p.returncode == 1, "dot2gxl did not reject missing ID" @pytest.mark.skipif(which("dot2gxl") is None, reason="dot2gxl not available") def test_2094(): """ dot2gxl should not crash when decoding a closing node tag after a closing graph tag https://gitlab.com/graphviz/graphviz/-/issues/2094 """ # locate our associated test case in this directory input = Path(__file__).parent / "2094.xml" assert input.exists(), "unexpectedly missing test case" dot2gxl = which("dot2gxl") ret = subprocess.call([dot2gxl, "-d", input]) assert ret in ( 0, 1, ), "dot2gxl crashed when processing a closing node tag after a closing graph tag" assert ret == 1, "dot2gxl did not reject malformed XML" @pytest.mark.skipif( os.environ.get("build_system") == "msbuild" and os.environ.get("configuration") == "Debug", reason="Graphviz built with MSBuild in Debug mode has an " "insufficient stack size for this test", ) def test_2095(): """ Exceeding 1000 boxes during computation should not cause a crash https://gitlab.com/graphviz/graphviz/-/issues/2095 """ # locate our associated test case in this directory input = Path(__file__).parent / "2095.dot" assert input.exists(), "unexpectedly missing test case" # ask Graphviz to process it dot("pdf", input) @pytest.mark.skipif(which("gv2gml") is None, reason="gv2gml not available") def test_2131(): """ gv2gml should be able to process basic Graphviz input https://gitlab.com/graphviz/graphviz/-/issues/2131 """ # a trivial graph input = "digraph { a -> b; }" # ask gv2gml what it thinks of this try: subprocess.run(["gv2gml"], input=input, check=True, universal_newlines=True) except subprocess.CalledProcessError as e: raise RuntimeError("gv2gml rejected a basic graph") from e @pytest.mark.skipif(which("gvpr") is None, reason="gvpr not available") @pytest.mark.parametrize("examine", ("indices", "tokens")) def test_2138(examine: str): """ gvpr splitting and tokenizing should not result in trailing garbage https://gitlab.com/graphviz/graphviz/-/issues/2138 """ # find our co-located GVPR program script = (Path(__file__).parent / "2138.gvpr").resolve() assert script.exists(), "missing test case" # run it with NUL input out = subprocess.check_output(["gvpr", "-f", script], stdin=subprocess.DEVNULL) # Decode into text. We do this instead of `universal_newlines=True` above # because the trailing garbage can contain invalid UTF-8 data causing cryptic # failures. We want to correctly surface this as trailing garbage, not an # obscure UTF-8 decoding error. result = out.decode("utf-8", "replace") if examine == "indices": # check no indices are miscalculated index_re = ( r"^// index of space \(st\) :\s*(?P-?\d+)\s*<< must " r"NOT be less than -1$" ) for m in re.finditer(index_re, result, flags=re.MULTILINE): index = int(m.group("index")) assert index >= -1, "illegal index computed" if examine == "tokens": # check for text the author of 2138.gvpr expected to find assert ( "// tok[3] >>3456789<< should NOT include trailing spaces or " "junk chars" in result ), "token 3456789 not found or has trailing garbage" assert ( "// tok[7] >>012<< should NOT include trailing spaces or " "junk chars" in result ), "token 012 not found or has trailing garbage" @pytest.mark.xfail( reason="https://gitlab.com/graphviz/graphviz/-/issues/2159", strict=True ) def test_2159(): """ space for HTML TDs should be allocated equally when expanding to fill a TR https://gitlab.com/graphviz/graphviz/-/issues/2159 """ # locate our associated test case in this directory input = Path(__file__).parent / "2159.dot" assert input.exists(), "unexpectedly missing test case" # translate this to SVG svg = dot("svg", input) # load it as XML root = ET.fromstring(svg) # the first node is expected to contain: # • 1 polygon for the top column-spanning cell # • 5 polygons for the bottom row’s cells # • 1 polygon for the outer table border polygons = root.findall( ".//{http://www.w3.org/2000/svg}title[.='node1']/../{http://www.w3.org/2000/svg}polygon" ) assert len(polygons) == 7 # extract the points delimiting the top row top_row = polygons[0] points = [ [float(n) for n in p.split(",")] for p in top_row.get("points").split(" ") ] assert len(points) == 5, "polygon not rectangular" (ul_x, _), (ll_x, _), (lr_x, _), (ur_x, _), (orig_x, _) = points assert ul_x == ll_x, "polygon left edge is not vertical" assert lr_x == ur_x, "polygon right edge is not vertical" assert orig_x == ul_x, "polygon is not closed" left = ul_x right = ur_x # extract the points for each cell in the bottom row bottom_row = [] for cell in polygons[1:-1]: bottom_row += [ [[float(n) for n in p.split(",")] for p in cell.get("points").split(" ")] ] assert len(bottom_row[-1]) == 5, "polygon not rectangular" # extract the widths of each cell in the bottom row widths = [] for cell in bottom_row: (ul_x, _), _, _, (ur_x, _), _ = cell widths += [ur_x - ul_x] # these should approximately sum to the width of the top row assert math.isclose( sum(widths), right - left, abs_tol=10 ), "bottom row not expanded to fill the space" # the width of each cell should be approximately equal for width in widths[1:]: assert math.isclose(width, widths[0], abs_tol=5), "cells not evenly expanded" def test_2168(): """ using spline routing should not cause fdp/neato to infinite loop https://gitlab.com/graphviz/graphviz/-/issues/2168 """ # locate our associated test case in this directory input = Path(__file__).parent / "2168.dot" assert input.exists(), "unexpectedly missing test case" subprocess.check_call(["fdp", "-o", os.devnull, input], timeout=5) def test_2168_1(): """ using spline routing should not cause fdp/neato to infinite loop https://gitlab.com/graphviz/graphviz/-/issues/2168 """ # locate our associated test case in this directory input = Path(__file__).parent / "2168_1.dot" assert input.exists(), "unexpectedly missing test case" subprocess.check_call(["fdp", "-o", os.devnull, input], timeout=5) def test_2168_2(): """ using spline routing should not cause fdp/neato to infinite loop https://gitlab.com/graphviz/graphviz/-/issues/2168 """ # locate our associated test case in this directory input = Path(__file__).parent / "2168_2.dot" assert input.exists(), "unexpectedly missing test case" subprocess.check_call(["fdp", "-o", os.devnull, input], timeout=5) def test_2168_3(): """ using spline routing should not cause fdp/neato to infinite loop https://gitlab.com/graphviz/graphviz/-/issues/2168 """ # locate our associated test case in this directory input = Path(__file__).parent / "2168_3.dot" assert input.exists(), "unexpectedly missing test case" subprocess.check_call(["fdp", "-o", os.devnull, input], timeout=5) def test_2168_4(): """ using spline routing should not cause fdp/neato to infinite loop https://gitlab.com/graphviz/graphviz/-/issues/2168 """ # locate our associated test case in this directory input = Path(__file__).parent / "2168_4.dot" assert input.exists(), "unexpectedly missing test case" subprocess.check_call(["fdp", "-o", os.devnull, input], timeout=5) def test_2168_5(): """ using spline routing should not cause fdp/neato to infinite loop https://gitlab.com/graphviz/graphviz/-/issues/2168 """ # locate our associated test case in this directory input = Path(__file__).parent / "2168_5.dot" assert input.exists(), "unexpectedly missing test case" out = subprocess.check_output( ["fdp", "-o", os.devnull, input], stderr=subprocess.STDOUT, universal_newlines=True, ) assert ( "Warning: the bounding boxes of some nodes touch - falling back to straight line edges" in out ) def test_2179(): """ processing a label with an empty line should not yield a warning https://gitlab.com/graphviz/graphviz/-/issues/2179 """ # a graph containing a label with an empty line input = 'digraph "" {\n 0 -> 1 [fontname="Lato",label=<
1>]\n}' # run a graph with an empty label through Graphviz with subprocess.Popen( ["dot", "-Tsvg", "-o", os.devnull], stdin=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) as p: _, stderr = p.communicate(input) assert p.returncode == 0 assert ( "Warning: no hard-coded metrics for" not in stderr ), "incorrect warning triggered" def test_2179_1(): """ processing a label with a line containing only a space should not yield a warning https://gitlab.com/graphviz/graphviz/-/issues/2179 """ # a graph containing a label with a line containing only a space input = 'digraph "" {\n 0 -> 1 [fontname="Lato",label=<
1>]\n}' # run a graph with an empty label through Graphviz with subprocess.Popen( ["dot", "-Tsvg", "-o", os.devnull], stdin=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) as p: _, stderr = p.communicate(input) assert p.returncode == 0 assert ( "Warning: no hard-coded metrics for" not in stderr ), "incorrect warning triggered" @pytest.mark.skipif(which("nop") is None, reason="nop not available") def test_2184_1(): """ nop should not reposition labelled graph nodes https://gitlab.com/graphviz/graphviz/-/issues/2184 """ # run `nop` on a sample with a labelled graph node at the end source = Path(__file__).parent / "2184.dot" assert source.exists(), "missing test case" nopped = subprocess.check_output(["nop", source], universal_newlines=True) # the normalized output should have a graph with no label within # `clusterSurround1` m = re.search( r"\bclusterSurround1\b.*\bgraph\b.*\bcluster1\b", nopped, flags=re.DOTALL ) assert m is not None, "nop rearranged a graph in a not-semantically-preserving way" def test_2184_2(): """ canonicalization should not reposition labelled graph nodes https://gitlab.com/graphviz/graphviz/-/issues/2184 """ # canonicalize a sample with a labelled graph node at the end source = Path(__file__).parent / "2184.dot" assert source.exists(), "missing test case" canonicalized = dot("canon", source) # the canonicalized output should have a graph with no label within # `clusterSurround1` m = re.search( r"\bclusterSurround1\b.*\bgraph\b.*\bcluster1\b", canonicalized, flags=re.DOTALL ) assert ( m is not None ), "`dot -Tcanon` rearranged a graph in a not-semantically-preserving way" def test_2185_1(): """ GVPR should deal with strings correctly https://gitlab.com/graphviz/graphviz/-/issues/2185 """ # find our collocated GVPR program script = Path(__file__).parent / "2185.gvpr" assert script.exists(), "missing test case" # run this with NUL input, checking output is valid UTF-8 gvpr(script) def test_2185_2(): """ GVPR should deal with strings correctly https://gitlab.com/graphviz/graphviz/-/issues/2185 """ # find our collocated GVPR program script = Path(__file__).parent / "2185.gvpr" assert script.exists(), "missing test case" # run this with NUL input out = subprocess.check_output(["gvpr", "-f", script], stdin=subprocess.DEVNULL) # decode output in a separate step to gracefully cope with garbage unicode out = out.decode("utf-8", "replace") # deal with Windows eccentricities eol = "\r\n" if platform.system() == "Windows" else "\n" expected = f"one two three{eol}" # check the first line is as expected assert out.startswith(expected), "incorrect GVPR interpretation" def test_2185_3(): """ GVPR should deal with strings correctly https://gitlab.com/graphviz/graphviz/-/issues/2185 """ # find our collocated GVPR program script = Path(__file__).parent / "2185.gvpr" assert script.exists(), "missing test case" # run this with NUL input out = subprocess.check_output(["gvpr", "-f", script], stdin=subprocess.DEVNULL) # decode output in a separate step to gracefully cope with garbage unicode out = out.decode("utf-8", "replace") # deal with Windows eccentricities eol = "\r\n" if platform.system() == "Windows" else "\n" expected = f"one two three{eol}one five three{eol}" # check the first two lines are as expected assert out.startswith(expected), "incorrect GVPR interpretation" def test_2185_4(): """ GVPR should deal with strings correctly https://gitlab.com/graphviz/graphviz/-/issues/2185 """ # find our collocated GVPR program script = Path(__file__).parent / "2185.gvpr" assert script.exists(), "missing test case" # run this with NUL input out = subprocess.check_output(["gvpr", "-f", script], stdin=subprocess.DEVNULL) # decode output in a separate step to gracefully cope with garbage unicode out = out.decode("utf-8", "replace") # deal with Windows eccentricities eol = "\r\n" if platform.system() == "Windows" else "\n" expected = f"one two three{eol}one five three{eol}99{eol}" # check the first three lines are as expected assert out.startswith(expected), "incorrect GVPR interpretation" def test_2185_5(): """ GVPR should deal with strings correctly https://gitlab.com/graphviz/graphviz/-/issues/2185 """ # find our collocated GVPR program script = Path(__file__).parent / "2185.gvpr" assert script.exists(), "missing test case" # run this with NUL input out = subprocess.check_output(["gvpr", "-f", script], stdin=subprocess.DEVNULL) # decode output in a separate step to gracefully cope with garbage unicode out = out.decode("utf-8", "replace") # deal with Windows eccentricities eol = "\r\n" if platform.system() == "Windows" else "\n" expected = f"one two three{eol}one five three{eol}99{eol}Constant{eol}" # check the first four lines are as expected assert out.startswith(expected), "incorrect GVPR interpretation" @pytest.mark.xfail(strict=True) # FIXME def test_2193(): """ the canonical format should be stable https://gitlab.com/graphviz/graphviz/-/issues/2193 """ # find our collocated test case input = Path(__file__).parent / "2193.dot" assert input.exists(), "unexpectedly missing test case" # derive the initial canonicalization canonical = dot("canon", input) # now canonicalize this again to see if it changes new = dot("canon", source=canonical) assert canonical == new, "canonical translation is not stable" @pytest.mark.skipif(which("gvpr") is None, reason="GVPR not available") def test_2211(): """ GVPR’s `index` function should return correct results https://gitlab.com/graphviz/graphviz/-/issues/2211 """ # find our collocated test case program = Path(__file__).parent / "2211.gvpr" assert program.exists(), "unexpectedly missing test case" # run it through GVPR output = gvpr(program) # it should have found the right string indices for characters assert ( output == "index: 9 should be 9\n" "index: 3 should be 3\n" "index: -1 should be -1\n" ) def test_2215(): """ Graphviz should not crash with `-v` https://gitlab.com/graphviz/graphviz/-/issues/2215 """ # try it on a simple graph input = "graph g { a -- b; }" subprocess.run(["dot", "-v"], input=input, check=True, universal_newlines=True) # try the same on a labelled version of this graph input = 'graph g { node[label=""] a -- b; }' subprocess.run(["dot", "-v"], input=input, check=True, universal_newlines=True) @pytest.mark.xfail( os.getenv("build_system") == "msbuild" or is_rocky(), strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/2241", ) def test_2241(): """ a graph with two nodes and one edge in each direction should be rendered with two visually distinct edges when using the neato engine and splines=true, not two edges on top of each other, visually looking like a single edge with both head and tail arrowheads. https://gitlab.com/graphviz/graphviz/-/issues/2241 """ # find our collocated test case input = Path(__file__).parent / "2241.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz svg = dot("svg", input) # load this as XML root = ET.fromstring(svg) # the output is expected to contain two paths which are well separated paths = root.findall(".//{http://www.w3.org/2000/svg}path") assert len(paths) == 2, "expected two paths in output" ellipses = root.findall(".//{http://www.w3.org/2000/svg}ellipse") assert len(ellipses) == 2, "expected two ellipses in output" # calculate the x coordinate of a vertical line which is equidistant from the two nodes x = statistics.mean(float(ellipse.get("cx")) for ellipse in ellipses) # for each edge path, get the y coordinate of a point on a line between the edge's endpoints # where the line intersects the node equidistant vertical line y_coordinates = [] for path in paths: d_attribute = path.get("d") points_str = re.split("[ C]", d_attribute.replace("M", "")) assert ( len(points_str) == 4 ), "expected four points in the 'd' attribute of the 'path' element" points = [ (float(x_str), float(y_str)) for x_str, y_str in [point_str.split(",") for point_str in points_str] ] dx = points[3][0] - points[0][0] dy = points[3][1] - points[0][1] y = points[0][1] + dy / dx * (x - points[0][0]) y_coordinates.append(y) # check that the lines are well separated vertically where they intersect the node equidistant # vertical line y_coordinates_abs_difference = abs(y_coordinates[1] - y_coordinates[0]) y_coordinates_abs_difference_when_ok = 11.5004437538844 y_coordinates_abs_difference_when_not_ok = 0.00658290568043185 min_y_coordinates_abs_difference = ( y_coordinates_abs_difference_when_ok + y_coordinates_abs_difference_when_not_ok ) / 2 assert y_coordinates_abs_difference > min_y_coordinates_abs_difference def test_2242(): """ repeated runs of a graph with subgraphs should yield a stable result https://gitlab.com/graphviz/graphviz/-/issues/2242 """ # get our baseline reference input = Path(__file__).parent / "2242.dot" assert input.exists(), "unexpectedly missing test case" ref = dot("png", input) # now repeat this, expecting it not to change for _ in range(20): png = dot("png", input) assert ref == png, "repeated rendering changed output" def test_2342(): """ using an arrow with size 0 should not trigger an assertion failure https://gitlab.com/graphviz/graphviz/-/issues/2342 """ # find our collocated test case input = Path(__file__).parent / "2342.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz dot("svg", input) @pytest.mark.skipif( os.getenv("build_system") == "msbuild", reason="https://gitlab.com/graphviz/graphviz/-/issues/1777", ) @pytest.mark.skipif( is_static_build(), reason="dynamic libraries are unavailable to link against in static builds", ) def test_2356(): """ Using `mindist` programmatically in a loop should not cause Windows crashes https://gitlab.com/graphviz/graphviz/-/issues/2356 """ # find co-located test source c_src = (Path(__file__).parent / "2356.c").resolve() assert c_src.exists(), "missing test case" # run the test run_c(c_src, link=["cgraph", "gvc"]) def test_2361(): """ using `ortho` and `concentrate` in combination should not cause a crash https://gitlab.com/graphviz/graphviz/-/issues/2361 """ # find our collocated test case input = Path(__file__).parent / "2361.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz dot("png", input) @pytest.mark.parametrize("arg", ("--filepath", "-Gimagepath")) def test_2396(arg: str): """ `--filepath` should work as a replacement for `$GV_FILE_PATH` https://gitlab.com/graphviz/graphviz/-/issues/2396 """ # use an arbitrary image we have in the tree image = Path(__file__).parent / "../cmd/gvedit/images/save.png" assert image.exists(), "missing test data" # a graph that tries to use the image by relative path slash = "/" if arg == "--filepath" else "" source = f'graph {{ N[image="{slash}save.png"]; }}' # run this through Graphviz proc = subprocess.run( ["dot", "-Tsvg", f"{arg}={image.parent}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, input=source, cwd=Path(__file__).parent, universal_newlines=True, check=True, ) # work around macOS warnings stderr = remove_xtype_warnings(proc.stderr).strip() assert stderr == "", "loading an image by relative path produced warnings" # whether we used `imagepath` or `filepath` should affect whether we get a leading # slash if arg == "-Gimagepath": assert '"save.png"' in proc.stdout, "incorrect relative path in output" else: assert '"/save.png"' in proc.stdout, "incorrect relative path in output" def test_2481(): """ `dot` should not exit with a syntax error if keywords are mixed-case https://gitlab.com/graphviz/graphviz/-/issues/2481 """ # try a simple graph with uppercase characters in 'digraph' input = "diGraph { }" # ensure this does not trigger warnings with subprocess.Popen( ["dot"], stdin=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) as p: _, stderr = p.communicate(input) assert p.returncode == 0, "mixed-case keyword was rejected" assert "syntax error" not in stderr, "dot displayed a syntax error message" @pytest.mark.skipif( os.getenv("build_system") == "msbuild", reason="https://gitlab.com/graphviz/graphviz/-/issues/1777", ) @pytest.mark.skipif( is_static_build(), reason="dynamic libraries are unavailable to link against in static builds", ) def test_2484(): """ Graphviz context should not preserve state across calls https://gitlab.com/graphviz/graphviz/-/issues/2484 """ # find our co-located driver c_src = (Path(__file__).parent / "2484.c").resolve() assert c_src.exists(), "missing test case" # find co-located input to the driver dot_src = (Path(__file__).parent / "2484.dot").resolve() assert dot_src.exists(), "missing test case" # compile and run it run_c( c_src, ["-Kdot", "-Tpng", str(dot_src), "-o", os.devnull], link=["cgraph", "gvc"], ) def test_package_version(): """ The graphviz_version.h header should define a non-empty PACKAGE_VERSION """ # FIXME: Remove skip when # https://gitlab.com/graphviz/graphviz/-/issues/1777 is fixed if os.getenv("build_system") == "msbuild": pytest.skip("Windows MSBuild release does not contain any header files (#1777)") # find co-located test source c_src = (Path(__file__).parent / "get-package-version.c").resolve() assert c_src.exists(), "missing test case" # run the test package_version, _ = run_c(c_src) assert ( package_version.strip() != "" ), "invalid PACKAGE_VERSION in graphviz_version.h" def test_user_shapes(): """ Graphviz should understand how to embed a custom SVG image as a node’s shape """ # find our collocated test case input = Path(__file__).parent / "usershape.dot" assert input.exists(), "unexpectedly missing test case" # ask Graphviz to translate this to SVG output = subprocess.check_output( ["dot", "-Tsvg", input], cwd=os.path.dirname(__file__), universal_newlines=True ) # the external SVG should have been parsed and is now referenced assert ' str: """translate Windows path separators to ease running this on non-Windows""" return path.replace("\\", os.sep) # FIXME: files missing a filters file FILTERS_WAIVERS = (Path("lib/version/version.vcxproj"),) filters = Path(f"{str(vcxproj)}.filters") if vcxproj.relative_to(ROOT) not in FILTERS_WAIVERS: assert filters.exists(), f"no {str(filters)} corresponding to {str(vcxproj)}" # namespace the MSBuild elements live in ns = "http://schemas.microsoft.com/developer/msbuild/2003" # parse XML out of the .vcxproj file srcs1 = set() tree = ET.parse(vcxproj) root = tree.getroot() for elem in root: if elem.tag == f"{{{ns}}}ItemGroup": for child in elem: if child.tag in (f"{{{ns}}}ClInclude", f"{{{ns}}}ClCompile"): filename = fix_sep(child.attrib["Include"]) assert ( filename not in srcs1 ), f"duplicate source {filename} in {str(vcxproj)}" srcs1.add(filename) # parse XML out of the .vcxproj.filters file if filters.exists(): srcs2 = set() tree = ET.parse(filters) root = tree.getroot() for elem in root: if elem.tag == f"{{{ns}}}ItemGroup": for child in elem: if child.tag in (f"{{{ns}}}ClInclude", f"{{{ns}}}ClCompile"): filename = fix_sep(child.attrib["Include"]) assert ( filename not in srcs2 ), f"duplicate source {filename} in {str(filters)}" srcs2.add(filename) assert ( srcs1 == srcs2 ), "mismatch between sources in {str(vcxproj)} and {str(filters)}" @pytest.mark.skipif(which("gvmap") is None, reason="gvmap not available") def test_gvmap_fclose(): """ gvmap should not attempt to fclose(NULL). This example will trigger a crash if this bug has been reintroduced and Graphviz is built with ASan support. """ # a reasonable input graph input = ( 'graph "Alík: Na vlastní oči" {\n' ' graph [bb="0,0,128.9,36",\n' " concentrate=true,\n" " overlap=prism,\n" " start=3\n" " ];\n" ' node [label="\\N"];\n' " {\n" " bob [height=0.5,\n" ' pos="100.95,18",\n' " width=0.77632];\n" " }\n" " {\n" " alice [height=0.5,\n" ' pos="32.497,18",\n' " width=0.9027];\n" " }\n" ' alice -- bob [pos="65.119,18 67.736,18 70.366,18 72.946,18"];\n' " bob -- alice;\n" "}" ) # pass this through gvmap proc = subprocess.run(["gvmap"], input=input.encode("utf-8")) assert proc.returncode in (0, 1), "gvmap crashed" @pytest.mark.skipif(which("gvpr") is None, reason="gvpr not available") def test_gvpr_usage(): """ gvpr usage information should be included when erroring on a malformed command """ # create a temporary directory, under which we know no files will exist with tempfile.TemporaryDirectory() as tmp: # ask GVPR to process a non-existent file with subprocess.Popen( ["gvpr", "-f", "nofile"], stderr=subprocess.PIPE, cwd=tmp, universal_newlines=True, ) as p: _, stderr = p.communicate() assert p.returncode != 0, "GVPR accepted a non-existent file" # the stderr output should have contained full usage instructions assert ( "-o - write output to ; stdout by default" in stderr ), "truncated or malformed GVPR usage information" def test_2225(): """ sfdp should not segfault with curved splines https://gitlab.com/graphviz/graphviz/-/issues/2225 """ # locate our associated test case in this directory input = Path(__file__).parent / "2225.dot" assert input.exists(), "unexpectedly missing test case" # run this through sfdp p = subprocess.run( ["sfdp", "-Gsplines=curved", "-o", os.devnull, input], stderr=subprocess.PIPE, universal_newlines=True, ) # if sfdp was built without libgts, it will not handle anything non-trivial no_gts_error = "remove_overlap: Graphviz not built with triangulation library" if no_gts_error in p.stderr: assert p.returncode != 0, "sfdp returned success after an error message" return p.check_returncode() def test_2257(): """ `$GV_FILE_PATH` being set should prevent Graphviz from running `$GV_FILE_PATH` was an environment variable formerly used to implement a file system sandboxing policy when Graphviz was exposed to the internet via a web server. These days, there are safer and more robust techniques to sandbox Graphviz and so `$GV_FILE_PATH` usage has been removed. But if someone attempts to use this legacy mechanism, we do not want Graphviz to “fail-open,” starting anyway and silently ignoring `$GV_FILE_PATH` giving the user the false impression the sandboxing is in force. https://gitlab.com/graphviz/graphviz/-/issues/2257 """ # locate our associated test case in this directory input = Path(__file__).parent / "2257.dot" assert input.exists(), "unexpectedly missing test case" env = os.environ.copy() env["GV_FILE_PATH"] = "/tmp" # Graphviz should refuse to process an input file with pytest.raises(subprocess.CalledProcessError): subprocess.check_call(["dot", "-Tsvg", input, "-o", os.devnull], env=env) def test_2258(): """ 'id' attribute should be propagated to all graph children in output https://gitlab.com/graphviz/graphviz/-/issues/2258 """ # locate our associated test case in this directory input = Path(__file__).parent / "2258.dot" assert input.exists(), "unexpectedly missing test case" # translate this to SVG svg = dot("svg", input) # load this as XML root = ET.fromstring(svg) # the output is expected to contain a number of linear gradients, all of which # are semantic children of graph marked `id = "G2"` gradients = root.findall(".//{http://www.w3.org/2000/svg}linearGradient") assert len(gradients) > 0, "no gradients in output" for gradient in gradients: assert "G2" in gradient.get("id"), "ID was not applied to linear gradients" def test_2270(tmp_path: Path): """ `-O` should result in the expected output filename https://gitlab.com/graphviz/graphviz/-/issues/2270 """ # write a simple graph input = tmp_path / "hello.gv" input.write_text("digraph { hello -> world }", encoding="utf-8") # process it with Graphviz subprocess.check_call( ["dot", "-T", "plain:dot:core", "-O", "hello.gv"], cwd=tmp_path ) # it should have produced output in the expected location output = tmp_path / "hello.gv.core.dot.plain" assert output.exists(), "-O resulted in an unexpected output filename" @pytest.mark.skipif( is_static_build(), reason="dynamic libraries are unavailable to link against in static builds", ) def test_2272(): """ using `agmemread` with an unterminated string should not fail assertions https://gitlab.com/graphviz/graphviz/-/issues/2272 """ # FIXME: Remove skip when # https://gitlab.com/graphviz/graphviz/-/issues/1777 is fixed if os.getenv("build_system") == "msbuild": pytest.skip("Windows MSBuild release does not contain any header files (#1777)") # find co-located test source c_src = (Path(__file__).parent / "2272.c").resolve() assert c_src.exists(), "missing test case" # run the test run_c(c_src, link=["cgraph", "gvc"]) def test_2272_2(): """ An unterminated string in the source should not crash Graphviz. Variant of `test_2272`. """ # a graph with an open string graph = 'graph { a[label="abc' # process it with Graphviz, which should not crash p = subprocess.run(["dot", "-o", os.devnull], input=graph, universal_newlines=True) assert p.returncode != 0, "dot accepted invalid input" assert p.returncode == 1, "dot crashed" def test_2282(): """ using the `fdp` layout with JSON output should result in valid JSON https://gitlab.com/graphviz/graphviz/-/issues/2282 """ # locate our associated test case in this directory input = Path(__file__).parent / "2282.dot" assert input.exists(), "unexpectedly missing test case" # translate this to JSON output = dot("json", input) # confirm this is valid JSON json.loads(output) def test_2283(): """ `beautify=true` should correctly space nodes https://gitlab.com/graphviz/graphviz/-/issues/2283 """ # find our collocated test case input = Path(__file__).parent / "2283.dot" assert input.exists(), "unexpectedly missing test case" # translate this to SVG p = subprocess.run( ["dot", "-Tsvg", input], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) # if sfdp was built without libgts, it will not handle anything non-trivial no_gts_error = "remove_overlap: Graphviz not built with triangulation library" if no_gts_error in p.stderr: assert p.returncode != 0, "sfdp returned success after an error message" return p.check_returncode() svg = p.stdout # parse this into something we can inspect root = ET.fromstring(svg) # find node N0 n0s = root.findall( ".//{http://www.w3.org/2000/svg}title[.='N0']/../{http://www.w3.org/2000/svg}ellipse" ) assert len(n0s) == 1, "failed to locate node N0" n0 = n0s[0] # find node N1 n1s = root.findall( ".//{http://www.w3.org/2000/svg}title[.='N1']/../{http://www.w3.org/2000/svg}ellipse" ) assert len(n1s) == 1, "failed to locate node N1" n1 = n1s[0] # find node N6 n6s = root.findall( ".//{http://www.w3.org/2000/svg}title[.='N6']/../{http://www.w3.org/2000/svg}ellipse" ) assert len(n6s) == 1, "failed to locate node N6" n6 = n6s[0] # N1 and N6 should not have been drawn on top of each other n1_x = float(n1.attrib["cx"]) n1_y = float(n1.attrib["cy"]) n6_x = float(n6.attrib["cx"]) n6_y = float(n6.attrib["cy"]) def sameish(a: float, b: float) -> bool: EPSILON = 0.2 return -EPSILON < abs(a - b) < EPSILON assert not ( sameish(n1_x, n6_x) and sameish(n1_y, n6_y) ), "N1 and N6 placed identically" # use the Law of Cosines to compute the angle between N0→N1 and N0→N6 n0_x = float(n0.attrib["cx"]) n0_y = float(n0.attrib["cy"]) n0_n1_dist = math.dist((n0_x, n0_y), (n1_x, n1_y)) n0_n6_dist = math.dist((n0_x, n0_y), (n6_x, n6_y)) n1_n6_dist = math.dist((n1_x, n1_y), (n6_x, n6_y)) angle = math.acos( (n0_n1_dist**2 + n0_n6_dist**2 - n1_n6_dist**2) / (2 * n0_n1_dist * n0_n6_dist) ) number_of_radial_nodes = 6 assert sameish( angle, 2 * math.pi / number_of_radial_nodes ), "nodes not placed evenly" def test_2285(): """ using the `svg_inline` output should result in SVG you can inline to HTML https://gitlab.com/graphviz/graphviz/-/issues/2285 """ # locate our associated test case in this directory input = Path(__file__).parent / "2285.dot" assert input.exists(), "unexpectedly missing test case" # translate this to JSON output = dot("svg_inline", input) assert " 0, "no polygons in output" for polygon in polygons: m = re.match(r"url\((?P.*)\)$", polygon.get("fill")) if m is None: continue assert ( re.search(r"\bG2_", m.group("url")) is not None ), "ID G2 was not applied to polygon fill url" def test_2325(): """ using more than 63 styles and/or more than 128 style bytes should not trigger an out-of-bounds memory read https://gitlab.com/graphviz/graphviz/-/issues/2325 """ # locate our associated test case in this directory input = Path(__file__).parent / "2325.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz dot("svg", input) @pytest.mark.skipif(shutil.which("groff") is None, reason="groff not available") def test_2341(): """ PIC backend should generate correct comments https://gitlab.com/graphviz/graphviz/-/issues/2341 """ # a simple graph source = "digraph { a -> b; }" # generate PIC from this pic = dot("pic", source=source) # run this through groff groffed = subprocess.check_output( ["groff", "-Tascii", "-p"], input=pic, universal_newlines=True ) # it should not contain any comments assert ( re.search(r"^\s*#", groffed) is None ), "Graphviz comment remains in groff output" def test_2352(): """ referencing an all-one-line external SVG file should work https://gitlab.com/graphviz/graphviz/-/issues/2352 """ # locate our associated test case in this directory input = Path(__file__).parent / "2352.dot" assert input.exists(), "unexpectedly missing test case" # translate it to SVG svg = subprocess.check_output( ["dot", "-Tsvg", input], cwd=Path(__file__).parent, universal_newlines=True ) assert '127 layers should not crash Graphviz https://gitlab.com/graphviz/graphviz/-/issues/2355 """ # construct a graph with 128 layers graph = io.StringIO() graph.write("digraph {\n") layers = ":".join(f"l{i}" for i in range(128)) graph.write(f' layers="{layers}";\n') for i in range(128): graph.write(f' n{i}[layer="l{i}"];\n') graph.write("}\n") # process this with dot dot("svg", source=graph.getvalue()) @pytest.mark.xfail(strict=True) # FIXME def test_2368(): """ routesplines should not corrupt its `prev` and `next` indices https://gitlab.com/graphviz/graphviz/-/issues/2368 """ # locate our associated test case in this directory input = Path(__file__).parent / "2368.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz dot("svg", input) @pytest.mark.skipif(shutil.which("tclsh") is None, reason="tclsh not available") def test_2370(): """ tcldot should have a version number TCL accepts https://gitlab.com/graphviz/graphviz/-/issues/2370 """ # ask TCL to import the Graphviz package response = subprocess.check_output( ["tclsh"], stderr=subprocess.STDOUT, input="package require Tcldot;", universal_newlines=True, ) assert ( "error reading package index file" not in response ), "tcldot cannot be loaded by TCL" def test_2371(): """ Large graphs should not cause rectangle area calculation overflows https://gitlab.com/graphviz/graphviz/-/issues/2371 """ # locate our associated test case in this directory input = Path(__file__).parent / "2371.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz subprocess.check_call(["dot", "-Tsvg", "-Knop2", "-o", os.devnull, input]) @pytest.mark.skipif( platform.system() == "Windows", reason="gvplugin_list symbol is not exposed on Windows", ) def test_2375(): """ `gvplugin_list` should return full plugin names https://gitlab.com/graphviz/graphviz/-/issues/2375 """ # find co-located test source c_src = (Path(__file__).parent / "2375.c").resolve() assert c_src.exists(), "missing test case" # run the test run_c(c_src, link=["gvc"]) def test_2377(): """ 3 letter hex color codes should be accepted https://gitlab.com/graphviz/graphviz/-/issues/2377 """ # run some 6 letter color input through Graphviz input = 'digraph { n [color="#cc0000" fillcolor="#ffcc00" style=filled] }' svg1 = dot("svg", source=input) # try the equivalent with 3 letter colors input = 'digraph { n [color="#c00" fillcolor="#fc0" style=filled] }' svg2 = dot("svg", source=input) assert svg1 == svg2, "3 letter hex colors were not translated correctly" def test_2390(): """ using an out of range `xdotversion` should not crash Graphviz https://gitlab.com/graphviz/graphviz/-/issues/2390 """ # some input with an invalid large `xdotversion` input = 'graph { xdotversion=99; n[label="hello world"]; }' # run this through Graphviz dot("xdot", source=input) def test_2391(): """ `nslimit1=0` should not cause Graphviz to crash https://gitlab.com/graphviz/graphviz/-/issues/2391 """ # locate our associated test case in this directory input = Path(__file__).parent / "2391.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz dot("svg", input) def test_2391_1(): """ `nslimit1=0` with a label should not cause Graphviz to crash https://gitlab.com/graphviz/graphviz/-/issues/2391 """ # locate our associated test case in this directory input = Path(__file__).parent / "2391_1.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz dot("svg", input) @pytest.mark.xfail( platform.system() == "Windows" and not is_mingw(), reason="cannot link Agdirected on Windows", strict=True, ) # FIXME def test_2397(): """ escapes in strings should be handled correctly https://gitlab.com/graphviz/graphviz/-/issues/2397 """ # find co-located test source c_src = (Path(__file__).parent / "2397.c").resolve() assert c_src.exists(), "missing test case" # run this to generate a graph source, _ = run_c(c_src, link=["cgraph", "gvc"]) # this should have produced a valid graph dot("svg", source=source) def test_2397_1(): """ a variant of test_2397 that confirms the same works via the command line https://gitlab.com/graphviz/graphviz/-/issues/2397 """ source = 'digraph { a[label="foo\\\\\\"bar"]; }' # run this through dot output = dot("dot", source=source) # the output should be valid dot dot("svg", source=output) @pytest.mark.skipif(shutil.which("shellcheck") is None, reason="shellcheck unavailable") def test_2404(): """ shell syntax used by gvmap should be correct https://gitlab.com/graphviz/graphviz/-/issues/2404 """ gvmap_sh = Path(__file__).parent / "../cmd/gvmap/gvmap.sh" subprocess.check_call(["shellcheck", "-S", "error", gvmap_sh]) def test_2406(): """ arrow types like `invdot` and `onormalonormal` should be displayed correctly https://gitlab.com/graphviz/graphviz/-/issues/2406 """ # locate our associated test case in this directory input = Path(__file__).parent / "2406.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz output = dot("svg", input) # the rounded hollows should be present assert re.search(r"\bellipse\b", output), "missing element of invdot arrow" @pytest.mark.parametrize("source", ("2413_1.dot", "2413_2.dot")) def test_2413(source: str): """ graphs that induce an edge length > 65535 should be supported https://gitlab.com/graphviz/graphviz/-/issues/2413 """ # locate our associated test case in this directory input = Path(__file__).parent / source assert input.exists(), "unexpectedly missing test case" # run it through Graphviz proc = subprocess.run( ["dot", "-Tsvg", "-o", os.devnull, input], stderr=subprocess.PIPE, check=True, universal_newlines=True, ) # work around macOS warnings stderr = remove_xtype_warnings(proc.stderr).strip() # no warnings should have been generated assert stderr == "", "long edges resulted in a warning" @pytest.mark.skipif( os.getenv("build_system") == "msbuild", reason="vt target is not supported under MS Build", ) def test_2429(): """ the vt target should be usable https://gitlab.com/graphviz/graphviz/-/issues/2429 """ # a basic graph source = "digraph { a -> b; }" # run it through Graphviz dot("vt", source=source) @pytest.mark.skipif(which("nop") is None, reason="nop not available") @pytest.mark.xfail( strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/2436" ) def test_2436(): """ nop should preserve empty labels https://gitlab.com/graphviz/graphviz/-/issues/2436 """ # locate our associated test case in this directory input = Path(__file__).parent / "2436.dot" assert input.exists(), "unexpectedly missing test case" # run it through nop nop = which("nop") output = subprocess.check_output([nop, input], universal_newlines=True) # the empty label should be present assert re.search(r'\blabel\s*=\s*""', output), "empty label was not preserved" @pytest.mark.xfail( strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/2434" ) def test_2434(): """ the order in which `agmemread` and `gvContext` calls are made should have no impact https://gitlab.com/graphviz/graphviz/-/issues/2434 """ # find co-located test source c_src = (Path(__file__).parent / "2434.c").resolve() assert c_src.exists(), "missing test case" # generate an SVG by calling `gvContext` first before, _ = run_c(c_src, ["before"], link=["cgraph", "gvc"]) # generate an SVG by calling `gvContext` second after, _ = run_c(c_src, ["after"], link=["cgraph", "gvc"]) # resulting images should be identical assert before == after, "agmemread/gvContext ordering affected image output" @pytest.mark.xfail( strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/2437" ) def test_2437(): """ both an arrowhead and an arrowtail shall be created when using dir=both, compass ports, an edge default attribute and rank=same https://gitlab.com/graphviz/graphviz/-/issues/2437 """ # locate our associated test case in this directory input = Path(__file__).parent / "2437.dot" assert input.exists(), "unexpectedly missing test case" # translate this to SVG svg = dot("svg", input) # load this as XML root = ET.fromstring(svg) # The output is expected to contain tree polygons. The graph "background" # polygon, the arrowhead polygon and the arrowtail polygon. polygons = root.findall(".//{http://www.w3.org/2000/svg}polygon") assert len(polygons) == 3, "wrong number of polygons in output" @pytest.mark.xfail( strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/2416" ) def test_2416(): """ `splines=curved` should not affect arrow directions https://gitlab.com/graphviz/graphviz/-/issues/2416 """ # an input graph that provokes the problem input = "digraph G { splines=curved; b -> a; a -> b; }" # run it through Graphviz output = dot("json", source=input) data = json.loads(output) edges = data["edges"] assert len(edges) == 2, "unexpected number of output edges" # extract the height each edge’s arrow starts at y_1 = edges[0]["_hdraw_"][3]["points"][0][1] y_2 = edges[1]["_hdraw_"][3]["points"][0][1] # assuming the graph is vertical, these should not be too close assert abs(y_1 - y_2) > 1, "edge arrows appear to be drawn next to the same node" @pytest.mark.skipif(which("gvpr") is None, reason="GVPR not available") def test_2454(): """ gvpr should support sscanf https://gitlab.com/graphviz/graphviz/-/issues/2454 """ # an input graph that provokes the problem input = "graph x{a -- {b c}}" # run it through Graphviz output = dot("dot", source=input) # run it through gvpr program = Path(__file__).parent / "2454.gvpr" with subprocess.Popen( ["gvpr", "-cf", program], stdin=subprocess.PIPE, universal_newlines=True ) as p: p.communicate(output) assert p.returncode == 0, "gvpr failed" @pytest.mark.skipif(which("twopi") is None, reason="twopi not available") @pytest.mark.xfail( strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/2457" ) def test_2457(): """ node definition order should not affect twopi’s layout https://gitlab.com/graphviz/graphviz/-/issues/2457 """ # locate our associated test cases in this directory case1 = Path(__file__).parent / "2457_1.dot" assert case1.exists(), "unexpectedly missing test case" case2 = Path(__file__).parent / "2457_2.dot" assert case2.exists(), "unexpectedly missing test case" # tweak the environment to force deterministic PDF generation env = os.environ.copy() env["SOURCE_DATE_EPOCH"] = "0" # generate PDFs pdf1 = subprocess.check_output(["twopi", "-Tpdf", case1], env=env) pdf2 = subprocess.check_output(["twopi", "-Tpdf", case2], env=env) assert pdf1 == pdf2, "node definition order affected PDF generation" @pytest.mark.xfail( strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/2458" ) def test_2458(): """ `pack=true` should not result in edge labels disappearing https://gitlab.com/graphviz/graphviz/-/issues/2458 """ # locate our associated test case in this directory input = Path(__file__).parent / "2458.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz output = dot("svg", input) # the edge label should be present assert re.search(r"\bconnected\b", output), "missing edge label" def test_2460(): """ labels involving back slashes should come out correctly in JSON https://gitlab.com/graphviz/graphviz/-/issues/2460 """ # locate our associated test case in this directory input = Path(__file__).parent / "2460.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz output = dot("json", input) data = json.loads(output) assert ( data["objects"][0]["_ldraw_"][2]["text"] == r"double back slash in label \\. End should be the last word - End" ), "back slashes in labels handled incorrectly" @pytest.mark.xfail( strict=platform.system() != "Windows", reason="https://gitlab.com/graphviz/graphviz/-/issues/2470", ) def test_2470(): """ another “trouble in init_rank variant” https://gitlab.com/graphviz/graphviz/-/issues/2470 """ # locate our associated test case in this directory input = Path(__file__).parent / "2470.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz dot("ps", input) @pytest.mark.xfail( reason="https://gitlab.com/graphviz/graphviz/-/issues/2471", strict=True, ) def test_2471(): """ another “trouble in init_rank variant” https://gitlab.com/graphviz/graphviz/-/issues/2471 """ # locate our associated test case in this directory input = Path(__file__).parent / "2471.dot" assert input.exists(), "unexpectedly missing test case" # run it through Graphviz dot("png", input) @pytest.mark.xfail( is_rocky_8(), reason="Cairo is \d+(\.\d+)?,\d+(\.\d+)?)\b", node["pos"]).group( "start" ) assert start not in starts, "nodes overlap" starts += [start] @pytest.mark.skipif(shutil.which("tclsh") is None, reason="tclsh not available") @pytest.mark.skipif( platform.system() == "Windows", reason="pexpect.spawn is not available on Windows " "(https://pexpect.readthedocs.io/en/stable/overview.html#pexpect-on-windows)", ) def test_2568(): """ tags used in TCL output should be usable for later lookup https://gitlab.com/graphviz/graphviz/-/issues/2568 """ # locate the TCL input for this test prelude = Path(__file__).parent / "2568.tcl" assert prelude.exists(), "unexpectedly missing test collateral" # startup TCL and load our graph setup code proc = pexpect.spawn("tclsh", timeout=1) proc.expect("% ") proc.sendline(f'source "{shlex.quote(str(prelude))}"') # look for tags to query while True: index = proc.expect( [ "invalid command name", re.compile(rb"-tags {\d(?P(edge|node)0x[\da-fA-F]+)}"), pexpect.TIMEOUT, ] ) # stdout and stderr are multiplexed onto the same stream by `pexpect`, so if one # of the commands we previously entered was not recognized, we will see an error # at the end of the output stream assert index != 0, "at least one tag was not recognized" # if we got no output within 1s, assume we are done and try to neatly exit if index == 2: proc.sendeof() proc.wait() break tag = proc.match.group("tag").decode("utf-8") # …try to look up its corresponding entities if tag.startswith("edge"): cmd = "listnodes" else: cmd = "listedges" proc.sendline(f"{tag} {cmd}") @pytest.mark.skipif(which("sfdp") is None, reason="sfdp not available") def test_2572(): """ sfdp should be able to find non-overlapping layouts https://gitlab.com/graphviz/graphviz/-/issues/2572 """ # locate our associated test case in this directory input = Path(__file__).parent / "2572.dot" assert input.exists(), "unexpectedly missing test case" # run this through SFDP and convert this to JSON sfdp = which("sfdp") layout = subprocess.check_output( [sfdp, "-Kneato", "-Tjson", input], universal_newlines=True ) parsed = json.loads(layout) @dataclasses.dataclass class Box: """ a geometric rectangle, defined by two of its corners """ llx: float # lower left X coordinate lly: float # lower left Y coordinate urx: float # upper right X coordinate ury: float # upper right Y coordinate def overlaps(self, other: "Box") -> bool: """ do we intersect the given box? """ if self.llx > other.urx: return False if self.lly > other.ury: return False if self.urx < other.llx: return False if self.ury < other.lly: return False return True nodes: List[Box] = [] for obj in parsed["objects"]: # extract the ellipse drawn for this node ellipses = [e for e in obj["_draw_"] if e["op"] == "e"] assert len(ellipses) == 1, "could not find ellipse for node" center_x, center_y, width, height = ellipses[0]["rect"] assert center_x >= width, "ellipse extends into negative X space" assert center_y >= height, "ellipse extends into negative Y space" node = Box( center_x - width, center_y - height, center_x + width, center_y + height ) assert not any(n.overlaps(node) for n in nodes), "nodes overlap" nodes.append(node) @pytest.mark.skipif(which("gvpr") is None, reason="GVPR not available") def test_2577(): """ accessing an uninitialized string should not corrupt GVPR’s state https://gitlab.com/graphviz/graphviz/-/issues/2577 """ # find our collocated test case program = Path(__file__).parent / "2577.gvpr" assert program.exists(), "unexpectedly missing test case" # run it through GVPR output = gvpr(program) # it should have printed an empty string for the uninitialized attribute assert ( "Before...\n<>\nAfter." in output ), "incorrect handling of uninitialized attribute in GVPR" @pytest.mark.skipif(which("gvpr") is None, reason="GVPR not available") def test_2577_1(): """ a variant of `test_2577` that does not involve attribute access https://gitlab.com/graphviz/graphviz/-/issues/2577 """ # run GVPR on a simple program gvprbin = which("gvpr") output = subprocess.check_output( [gvprbin, 'BEGIN { printf("hello%s world\\n", ""); }'], stdin=subprocess.DEVNULL, universal_newlines=True, ) # it should have printed the expected text assert output == "hello world\n", "gvpr cannot handle empty strings to printf" @pytest.mark.skipif(which("gvgen") is None, reason="gvgen not available") def test_2588(): """ `gvgen` should not crash when producing random graphs https://gitlab.com/graphviz/graphviz/-/issues/2588 """ gvgen = which("gvgen") # this execution depends on random numbers, so we need to run many times to # have a chance of provoking the bug for _ in range(200): subprocess.check_call([gvgen, "-R", "20"], stdout=subprocess.DEVNULL) @pytest.mark.skipif(which("edgepaint") is None, reason="edgepaint not available") @pytest.mark.skipif(which("gvgen") is None, reason="gvgen not available") def test_2591(): """ edgepaint color schemes should do something https://gitlab.com/graphviz/graphviz/-/issues/2591 """ # make an input graph gvgen = which("gvgen") graph = subprocess.check_output([gvgen, "-k", "5"], universal_newlines=True) # run it through neato laidout = subprocess.check_output( ["dot", "-Kneato", "-Goverlap=false"], input=graph, universal_newlines=True ) # try two different edgepaint invocations edgepaint = which("edgepaint") gray = subprocess.check_output( [edgepaint, "--angle=89.999", "--color_scheme=gray"], input=laidout, universal_newlines=True, ) rgb = subprocess.check_output( [edgepaint, "--angle=89.999", "--color_scheme=#00ff00,#0000ff"], input=laidout, universal_newlines=True, ) # process these into an image gray_svg = subprocess.check_output( ["dot", "-Kneato", "-n2", "-Tsvg"], input=gray, universal_newlines=True ) rgb_svg = subprocess.check_output( ["dot", "-Kneato", "-n2", "-Tsvg"], input=rgb, universal_newlines=True ) assert gray_svg != rgb_svg, "edgepaint --color_scheme had no effect" def test_changelog_dates(): """ Check the dates of releases in the changelog are correctly formatted """ changelog = Path(__file__).parent / "../CHANGELOG.md" with open(changelog, "rt", encoding="utf-8") as f: for lineno, line in enumerate(f, 1): m = re.match(r"## \[\d+\.\d+\.\d+\] [-–] (?P.*)$", line) if m is None: continue d = re.match(r"\d{4}-\d{2}-\d{2}", m.group("date")) assert ( d is not None ), f"CHANGELOG.md:{lineno}: date in incorrect format: {line}" @pytest.mark.skipif(which("gvpack") is None, reason="gvpack not available") def test_duplicate_hard_coded_metrics_warnings(): """ Check “no hard-coded metrics” warnings are not repeated """ # use the #2239 test case that happens to provoke this input = Path(__file__).parent / "2239.dot" assert input.exists(), "unexpectedly missing test case" # run it through gvpack gvpack = which("gvpack") p = subprocess.run( [gvpack, "-u", "-o", os.devnull, input], stderr=subprocess.PIPE, universal_newlines=True, ) assert ( p.stderr.count("no hard-coded metrics for 'sans'") <= 1 ), "multiple identical “no hard-coded metrics” warnings printed" @pytest.mark.parametrize("branch", (0, 1, 2, 3)) @pytest.mark.skipif(which("gvpr") is None, reason="gvpr not available") def test_gvpr_switches(branch: int): """ confirm the behavior of GVPR switch statements """ # an input GVPR program with multiple blocks and switches program = textwrap.dedent( f"""\ BEGIN {{ switch ({branch}) {{ case 0: printf("begin 0\\n"); break; case 1: printf("begin 1\\n"); break; case 2: printf("begin 2\\n"); break; default: printf("begin 3\\n"); break; }} }} END {{ switch ({branch}) {{ case 0: printf("end 0\\n"); break; case 1: printf("end 1\\n"); break; case 2: printf("end 2\\n"); break; default: printf("end 3\\n"); break; }} }} """ ) # run this through GVPR with no input graph gvpr_bin = which("gvpr") result = subprocess.check_output( [gvpr_bin, program], stdin=subprocess.DEVNULL, universal_newlines=True ) # confirm we got the expected output assert result == f"begin {branch}\nend {branch}\n", "incorrect GVPR switch behavior" @pytest.mark.parametrize( "statement,expected", ( ('printf("%d", 5)', "5"), ('printf("%d", 0)', "0"), ('printf("%.0d", 0)', ""), ('printf("%.0d", 1)', "1"), ('printf("%.d", 2)', "2"), ('printf("%d", -1)', "-1"), ('printf("%.3d", 5)', "005"), ('printf("%.3d", -5)', "-005"), ('printf("%5.3d", 5)', " 005"), ('printf("%-5.3d", -5)', "-005 "), ('printf("%-d", 5)', "5"), ('printf("%-+d", 5)', "+5"), ('printf("%+-d", 5)', "+5"), ('printf("%+d", -5)', "-5"), ('printf("% d", 5)', " 5"), ('printf("% .0d", 0)', " "), ('printf("%03d", 5)', "005"), ('printf("%03d", -5)', "-05"), ('printf("% +d", 5)', "+5"), ('printf("%-03d", -5)', "-5 "), ('printf("%o", 5)', "5"), ('printf("%o", 8)', "10"), ('printf("%o", 0)', "0"), ('printf("%.0o", 0)', ""), ('printf("%.0o", 1)', "1"), ('printf("%.3o", 5)', "005"), ('printf("%.3o", 8)', "010"), ('printf("%5.3o", 5)', " 005"), ('printf("%u", 5)', "5"), ('printf("%u", 0)', "0"), ('printf("%.0u", 0)', ""), ('printf("%.0u", 1)', "1"), ('printf("%.3u", 5)', "005"), ('printf("%5.3u", 5)', " 005"), ('printf("%u", 5)', "5"), ('printf("%u", 0)', "0"), ('printf("%.0u", 0)', ""), ('printf("%.0u", 1)', "1"), ('printf("%.3u", 5)', "005"), ('printf("%5.3u", 5)', " 005"), ('printf("%-x", 5)', "5"), ('printf("%03x", 5)', "005"), ('printf("%-x", 5)', "5"), ('printf("%03x", 5)', "005"), ('printf("%-X", 5)', "5"), ('printf("%03X", 5)', "005"), ('printf("%.2s", "abc")', "ab"), ('printf("%.6s", "abc")', "abc"), ('printf("%5s", "abc")', " abc"), ('printf("%-5s", "abc")', "abc "), ('printf("%5.2s", "abc")', " ab"), ('printf("%%")', "%"), ), ) @pytest.mark.skipif(which("gvpr") is None, reason="gvpr not available") def test_gvpr_printf(statement: str, expected: str): """ check various behaviors of `printf` in a GVPR program """ # a program that performs the given `printf` program = f"BEGIN {{ {statement}; }}" # run this through GVPR with no input graph gvpr_bin = which("gvpr") result = subprocess.check_output( [gvpr_bin, program], stdin=subprocess.DEVNULL, universal_newlines=True ) # confirm we got the expected output assert result == expected, "incorrect GVPR printf behavior" usage_info = """\ Usage: dot [-Vv?] [-(GNE)name=val] [-(KTlso)] (additional options for neato) [-x] [-n] (additional options for fdp) [-L(gO)] [-L(nUCT)] (additional options for config) [-cv] -V - Print version and exit -v - Enable verbose mode -Gname=val - Set graph attribute 'name' to 'val' -Nname=val - Set node attribute 'name' to 'val' -Ename=val - Set edge attribute 'name' to 'val' -Tv - Set output format to 'v' -Kv - Set layout engine to 'v' (overrides default based on command name) -lv - Use external library 'v' -ofile - Write output to 'file' -O - Automatically generate an output filename based on the input filename with a .'format' appended. (Causes all -ofile options to be ignored.) -P - Internally generate a graph of the current plugins. -q[l] - Set level of message suppression (=1) -s[v] - Scale input by 'v' (=72) -y - Invert y coordinate in output -n[v] - No layout mode 'v' (=1) -x - Reduce graph -Lg - Don't use grid -LO - Use old attractive force -Ln - Set number of iterations to i -LU - Set unscaled factor to i -LC - Set overlap expansion factor to v -LT[*] - Set temperature (temperature factor) to v -c - Configure plugins (Writes $prefix/lib/graphviz/config with available plugin information. Needs write privilege.) -? - Print usage and exit """ def test_dot_questionmarkV(): """ test the output from two short options combined """ out = subprocess.check_output( ["dot", "-?V"], universal_newlines=True, ) assert out == usage_info, "unexpected usage info" def test_dot_randomV(): """ test the output from a malformed command """ expected = f"Error: dot: option -r unrecognized\n\n{usage_info}" proc = subprocess.run( ["dot", "-randomV"], stderr=subprocess.PIPE, universal_newlines=True, check=False, ) assert proc.returncode != 0, "malformed options were accepted" assert proc.stderr == expected, "unexpected usage info" @pytest.mark.skipif( os.getenv("build_system") == "msbuild", reason="https://gitlab.com/graphviz/graphviz/-/issues/1777", ) def test_dot_V(): """ test the output from `dot -V` """ proc = subprocess.run( ["dot", "-V"], stderr=subprocess.PIPE, universal_newlines=True, check=True ) c_src = (Path(__file__).parent / "get-package-version.c").resolve() assert c_src.exists(), "missing test case" package_version, _ = run_c(c_src) assert proc.stderr.startswith( f"dot - graphviz version {package_version.strip()} (" ), "unexpected -V info" @pytest.mark.skipif( os.getenv("build_system") == "msbuild", reason="https://gitlab.com/graphviz/graphviz/-/issues/1777", ) def test_dot_Vquestionmark(): """ test the output from two short options combined """ proc = subprocess.run( ["dot", "-V?"], stderr=subprocess.PIPE, universal_newlines=True, check=True ) c_src = (Path(__file__).parent / "get-package-version.c").resolve() assert c_src.exists(), "missing test case" package_version, _ = run_c(c_src) assert proc.stderr.startswith( f"dot - graphviz version {package_version.strip()} (" ), "unexpected -V info" @pytest.mark.skipif( os.getenv("build_system") == "msbuild", reason="https://gitlab.com/graphviz/graphviz/-/issues/1777", ) def test_dot_Vrandom(): """ test the output from a short option mixed with long """ proc = subprocess.run( ["dot", "-Vrandom"], stderr=subprocess.PIPE, universal_newlines=True, check=True ) c_src = (Path(__file__).parent / "get-package-version.c").resolve() assert c_src.exists(), "missing test case" package_version, _ = run_c(c_src) assert proc.stderr.startswith( f"dot - graphviz version {package_version.strip()} (" ), "unexpected -V info" def test_pic_font_size(): """ font size in PIC output format should not be clamped down to 1 related to https://gitlab.com/graphviz/graphviz/-/issues/2487 """ # run a basic graph through PIC generation src = "graph { a -- b; }" pic = dot("pic", source=src) # confirm we got a non-1 font size m = re.search(r"^\.ps (\d+)", pic, flags=re.MULTILINE) assert int(m.group(1)) > 1, "font size clamped down to 1" @pytest.mark.skipif(which("mm2gv") is None, reason="mm2gv not available") def test_mm_banner_overflow(tmp_path: Path): """mm2gv should be robust against files with a corrupted banner""" # construct a file with a corrupted banner > MM_MAX_TOKEN_LENGTH and < MM_MAX_LINE_LENGTH mm = tmp_path / "matrix.mm" mm.write_text(f"%{'a' * 10000}", encoding="utf-8") # run this through mm2gv ret = subprocess.call(["mm2gv", "-o", os.devnull, mm]) assert ret in (0, 1), "mm2gv crashed when processing malformed input" assert ret == 1, "mm2gv did not reject malformed input" def test_control_characters_in_error(): """ malformed input should not result in misleading control data making it to the output terminal unfiltered """ # Run something through Graphviz that will trigger an error where the error message # will contain a color control sequence. This could be used to disrupt the user’s # terminal in confusing ways. src = 'graph { a[image="\033[31mfoo"]; }' ret = subprocess.run( ["dot", "-Tsvg", "-o", os.devnull], input=src, stderr=subprocess.PIPE, universal_newlines=True, ) assert "\033" not in ret.stderr, "control character appears in error message" # Now try something more malicious. Use the backspace character to display a different # filename in the error message to what was referenced. src = 'graph { a[image="foo.svg\010\010\010png"]; }' ret = subprocess.run( ["dot", "-Tsvg", "-o", os.devnull], input=src, stderr=subprocess.PIPE, universal_newlines=True, ) assert "\010" not in ret.stderr, "control character appears in error message" def test_fig_max_colors(): """ using a large number of colors should not crash the FIG renderer """ # contruct a graph that uses well over 256 colors buf = io.StringIO() buf.write("graph {\n") for red in range(256): for green in range(10): buf.write(f' n_{red}_{green}[color="#{red:02x}{green:02x}00"];\n') buf.write("}\n") # render this using the FIG renderer dot("fig", source=buf.getvalue()) @pytest.mark.skipif(which("gvpr") is None, reason="gvpr not available") def test_gvpr_s2f(): """ casting a string to floating point in GVPR should work """ # a GVPR program that casts a string to floating point and prints the result program = 'BEGIN { float x = (float)"1.5"; printf("%0.1f\\n", x); }' # run this through GVPR with no input graph gvpr_bin = which("gvpr") result = subprocess.check_output( [gvpr_bin, program], stdin=subprocess.DEVNULL, universal_newlines=True ) # confirm we got the expected output assert result == "1.5\n", "incorrect GVPR float cast behavior" def test_changelog(): """ sanity checks on ../CHANGELOG.md """ changelog = Path(__file__).parent / "../CHANGELOG.md" assert changelog.exists(), "CHANGELOG.md missing" with open(changelog, "rt", encoding="utf-8") as f: for lineno, line in enumerate(f, 1): ignore_h2 = False # an exception for an old heading if line == "## [2.42.3] and earlier\n": ignore_h2 = True # an exception for unreleased versions if line.startswith("## ") and "Unreleased" in line: ignore_h2 = True if (m := re.match("##(?P[^#].*)$", line)) and not ignore_h2: expected_format = r" \[\d+\.\d+\.\d+\] [\-–] \d{4}-\d{2}-\d{2}$" assert re.match(expected_format, m.group("remainder")), ( f"CHANGELOG.md:{lineno}: second-level heading did not match " f'regex r"{expected_format}": {line}' ) if m := re.match("###(?P.*)$", line): assert m.group("remainder") in ( " Added", " Changed", " Fixed", " Removed", ), f"CHANGELOG.md:{lineno}: unexpected third-level heading: {line}" if m := re.match( r"\[(?P\d+\.\d+\.\d+)\]:(?P.*)$", line ): prefix = " https://gitlab.com/graphviz/graphviz/compare/" assert m.group("remainder").startswith( prefix ), f"CHANGELOG.md:{lineno}: unexpected finalized history link: {line}" remainder = m.group("remainder")[len(prefix) :] assert m.group("remainder").endswith( f'...{m.group("version")}' ), f"CHANGELOG.md:{lineno}: history link is for wrong version: {line}" version_range = re.match( r"(?P\d+)\.(?P\d+)\.(?P\d+)" r"\.\.\." r"(?P\d+)\.(?P\d+)\.(?P\d+)$", remainder, ) assert ( version_range ), f"CHANGELOG.md:{lineno}: unexpected finalized history link: {line}" start = tuple( int(version_range.group(v)) for v in ("start_major", "start_minor", "start_patch") ) end = tuple( int(version_range.group(v)) for v in ("end_major", "end_minor", "end_patch") ) assert ( start < end ), f"CHANGELOG.md:{lineno}: invalid version range: {line}" def test_agxbuf_print_nul(): """ `agxbprint` should not account for nor append a NUL byte """ # find co-located test source c_src = (Path(__file__).parent / "agxbuf-print-nul.c").resolve() assert c_src.exists(), "missing test case" lib = Path(__file__).parents[1] / "lib" if platform.system() == "Windows" and not is_mingw(): cflags = [f"/I{lib}"] else: # GNU99 needed for `strndup` cflags = ["-std=gnu99", f"-I{lib}"] run_c(c_src, cflags=cflags) def test_agxbuf_use_implicit_nul(): """ `agxbuf` should be able to use its entire memory as an inline string """ # find co-located test source c_src = (Path(__file__).parent / "agxbuf-use-implicit-nul.c").resolve() assert c_src.exists(), "missing test case" lib = Path(__file__).parents[1] / "lib" if platform.system() == "Windows" and not is_mingw(): cflags = [f"/I{lib}"] else: # GNU99 needed for `strndup` cflags = ["-std=gnu99", f"-I{lib}"] run_c(c_src, cflags=cflags) @pytest.mark.skipif(which("edgepaint") is None, reason="edgepaint not available") def test_edgepaint_error_message(): """ when failing to open its output, edgepaint should not dereference a null pointer """ # try to open a non-existent file edgepaint = which("edgepaint") proc = subprocess.run( [edgepaint, "-o", "/a/nonexistent/path"], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, universal_newlines=True, ) # edgepaint should name itself in the error message, not “(null)” assert re.search( r"\bedgepaint\b", proc.stderr ), "edgepaint does not know its own name"