test_rtest.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. #!/usr/bin/env python3
  2. """
  3. Older Graphviz regression test suite that has been encapsulated
  4. TODO:
  5. Report differences with shared version and with new output.
  6. """
  7. import hashlib
  8. import io
  9. import platform
  10. import re
  11. import shutil
  12. import subprocess
  13. import sys
  14. from pathlib import Path
  15. from typing import List
  16. import pytest
  17. # Test specifications
  18. GRAPHDIR = Path(__file__).parent / "graphs"
  19. # Directory of input graphs and data
  20. OUTDIR = Path("ndata") # Directory for test output
  21. OUTHTML = Path("nhtml") # Directory for html test report
  22. class Case:
  23. """
  24. test case struct
  25. """
  26. def __init__(
  27. self,
  28. name: str,
  29. input: Path,
  30. algorithm: str,
  31. format: str,
  32. flags: List[str],
  33. ): # pylint: disable=too-many-arguments
  34. self.name = name
  35. self.input = input
  36. self.algorithm = algorithm
  37. self.format = format
  38. self.flags = flags[:]
  39. TESTS: List[Case] = [
  40. Case("shapes", Path("shapes.gv"), "dot", "gv", []),
  41. Case("shapes", Path("shapes.gv"), "dot", "ps", []),
  42. Case("crazy", Path("crazy.gv"), "dot", "png", []),
  43. Case("crazy", Path("crazy.gv"), "dot", "ps", []),
  44. Case("arrows", Path("arrows.gv"), "dot", "gv", []),
  45. Case("arrows", Path("arrows.gv"), "dot", "ps", []),
  46. Case("arrowsize", Path("arrowsize.gv"), "dot", "png", []),
  47. Case("center", Path("center.gv"), "dot", "ps", []),
  48. Case("center", Path("center.gv"), "dot", "png", ["-Gmargin=1"]),
  49. # color encodings
  50. # multiple edge colors
  51. Case("color", Path("color.gv"), "dot", "png", []),
  52. Case("color", Path("color.gv"), "dot", "png", ["-Gbgcolor=lightblue"]),
  53. Case("decorate", Path("decorate.gv"), "dot", "png", []),
  54. Case("record", Path("record.gv"), "dot", "gv", []),
  55. Case("record", Path("record.gv"), "dot", "ps", []),
  56. Case("html", Path("html.gv"), "dot", "gv", []),
  57. Case("html", Path("html.gv"), "dot", "ps", []),
  58. Case("html2", Path("html2.gv"), "dot", "gv", []),
  59. Case("html2", Path("html2.gv"), "dot", "ps", []),
  60. Case("html2", Path("html2.gv"), "dot", "svg", []),
  61. Case("pslib", Path("pslib.gv"), "dot", "ps", ["-lgraphs/sdl.ps"]),
  62. Case("user_shapes", Path("user_shapes.gv"), "dot", "ps", []),
  63. # dot png - doesn't work: Warning: No loadimage plugin for "gif:cairo"
  64. Case("user_shapes", Path("user_shapes.gv"), "dot", "png:gd", []),
  65. # bug - the epsf version has problems
  66. Case(
  67. "ps_user_shapes",
  68. Path("ps_user_shapes.gv"),
  69. "dot",
  70. "ps",
  71. ["-Nshapefile=graphs/dice.ps"],
  72. ),
  73. Case("colorscheme", Path("colorscheme.gv"), "dot", "ps", []),
  74. Case("colorscheme", Path("colorscheme.gv"), "dot", "png", []),
  75. Case("compound", Path("compound.gv"), "dot", "gv", []),
  76. Case("dir", Path("dir.gv"), "dot", "ps", []),
  77. Case("clusters", Path("clusters.gv"), "dot", "ps", []),
  78. Case("clusters", Path("clusters.gv"), "dot", "png", []),
  79. Case(
  80. "clustlabel",
  81. Path("clustlabel.gv"),
  82. "dot",
  83. "ps",
  84. ["-Glabelloc=t", "-Glabeljust=r"],
  85. ),
  86. Case(
  87. "clustlabel",
  88. Path("clustlabel.gv"),
  89. "dot",
  90. "ps",
  91. ["-Glabelloc=b", "-Glabeljust=r"],
  92. ),
  93. Case(
  94. "clustlabel",
  95. Path("clustlabel.gv"),
  96. "dot",
  97. "ps",
  98. ["-Glabelloc=t", "-Glabeljust=l"],
  99. ),
  100. Case(
  101. "clustlabel",
  102. Path("clustlabel.gv"),
  103. "dot",
  104. "ps",
  105. ["-Glabelloc=b", "-Glabeljust=l"],
  106. ),
  107. Case(
  108. "clustlabel",
  109. Path("clustlabel.gv"),
  110. "dot",
  111. "ps",
  112. ["-Glabelloc=t", "-Glabeljust=c"],
  113. ),
  114. Case(
  115. "clustlabel",
  116. Path("clustlabel.gv"),
  117. "dot",
  118. "ps",
  119. ["-Glabelloc=b", "-Glabeljust=c"],
  120. ),
  121. Case("clustlabel", Path("clustlabel.gv"), "dot", "ps", ["-Glabelloc=t"]),
  122. Case("clustlabel", Path("clustlabel.gv"), "dot", "ps", ["-Glabelloc=b"]),
  123. Case(
  124. "rootlabel",
  125. Path("rootlabel.gv"),
  126. "dot",
  127. "ps",
  128. ["-Glabelloc=t", "-Glabeljust=r"],
  129. ),
  130. Case(
  131. "rootlabel",
  132. Path("rootlabel.gv"),
  133. "dot",
  134. "ps",
  135. ["-Glabelloc=b", "-Glabeljust=r"],
  136. ),
  137. Case(
  138. "rootlabel",
  139. Path("rootlabel.gv"),
  140. "dot",
  141. "ps",
  142. ["-Glabelloc=t", "-Glabeljust=l"],
  143. ),
  144. Case(
  145. "rootlabel",
  146. Path("rootlabel.gv"),
  147. "dot",
  148. "ps",
  149. ["-Glabelloc=b", "-Glabeljust=l"],
  150. ),
  151. Case(
  152. "rootlabel",
  153. Path("rootlabel.gv"),
  154. "dot",
  155. "ps",
  156. ["-Glabelloc=t", "-Glabeljust=c"],
  157. ),
  158. Case(
  159. "rootlabel",
  160. Path("rootlabel.gv"),
  161. "dot",
  162. "ps",
  163. ["-Glabelloc=b", "-Glabeljust=c"],
  164. ),
  165. Case("rootlabel", Path("rootlabel.gv"), "dot", "ps", ["-Glabelloc=t"]),
  166. Case("rootlabel", Path("rootlabel.gv"), "dot", "ps", ["-Glabelloc=b"]),
  167. Case("layers", Path("layers.gv"), "dot", "ps", []),
  168. # check mode=hier
  169. Case("mode", Path("mode.gv"), "neato", "ps", ["-Gmode=KK"]),
  170. Case("mode", Path("mode.gv"), "neato", "ps", ["-Gmode=hier"]),
  171. Case("mode", Path("mode.gv"), "neato", "ps", ["-Gmode=hier", "-Glevelsgap=1"]),
  172. Case("model", Path("mode.gv"), "neato", "ps", ["-Gmodel=circuit"]),
  173. Case(
  174. "model", Path("mode.gv"), "neato", "ps", ["-Goverlap=false", "-Gmodel=subset"]
  175. ),
  176. # cairo versions have problems
  177. Case("nojustify", Path("nojustify.gv"), "dot", "png", []),
  178. Case("nojustify", Path("nojustify.gv"), "dot", "png:gd", []),
  179. Case("nojustify", Path("nojustify.gv"), "dot", "ps", []),
  180. Case("nojustify", Path("nojustify.gv"), "dot", "ps:cairo", []),
  181. # bug
  182. Case("ordering", Path("ordering.gv"), "dot", "gv", ["-Gordering=in"]),
  183. Case("ordering", Path("ordering.gv"), "dot", "gv", ["-Gordering=out"]),
  184. Case("overlap", Path("overlap.gv"), "neato", "gv", ["-Goverlap=false"]),
  185. Case("overlap", Path("overlap.gv"), "neato", "gv", ["-Goverlap=scale"]),
  186. Case("pack", Path("pack.gv"), "neato", "gv", []),
  187. Case("pack", Path("pack.gv"), "neato", "gv", ["-Gpack=20"]),
  188. Case("pack", Path("pack.gv"), "neato", "gv", ["-Gpackmode=graph"]),
  189. Case("page", Path("mode.gv"), "neato", "ps", ["-Gpage=8.5,11"]),
  190. Case("page", Path("mode.gv"), "neato", "ps", ["-Gpage=8.5,11", "-Gpagedir=TL"]),
  191. Case("page", Path("mode.gv"), "neato", "ps", ["-Gpage=8.5,11", "-Gpagedir=TR"]),
  192. # pencolor, fontcolor, fillcolor
  193. Case("colors", Path("colors.gv"), "dot", "ps", []),
  194. Case("polypoly", Path("polypoly.gv"), "dot", "ps", []),
  195. Case("polypoly", Path("polypoly.gv"), "dot", "png", []),
  196. Case("ports", Path("ports.gv"), "dot", "gv", []),
  197. Case("rotate", Path("crazy.gv"), "dot", "png", ["-Glandscape"]),
  198. Case("rotate", Path("crazy.gv"), "dot", "ps", ["-Glandscape"]),
  199. Case("rotate", Path("crazy.gv"), "dot", "png", ["-Grotate=90"]),
  200. Case("rotate", Path("crazy.gv"), "dot", "ps", ["-Grotate=90"]),
  201. Case("rankdir", Path("crazy.gv"), "dot", "gv", ["-Grankdir=LR"]),
  202. Case("rankdir", Path("crazy.gv"), "dot", "gv", ["-Grankdir=BT"]),
  203. Case("rankdir", Path("crazy.gv"), "dot", "gv", ["-Grankdir=RL"]),
  204. Case("url", Path("url.gv"), "dot", "ps2", []),
  205. Case("url", Path("url.gv"), "dot", "svg", ["-Gstylesheet=stylesheet"]),
  206. Case("url", Path("url.gv"), "dot", "imap", []),
  207. Case("url", Path("url.gv"), "dot", "cmapx", []),
  208. Case("url", Path("url.gv"), "dot", "imap_np", []),
  209. Case("url", Path("url.gv"), "dot", "cmapx_np", []),
  210. Case(
  211. "viewport", Path("viewport.gv"), "neato", "png", ["-Gviewport=300,300", "-n2"]
  212. ),
  213. Case("viewport", Path("viewport.gv"), "neato", "ps", ["-Gviewport=300,300", "-n2"]),
  214. Case(
  215. "viewport",
  216. Path("viewport.gv"),
  217. "neato",
  218. "png",
  219. ["-Gviewport=300,300,1,200,620", "-n2"],
  220. ),
  221. Case(
  222. "viewport",
  223. Path("viewport.gv"),
  224. "neato",
  225. "ps",
  226. ["-Gviewport=300,300,1,200,620", "-n2"],
  227. ),
  228. Case(
  229. "viewport",
  230. Path("viewport.gv"),
  231. "neato",
  232. "png",
  233. ["-Gviewport=300,300,2,200,620", "-n2"],
  234. ),
  235. Case(
  236. "viewport",
  237. Path("viewport.gv"),
  238. "neato",
  239. "ps",
  240. ["-Gviewport=300,300,2,200,620", "-n2"],
  241. ),
  242. Case("rowcolsep", Path("rowcolsep.gv"), "dot", "gv", ["-Gnodesep=0.5"]),
  243. Case("rowcolsep", Path("rowcolsep.gv"), "dot", "gv", ["-Granksep=1.5"]),
  244. Case("size", Path("mode.gv"), "neato", "ps", ["-Gsize=5,5"]),
  245. Case("size", Path("mode.gv"), "neato", "png", ["-Gsize=5,5"]),
  246. # size with !
  247. Case("size_ex", Path("root.gv"), "dot", "ps", ["-Gsize=6,6!"]),
  248. Case("size_ex", Path("root.gv"), "dot", "png", ["-Gsize=6,6!"]),
  249. Case("dotsplines", Path("size.gv"), "dot", "gv", ["-Gsplines=line"]),
  250. Case("dotsplines", Path("size.gv"), "dot", "gv", ["-Gsplines=polyline"]),
  251. Case(
  252. "neatosplines",
  253. Path("overlap.gv"),
  254. "neato",
  255. "gv",
  256. ["-Goverlap=false", "-Gsplines"],
  257. ),
  258. Case(
  259. "neatosplines",
  260. Path("overlap.gv"),
  261. "neato",
  262. "gv",
  263. ["-Goverlap=false", "-Gsplines=polyline"],
  264. ),
  265. Case("style", Path("style.gv"), "dot", "ps", []),
  266. Case("style", Path("style.gv"), "dot", "png", []),
  267. # edge clipping
  268. Case("edgeclip", Path("edgeclip.gv"), "dot", "gv", []),
  269. # edge weight
  270. Case("weight", Path("weight.gv"), "dot", "gv", []),
  271. Case("root", Path("root.gv"), "twopi", "gv", []),
  272. Case("cairo", Path("cairo.gv"), "dot", "ps:cairo", []),
  273. Case("cairo", Path("cairo.gv"), "dot", "png:cairo", []),
  274. Case("cairo", Path("cairo.gv"), "dot", "svg:cairo", []),
  275. Case("flatedge", Path("flatedge.gv"), "dot", "gv", []),
  276. Case("nestedclust", Path("nestedclust"), "dot", "gv", []),
  277. Case("rd_rules", Path("rd_rules.gv"), "dot", "png", []),
  278. Case("sq_rules", Path("sq_rules.gv"), "dot", "png", []),
  279. Case("fdp_clus", Path("fdp.gv"), "fdp", "png", []),
  280. Case("japanese", Path("japanese.gv"), "dot", "png", []),
  281. Case("russian", Path("russian.gv"), "dot", "png", []),
  282. Case("AvantGarde", Path("AvantGarde.gv"), "dot", "png", []),
  283. Case("AvantGarde", Path("AvantGarde.gv"), "dot", "ps", []),
  284. Case("Bookman", Path("Bookman.gv"), "dot", "png", []),
  285. Case("Bookman", Path("Bookman.gv"), "dot", "ps", []),
  286. Case("Helvetica", Path("Helvetica.gv"), "dot", "png", []),
  287. Case("Helvetica", Path("Helvetica.gv"), "dot", "ps", []),
  288. Case("NewCenturySchlbk", Path("NewCenturySchlbk.gv"), "dot", "png", []),
  289. Case("NewCenturySchlbk", Path("NewCenturySchlbk.gv"), "dot", "ps", []),
  290. Case("Palatino", Path("Palatino.gv"), "dot", "png", []),
  291. Case("Palatino", Path("Palatino.gv"), "dot", "ps", []),
  292. Case("Times", Path("Times.gv"), "dot", "png", []),
  293. Case("Times", Path("Times.gv"), "dot", "ps", []),
  294. Case("ZapfChancery", Path("ZapfChancery.gv"), "dot", "png", []),
  295. Case("ZapfChancery", Path("ZapfChancery.gv"), "dot", "ps", []),
  296. Case("ZapfDingbats", Path("ZapfDingbats.gv"), "dot", "png", []),
  297. Case("ZapfDingbats", Path("ZapfDingbats.gv"), "dot", "ps", []),
  298. Case("xlabels", Path("xlabels.gv"), "dot", "png", []),
  299. Case("xlabels", Path("xlabels.gv"), "neato", "png", []),
  300. Case("sides", Path("sides.gv"), "dot", "ps", []),
  301. ]
  302. def doDiff(OUTFILE, testname, fmt):
  303. """
  304. Compare old and new output and report if different.
  305. Args: testname index fmt
  306. """
  307. FILE1 = OUTDIR / OUTFILE
  308. FILE2 = REFDIR / OUTFILE
  309. F = fmt.split(":")[0]
  310. if F in ["ps", "ps2"]:
  311. with open(FILE1, "rt", encoding="latin-1") as src:
  312. dst1 = io.StringIO()
  313. done_setup = False
  314. for line in src:
  315. if done_setup:
  316. dst1.write(line)
  317. else:
  318. done_setup = re.match(r"%%End.*Setup", line) is not None
  319. with open(FILE2, "rt", encoding="latin-1") as src:
  320. dst2 = io.StringIO()
  321. done_setup = False
  322. for line in src:
  323. if done_setup:
  324. dst2.write(line)
  325. else:
  326. done_setup = re.match(r"%%End.*Setup", line) is not None
  327. returncode = 0 if dst1.getvalue() == dst2.getvalue() else -1
  328. elif F == "svg":
  329. with open(FILE1, "rt", encoding="utf-8") as f:
  330. a = re.sub(r"^<!--.*-->$", "", f.read(), flags=re.MULTILINE)
  331. with open(FILE2, "rt", encoding="utf-8") as f:
  332. b = re.sub(r"^<!--.*-->$", "", f.read(), flags=re.MULTILINE)
  333. returncode = 0 if a.strip() == b.strip() else -1
  334. elif F == "png":
  335. OUTHTML.mkdir(exist_ok=True)
  336. returncode = subprocess.call(
  337. ["diffimg", FILE1, FILE2, OUTHTML / f"dif_{OUTFILE}"],
  338. )
  339. if returncode != 0:
  340. with open(OUTHTML / "index.html", "at", encoding="utf-8") as fd:
  341. fd.write("<p>\n")
  342. shutil.copyfile(FILE2, OUTHTML / f"old_{OUTFILE}")
  343. fd.write(f'<img src="old_{OUTFILE}" width="192" height="192">\n')
  344. shutil.copyfile(FILE1, OUTHTML / f"new_{OUTFILE}")
  345. fd.write(f'<img src="new_{OUTFILE}" width="192" height="192">\n')
  346. fd.write(f'<img src="dif_{OUTFILE}" width="192" height="192">\n')
  347. else:
  348. (OUTHTML / f"dif_{OUTFILE}").unlink()
  349. else:
  350. with open(FILE2, "rt", encoding="utf-8") as a:
  351. with open(FILE1, "rt", encoding="utf-8") as b:
  352. returncode = 0 if a.read().strip() == b.read().strip() else -1
  353. if returncode != 0:
  354. # FIXME: pytest.fail when all tests pass on all platforms
  355. print(f"Test {testname}: == Failed == {OUTFILE}", file=sys.stderr)
  356. def genOutname(name, alg, fmt, flags: List[str]):
  357. """
  358. Generate output file name given 4 parameters.
  359. testname layout format flags
  360. If format ends in :*, remove this, change the colons to underscores,
  361. and append to basename
  362. """
  363. fmt_split = fmt.split(":")
  364. if len(fmt_split) >= 2:
  365. F = fmt_split[0]
  366. XFMT = f'_{"_".join(fmt_split[1:])}'
  367. else:
  368. F = fmt
  369. XFMT = ""
  370. suffix = hashlib.sha256()
  371. for flag in flags:
  372. suffix.update(f"{flag} ".encode("utf-8"))
  373. OUTFILE = f"{name}_{alg}{XFMT}_{suffix.hexdigest()}.{F}"
  374. return OUTFILE
  375. @pytest.mark.parametrize(
  376. "name,input,algorithm,format,flags",
  377. ((c.name, c.input, c.algorithm, c.format, c.flags) for c in TESTS),
  378. )
  379. def test_graph(name: str, input: Path, algorithm: str, format: str, flags: List[str]):
  380. """
  381. Run a single test.
  382. """
  383. if input.suffix != ".gv":
  384. pytest.skip(f"Unknown graph spec, test {name} - ignoring")
  385. INFILE = GRAPHDIR / input
  386. OUTFILE = genOutname(name, algorithm, format, flags)
  387. OUTDIR.mkdir(exist_ok=True)
  388. OUTPATH = OUTDIR / OUTFILE
  389. testcmd = ["dot", f"-K{algorithm}", f"-T{format}"] + flags + ["-o", OUTPATH, INFILE]
  390. # FIXME: Remove when https://gitlab.com/graphviz/graphviz/-/issues/1790 is
  391. # fixed
  392. if platform.system() == "Windows" and name == "ps_user_shapes":
  393. pytest.skip(
  394. f"Skipping test {name}: using PostScript shapefile "
  395. "because it fails with Windows builds (#1790)"
  396. )
  397. RVAL = subprocess.call(testcmd, stderr=subprocess.STDOUT)
  398. if RVAL != 0 or not OUTPATH.exists():
  399. pytest.fail(
  400. f'Test {name}: == Layout failed ==\n {" ".join(str(a) for a in testcmd)}'
  401. )
  402. elif (REFDIR / OUTFILE).exists():
  403. doDiff(OUTFILE, name, format)
  404. else:
  405. sys.stderr.write(
  406. f"Test {name}: == No file {REFDIR}/{OUTFILE} for comparison ==\n"
  407. )
  408. # Set REFDIR
  409. if platform.system() == "Linux":
  410. REFDIR = Path("linux.x86")
  411. elif platform.system() == "Darwin":
  412. REFDIR = Path("macosx")
  413. elif platform.system() == "Windows":
  414. REFDIR = Path("nshare")
  415. else:
  416. print(f'Unrecognized system "{platform.system()}"', file=sys.stderr)
  417. REFDIR = Path("nshare")