test_regression.py 172 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629
  1. """
  2. Graphviz regression tests
  3. The test cases in this file relate to previously observed bugs. A failure of one
  4. of these indicates that a past bug has been reintroduced.
  5. """
  6. import dataclasses
  7. import io
  8. import json
  9. import math
  10. import os
  11. import platform
  12. import re
  13. import shlex
  14. import shutil
  15. import signal
  16. import stat
  17. import statistics
  18. import subprocess
  19. import sys
  20. import tempfile
  21. import textwrap
  22. import time
  23. import xml.etree.ElementTree as ET
  24. from pathlib import Path
  25. from typing import Iterator, List, Set
  26. import pexpect
  27. import pytest
  28. sys.path.append(os.path.dirname(__file__))
  29. from gvtest import ( # pylint: disable=wrong-import-position
  30. dot,
  31. gvpr,
  32. is_asan_instrumented,
  33. is_autotools,
  34. is_cmake,
  35. is_macos,
  36. is_mingw,
  37. is_rocky,
  38. is_rocky_8,
  39. is_static_build,
  40. is_ubuntu_2004,
  41. remove_asan_summary,
  42. remove_xtype_warnings,
  43. run_c,
  44. which,
  45. )
  46. def is_ndebug_defined() -> bool:
  47. """
  48. are assertions disabled in the Graphviz build under test?
  49. """
  50. # the Windows release builds set NDEBUG
  51. if os.environ.get("configuration") == "Release":
  52. return True
  53. return False
  54. @pytest.mark.xfail(
  55. platform.system() == "Windows", reason="#56", strict=not is_ndebug_defined()
  56. ) # FIXME
  57. def test_14():
  58. """
  59. using ortho and twopi in combination should not cause an assertion failure
  60. https://gitlab.com/graphviz/graphviz/-/issues/14
  61. """
  62. # locate our associated test case in this directory
  63. input = Path(__file__).parent / "14.dot"
  64. assert input.exists(), "unexpectedly missing test case"
  65. # process it with Graphviz
  66. dot("svg", input)
  67. @pytest.mark.skipif(which("neato") is None, reason="neato not available")
  68. def test_42():
  69. """
  70. check for a former crash in neatogen
  71. https://gitlab.com/graphviz/graphviz/-/issues/42
  72. """
  73. # locate our associated test case in this directory
  74. input = Path(__file__).parent / "42.dot"
  75. assert input.exists(), "unexpectedly missing test case"
  76. # process it with Graphviz
  77. subprocess.check_call(["neato", "-n2", "-Tpng", input], stdout=subprocess.DEVNULL)
  78. def test_56():
  79. """
  80. parsing a particular graph should not cause a Trapezoid-table overflow
  81. assertion failure
  82. https://gitlab.com/graphviz/graphviz/-/issues/56
  83. """
  84. # locate our associated test case in this directory
  85. input = Path(__file__).parent / "56.dot"
  86. assert input.exists(), "unexpectedly missing test case"
  87. # process it with Graphviz
  88. dot("svg", input)
  89. def test_121():
  90. """
  91. test a graph that previously caused an assertion failure in `merge_chain`
  92. https://gitlab.com/graphviz/graphviz/-/issues/121
  93. """
  94. # locate our associated test case in this directory
  95. input = Path(__file__).parent / "121.dot"
  96. assert input.exists(), "unexpectedly missing test case"
  97. # process it with Graphviz
  98. dot("pdf", input)
  99. def test_131():
  100. """
  101. PIC back end should produce valid output
  102. https://gitlab.com/graphviz/graphviz/-/issues/131
  103. """
  104. # a basic graph
  105. src = "digraph { a -> b; c -> d; }"
  106. # ask Graphviz to process this to PIC
  107. pic = dot("pic", source=src)
  108. if which("gpic") is None:
  109. pytest.skip("GNU PIC not available")
  110. # ask GNU PIC to process the Graphviz output
  111. subprocess.run(
  112. ["gpic"],
  113. input=pic,
  114. stdout=subprocess.DEVNULL,
  115. check=True,
  116. universal_newlines=True,
  117. )
  118. @pytest.mark.parametrize(
  119. "testcase",
  120. (
  121. "144_no_ortho.dot",
  122. pytest.param(
  123. "144_ortho.dot",
  124. marks=pytest.mark.xfail(platform.system() == "Windows", reason="flaky"),
  125. ),
  126. ),
  127. ) # FIXME
  128. def test_144(testcase: str):
  129. """
  130. using ortho should not result in head/tail confusion
  131. https://gitlab.com/graphviz/graphviz/-/issues/144
  132. """
  133. # locate our associated test cases in this directory
  134. input = Path(__file__).parent / testcase
  135. assert input.exists(), "unexpectedly missing test case"
  136. # process the non-ortho one into JSON
  137. out = dot("json", input)
  138. data = json.loads(out)
  139. # find the nodes “A”, “B” and “C”
  140. A = [x for x in data["objects"] if x["name"] == "A"][0]
  141. B = [x for x in data["objects"] if x["name"] == "B"][0]
  142. C = [x for x in data["objects"] if x["name"] == "C"][0]
  143. # find the straight A→B and the angular A→C edges
  144. straight_edge = [
  145. x for x in data["edges"] if x["tail"] == A["_gvid"] and x["head"] == B["_gvid"]
  146. ][0]
  147. angular_edge = [
  148. x for x in data["edges"] if x["tail"] == A["_gvid"] and x["head"] == C["_gvid"]
  149. ][0]
  150. # the A→B edge should have been routed vertically down
  151. straight_points = straight_edge["_draw_"][1]["points"]
  152. xs = [x for x, _ in straight_points]
  153. ys = [y for _, y in straight_points]
  154. assert all(x == xs[0] for x in xs), "A->B not routed vertically"
  155. assert ys == sorted(ys, reverse=True), "A->B is not routed down"
  156. # determine Graphviz’ idea of head and tail ends
  157. straight_head_point = straight_edge["_hdraw_"][3]["points"][0]
  158. straight_tail_point = straight_edge["_tdraw_"][3]["points"][0]
  159. assert straight_head_point[1] < straight_tail_point[1], "A->B head/tail confusion"
  160. # the A→C edge should have been routed in zigzag down and right
  161. angular_points = angular_edge["_draw_"][1]["points"]
  162. xs = [x for x, _ in angular_points]
  163. ys = [y for _, y in angular_points]
  164. assert xs == sorted(xs), "A->B is not routed down"
  165. assert ys == sorted(ys, reverse=True), "A->B is not routed right"
  166. # determine Graphviz’ idea of head and tail ends
  167. angular_head_point = angular_edge["_hdraw_"][3]["points"][0]
  168. angular_tail_point = angular_edge["_tdraw_"][3]["points"][0]
  169. assert angular_head_point[0] > angular_tail_point[0], "A->C head/tail confusion"
  170. def test_146():
  171. """
  172. dot should respect an alpha channel value of 0 when writing SVG
  173. https://gitlab.com/graphviz/graphviz/-/issues/146
  174. """
  175. # a graph using white text but with 0 alpha
  176. source = (
  177. "graph {\n"
  178. ' n[style="filled", fontcolor="#FFFFFF00", label="hello world"];\n'
  179. "}"
  180. )
  181. # ask Graphviz to process this
  182. svg = dot("svg", source=source)
  183. # the SVG should be setting opacity
  184. opacity = re.search(r'\bfill-opacity="(\d+(\.\d+)?)"', svg)
  185. assert opacity is not None, "transparency not set for alpha=0 color"
  186. # it should be zeroed
  187. assert (
  188. float(opacity.group(1)) == 0
  189. ), "alpha=0 color set to something non-transparent"
  190. def test_165():
  191. """
  192. dot should be able to produce properly escaped xdot output
  193. https://gitlab.com/graphviz/graphviz/-/issues/165
  194. """
  195. # locate our associated test case in this directory
  196. input = Path(__file__).parent / "165.dot"
  197. assert input.exists(), "unexpectedly missing test case"
  198. # ask Graphviz to translate it to xdot
  199. output = dot("xdot", input)
  200. # find the line containing the _ldraw_ attribute
  201. ldraw = re.search(r"^\s*_ldraw_\s*=(?P<value>.*?)$", output, re.MULTILINE)
  202. assert ldraw is not None, "no _ldraw_ attribute in graph"
  203. # this should contain the label correctly escaped
  204. assert r"hello \\\" world" in ldraw.group("value"), "unexpected ldraw contents"
  205. def test_165_2():
  206. """
  207. variant of test_165() that checks a similar problem for edges
  208. https://gitlab.com/graphviz/graphviz/-/issues/165
  209. """
  210. # locate our associated test case in this directory
  211. input = Path(__file__).parent / "165_2.dot"
  212. assert input.exists(), "unexpectedly missing test case"
  213. # ask Graphviz to translate it to xdot
  214. output = dot("xdot", input)
  215. # find the lines containing _ldraw_ attributes
  216. ldraw = re.findall(r"^\s*_ldraw_\s*=(.*?)$", output, re.MULTILINE)
  217. assert ldraw is not None, "no _ldraw_ attributes in graph"
  218. # one of these should contain the label correctly escaped
  219. assert any(r"hello \\\" world" in l for l in ldraw), "unexpected ldraw contents"
  220. def test_165_3():
  221. """
  222. variant of test_165() that checks a similar problem for graph labels
  223. https://gitlab.com/graphviz/graphviz/-/issues/165
  224. """
  225. # locate our associated test case in this directory
  226. input = Path(__file__).parent / "165_3.dot"
  227. assert input.exists(), "unexpectedly missing test case"
  228. # ask Graphviz to translate it to xdot
  229. output = dot("xdot", input)
  230. # find the lines containing _ldraw_ attributes
  231. ldraw = re.findall(r"^\s*_ldraw_\s*=(.*?)$", output, re.MULTILINE)
  232. assert ldraw is not None, "no _ldraw_ attributes in graph"
  233. # one of these should contain the label correctly escaped
  234. assert any(r"hello \\\" world" in l for l in ldraw), "unexpected ldraw contents"
  235. def test_167():
  236. """
  237. using concentrate=true should not result in a segfault
  238. https://gitlab.com/graphviz/graphviz/-/issues/167
  239. """
  240. # locate our associated test case in this directory
  241. input = Path(__file__).parent / "167.dot"
  242. assert input.exists(), "unexpectedly missing test case"
  243. # process this with dot
  244. ret = subprocess.call(["dot", "-Tpdf", "-o", os.devnull, input])
  245. # Graphviz should not have caused a segfault
  246. assert ret != -signal.SIGSEGV, "Graphviz segfaulted"
  247. def test_191():
  248. """
  249. a comma-separated list without quotes should cause a hard error, not a warning
  250. https://gitlab.com/graphviz/graphviz/-/issues/191
  251. """
  252. source = (
  253. "graph {\n"
  254. ' "Trackable" [fontcolor=grey45,labelloc=c,fontname=Vera Sans, '
  255. "DejaVu Sans, Liberation Sans, Arial, Helvetica, sans,shape=box,"
  256. 'height=0.3,align=center,fontsize=10,style="setlinewidth(0.5)"];\n'
  257. "}"
  258. )
  259. with subprocess.Popen(
  260. ["dot", "-Tdot"],
  261. stdin=subprocess.PIPE,
  262. stderr=subprocess.PIPE,
  263. universal_newlines=True,
  264. ) as p:
  265. _, stderr = p.communicate(source)
  266. assert "syntax error" in stderr, "missing error message for unquoted list"
  267. assert p.returncode != 0, "syntax error was only a warning, not an error"
  268. def test_218():
  269. """
  270. out-of-spec font names should cause warnings in the core PS renderer
  271. https://gitlab.com/graphviz/graphviz/-/issues/218
  272. """
  273. # a graph using a font name with a space in it
  274. source = 'graph { a[fontname="PT Sans"]; }'
  275. # render it to PS
  276. warnings = subprocess.check_output(
  277. ["dot", "-Tps", "-o", os.devnull],
  278. stderr=subprocess.STDOUT,
  279. input=source,
  280. universal_newlines=True,
  281. )
  282. assert warnings.strip() != "", "no warning issued for a font name containing space"
  283. @pytest.mark.parametrize("test_case", ("241_0.dot", "241_1.dot"))
  284. @pytest.mark.xfail(
  285. strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/241"
  286. )
  287. def test_241(test_case: str):
  288. """
  289. processing a graph with a `splines=…` setting should not causes warnings
  290. https://gitlab.com/graphviz/graphviz/-/issues/241
  291. """
  292. # locate our associated test case in this directory
  293. input = Path(__file__).parent / test_case
  294. assert input.exists(), "unexpectedly missing test case"
  295. proc = subprocess.run(
  296. ["dot", "-Tsvg", "-o", os.devnull, input],
  297. stderr=subprocess.PIPE,
  298. universal_newlines=True,
  299. check=True,
  300. )
  301. assert (
  302. "Something is probably seriously wrong" not in proc.stderr
  303. ), "splines setting caused warnings"
  304. def test_358():
  305. """
  306. setting xdot version to 1.7 should enable font characteristics
  307. https://gitlab.com/graphviz/graphviz/-/issues/358
  308. """
  309. # locate our associated test case in this directory
  310. input = Path(__file__).parent / "358.dot"
  311. assert input.exists(), "unexpectedly missing test case"
  312. # process this with dot
  313. xdot = dot("xdot", input)
  314. for i in range(6):
  315. m = re.search(f"\\bt {1 << i}\\b", xdot)
  316. assert m is not None, f"font characteristic {1 << i} not enabled in xdot 1.7"
  317. @pytest.mark.parametrize("attribute", ("samehead", "sametail"))
  318. def test_452(attribute: str):
  319. """
  320. more than 5 unique `samehead` and `sametail` values should be usable
  321. https://gitlab.com/graphviz/graphviz/-/issues/452
  322. """
  323. # a graph using more than 5 of the same attribute with the same node on one
  324. # side of each edge
  325. graph = io.StringIO()
  326. graph.write("digraph {\n")
  327. for i in range(6):
  328. if attribute == "samehead":
  329. graph.write(f" m{i} -> n1")
  330. else:
  331. graph.write(f" n1 -> m{i}")
  332. graph.write(f'[{attribute}="foo{i}"];\n')
  333. graph.write("}\n")
  334. # process this with dot
  335. dot("svg", source=graph.getvalue())
  336. def test_510():
  337. """
  338. HSV colors should also support an alpha channel
  339. https://gitlab.com/graphviz/graphviz/-/issues/510
  340. """
  341. # a graph with a turquoise, partially transparent node
  342. source = 'digraph { a [color="0.482 0.714 0.878 0.5"]; }'
  343. # process this with dot
  344. svg = dot("svg", source=source)
  345. # see if we can locate an opacity adjustment
  346. m = re.search(r'\bstroke-opacity="(?P<opacity>\d*.\d*)"', svg)
  347. assert m is not None, "no stroke-opacity set; alpha channel ignored?"
  348. # it should be something in-between transparent and opaque
  349. opacity = float(m.group("opacity"))
  350. assert opacity > 0, "node set transparent; misinterpreted alpha channel?"
  351. assert opacity < 1, "node set opaque; misinterpreted alpha channel?"
  352. @pytest.mark.skipif(
  353. which("gv2gxl") is None or which("gxl2gv") is None, reason="GXL tools not available"
  354. )
  355. def test_517():
  356. """
  357. round tripping a graph through gv2gxl should not lose HTML labels
  358. https://gitlab.com/graphviz/graphviz/-/issues/517
  359. """
  360. # our test case input
  361. input = (
  362. "digraph{\n"
  363. " A[label=<<TABLE><TR><TD>(</TD><TD>A</TD><TD>)</TD></TR></TABLE>>]\n"
  364. ' B[label="<TABLE><TR><TD>(</TD><TD>B</TD><TD>)</TD></TR></TABLE>"]\n'
  365. "}"
  366. )
  367. # translate it to GXL
  368. gv2gxl = which("gv2gxl")
  369. gxl = subprocess.check_output([gv2gxl], input=input, universal_newlines=True)
  370. # translate this back to Dot
  371. gxl2gv = which("gxl2gv")
  372. dot_output = subprocess.check_output([gxl2gv], input=gxl, universal_newlines=True)
  373. # the result should have both expected labels somewhere
  374. assert (
  375. "label=<<TABLE><TR><TD>(</TD><TD>A</TD><TD>)</TD></TR></TABLE>>" in dot_output
  376. ), "HTML label missing"
  377. assert (
  378. 'label="<TABLE><TR><TD>(</TD><TD>B</TD><TD>)</TD></TR></TABLE>"' in dot_output
  379. ), "regular label missing"
  380. def test_793():
  381. """
  382. Graphviz should not crash when using VRML output with a non-writable current
  383. directory
  384. https://gitlab.com/graphviz/graphviz/-/issues/793
  385. """
  386. # create a non-writable directory
  387. with tempfile.TemporaryDirectory() as tmp:
  388. t = Path(tmp)
  389. t.chmod(t.stat().st_mode & ~stat.S_IWRITE)
  390. # ask the VRML back end to handle a simple graph, using the above as the
  391. # current working directory
  392. with subprocess.Popen(["dot", "-Tvrml", "-o", os.devnull], cwd=t) as p:
  393. p.communicate("digraph { a -> b; }")
  394. # Graphviz should not have caused a segfault
  395. assert p.returncode != -signal.SIGSEGV, "Graphviz segfaulted"
  396. def test_797():
  397. """
  398. “&;” should not be considered an XML escape sequence
  399. https://gitlab.com/graphviz/graphviz/-/issues/797
  400. """
  401. # some input containing the invalid escape
  402. input = 'digraph tree {\n"1" [shape="box", label="&amp; &amp;;", URL="a"];\n}'
  403. # process this with the client-side imagemap back end
  404. output = dot("cmapx", source=input)
  405. # the escape sequences should have been preserved
  406. assert "&amp; &amp;" in output
  407. @pytest.mark.xfail(
  408. strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/813"
  409. )
  410. def test_813():
  411. """
  412. nodes with multiple peripheries should still have a stable rendering
  413. https://gitlab.com/graphviz/graphviz/-/issues/813
  414. """
  415. # locate our associated test case in this directory
  416. input = Path(__file__).parent / "813.dot"
  417. assert input.exists(), "unexpectedly mising test case"
  418. # render this to dot
  419. reference = dot("dot", input)
  420. # run it through multiple passes
  421. iterated = reference
  422. for _ in range(4):
  423. iterated = dot("dot", source=iterated)
  424. assert (
  425. reference == iterated
  426. ), "rendering of shapes with multiple peripheries is unstable"
  427. def test_827():
  428. """
  429. Graphviz should not crash when processing the b15.gv example
  430. https://gitlab.com/graphviz/graphviz/-/issues/827
  431. """
  432. b15gv = Path(__file__).parent / "graphs/b15.gv"
  433. assert b15gv.exists(), "missing test case file"
  434. dot("svg", b15gv)
  435. def test_925():
  436. """
  437. spaces should be handled correctly in UTF-8-containing labels in record shapes
  438. https://gitlab.com/graphviz/graphviz/-/issues/925
  439. """
  440. # locate our associated test case in this directory
  441. input = Path(__file__).parent / "925.dot"
  442. assert input.exists(), "unexpectedly mising test case"
  443. # process this with dot
  444. svg = dot("svg", input)
  445. # The output should include the correctly spaced UTF-8 label. Note that these
  446. # are not ASCII capital As in this string, but rather UTF-8 Cyrillic Capital
  447. # Letter As.
  448. assert "ААА ААА ААА" in svg, "incorrect spacing in UTF-8 label"
  449. @pytest.mark.parametrize("testcase", ("1213-1.dot", "1213-2.dot"))
  450. @pytest.mark.xfail(
  451. strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/1213"
  452. )
  453. def test_1213(testcase: str):
  454. """
  455. clustering should not trigger “trouble in init_rank” errors
  456. https://gitlab.com/graphviz/graphviz/-/issues/1213
  457. """
  458. # locate our associated test case in this directory
  459. input = Path(__file__).parent / testcase
  460. assert input.exists(), "unexpectedly mising test case"
  461. # process this with dot
  462. dot("png", input)
  463. def test_1221():
  464. """
  465. assigning a node to two clusters with newrank should not cause a crash
  466. https://gitlab.com/graphviz/graphviz/-/issues/1221
  467. """
  468. # locate our associated test case in this directory
  469. input = Path(__file__).parent / "1221.dot"
  470. assert input.exists(), "unexpectedly missing test case"
  471. # process this with dot
  472. dot("svg", input)
  473. @pytest.mark.skipif(which("gv2gml") is None, reason="gv2gml not available")
  474. def test_1276():
  475. """
  476. quotes within a label should be escaped in translation to GML
  477. https://gitlab.com/graphviz/graphviz/-/issues/1276
  478. """
  479. # DOT input containing a label with quotes
  480. src = 'digraph test {\n x[label=<"Label">];\n}'
  481. # process this to GML
  482. gml = subprocess.check_output(["gv2gml"], input=src, universal_newlines=True)
  483. # the unescaped label should not appear in the output
  484. assert '""Label""' not in gml, "quotes not escaped in label"
  485. # the escaped label should appear in the output
  486. assert (
  487. '"&quot;Label&quot;"' in gml or '"&#34;Label&#34;"' in gml
  488. ), "escaped label not found in GML output"
  489. def test_1308():
  490. """
  491. processing a minimized graph found by Google Autofuzz should not crash
  492. https://gitlab.com/graphviz/graphviz/-/issues/1308
  493. """
  494. # locate our associated test case in this directory
  495. input = Path(__file__).parent / "1308.dot"
  496. assert input.exists(), "unexpectedly missing test case"
  497. # run it through Graphviz
  498. dot("svg", input)
  499. def test_1308_1():
  500. """
  501. processing a malformed graph found by Google Autofuzz should not crash
  502. https://gitlab.com/graphviz/graphviz/-/issues/1308
  503. """
  504. # locate our associated test case in this directory
  505. input = Path(__file__).parent / "1308_1.dot"
  506. assert input.exists(), "unexpectedly missing test case"
  507. # run it through Graphviz
  508. ret = subprocess.call(["dot", "-Tsvg", "-o", os.devnull, input])
  509. assert ret in (0, 1), "Graphviz crashed when processing malformed input"
  510. assert ret == 1, "Graphviz did not reject malformed input"
  511. def test_1314():
  512. """
  513. test that a large font size that produces an overflow in Pango is rejected
  514. https://gitlab.com/graphviz/graphviz/-/issues/1314
  515. """
  516. # locate our associated test case in this directory
  517. input = Path(__file__).parent / "1314.dot"
  518. assert input.exists(), "unexpectedly missing test case"
  519. # ask Graphviz to process it, which should fail
  520. try:
  521. dot("svg", input)
  522. except subprocess.CalledProcessError:
  523. return
  524. # the execution did not fail as expected
  525. pytest.fail("dot incorrectly exited with success")
  526. def test_1318():
  527. """
  528. processing a large number in a comment should not trigger integer overflow
  529. https://gitlab.com/graphviz/graphviz/-/issues/1318
  530. """
  531. # sample input consisting of a large number in a comment
  532. source = "#8828066547613302784"
  533. # processing this should succeed
  534. dot("svg", source=source)
  535. @pytest.mark.xfail(
  536. strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/1328"
  537. )
  538. def test_1328():
  539. """
  540. a node with conflicting rank constraints should not cause a crash
  541. https://gitlab.com/graphviz/graphviz/-/issues/1328
  542. """
  543. # locate our associated test case in this directory
  544. input = Path(__file__).parent / "1328.dot"
  545. assert input.exists(), "unexpectedly missing test case"
  546. proc = subprocess.run(
  547. ["dot", "-Tsvg", "-o", os.devnull, input],
  548. stderr=subprocess.PIPE,
  549. universal_newlines=True,
  550. )
  551. assert proc.returncode in (0, 1), "multiple rank constraints caused a crash"
  552. assert (
  553. "trouble in init_rank" not in proc.stderr
  554. ), "multiple rank constraints caused ranking failure"
  555. def test_1332():
  556. """
  557. Triangulation calculation on the associated example should succeed.
  558. A prior change that was intended to increase accuracy resulted in the
  559. example in this test now failing some triangulation calculations. It is not
  560. clear whether the outcome before or after is correct, but this test ensures
  561. that the older behavior users are accustomed to is preserved.
  562. """
  563. # locate our associated test case in this directory
  564. input = Path(__file__).parent / "1332.dot"
  565. assert input.exists(), "unexpectedly missing test case"
  566. # process it with Graphviz
  567. warnings = subprocess.check_output(
  568. ["dot", "-Tpdf", "-o", os.devnull, input],
  569. stderr=subprocess.STDOUT,
  570. universal_newlines=True,
  571. )
  572. # work around macOS warnings
  573. warnings = remove_xtype_warnings(warnings).strip()
  574. # no warnings should have been printed
  575. assert (
  576. warnings == ""
  577. ), "warnings were printed when processing graph involving triangulation"
  578. def test_1408():
  579. """
  580. parsing particular ortho layouts should not cause an assertion failure
  581. https://gitlab.com/graphviz/graphviz/-/issues/1408
  582. """
  583. # locate our associated test case in this directory
  584. input = Path(__file__).parent / "1408.dot"
  585. assert input.exists(), "unexpectedly missing test case"
  586. # process it with Graphviz
  587. dot("svg", input)
  588. def test_1411():
  589. """
  590. parsing strings containing newlines should not disrupt line number tracking
  591. https://gitlab.com/graphviz/graphviz/-/issues/1411
  592. """
  593. # locate our associated test case in this directory
  594. input = Path(__file__).parent / "1411.dot"
  595. assert input.exists(), "unexpectedly missing test case"
  596. # process it with Graphviz (should fail)
  597. with subprocess.Popen(
  598. ["dot", "-Tsvg", "-o", os.devnull, input],
  599. stderr=subprocess.PIPE,
  600. universal_newlines=True,
  601. ) as p:
  602. _, output = p.communicate()
  603. assert p.returncode != 0, "Graphviz accepted broken input"
  604. assert (
  605. "syntax error in line 17 near '\\'" in output
  606. ), "error message did not identify correct location"
  607. def test_1425():
  608. """
  609. tooltips should propagate to SVG even without an HREF
  610. https://gitlab.com/graphviz/graphviz/-/issues/1425
  611. """
  612. # locate our associated test case in this directory
  613. input = Path(__file__).parent / "1425.dot"
  614. assert input.exists(), "unexpectedly missing test case"
  615. # translate this to SVG
  616. svg = dot("svg", input)
  617. assert re.search(r"\btable tip\b", svg) is not None, "tooltip not propagated to SVG"
  618. def test_1425_1():
  619. """
  620. tooltips should propagate to SVG even without an HREF
  621. https://gitlab.com/graphviz/graphviz/-/issues/1425
  622. """
  623. # locate our associated test case in this directory
  624. input = Path(__file__).parent / "1425_1.dot"
  625. assert input.exists(), "unexpectedly missing test case"
  626. # translate this to SVG
  627. svg = dot("svg", input)
  628. assert (
  629. re.search(r"\bgreek to me\b", svg) is not None
  630. ), "tooltip not propagated to SVG"
  631. assert re.search(r"\benglish\b", svg) is not None, "tooltip not propagated to SVG"
  632. assert (
  633. re.search(r"\bleave a tip\b", svg) is not None
  634. ), "tooltip not propagated to SVG"
  635. assert (
  636. re.search(r"\bcell tool tip\b", svg) is not None
  637. ), "tooltip not propagated to SVG"
  638. assert re.search(r"\btd tip\b", svg) is not None, "tooltip not propagated to SVG"
  639. assert re.search(r"\btable tip\b", svg) is not None, "tooltip not propagated to SVG"
  640. @pytest.mark.xfail(
  641. strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/1435"
  642. )
  643. def test_1435():
  644. """
  645. triangulation paths should be findable on this graph
  646. https://gitlab.com/graphviz/graphviz/-/issues/1435
  647. """
  648. # locate our associated test case in this directory
  649. input = Path(__file__).parent / "1435.dot"
  650. assert input.exists(), "unexpectedly missing test case"
  651. # process it with Graphviz
  652. err = subprocess.check_output(
  653. ["dot", "-Tpng", "-o", os.devnull, input],
  654. stderr=subprocess.STDOUT,
  655. universal_newlines=True,
  656. )
  657. assert err.strip() == "", "errors were printed"
  658. def test_1436():
  659. """
  660. test a segfault from https://gitlab.com/graphviz/graphviz/-/issues/1436 has
  661. not reappeared
  662. """
  663. # locate our associated test case in this directory
  664. input = Path(__file__).parent / "1436.dot"
  665. assert input.exists(), "unexpectedly missing test case"
  666. # ask Graphviz to process it, which should generate a segfault if this bug
  667. # has been reintroduced
  668. dot("svg", input)
  669. def test_1444():
  670. """
  671. specifying 'headport' as an edge attribute should work regardless of what
  672. order attributes appear in
  673. https://gitlab.com/graphviz/graphviz/-/issues/1444
  674. """
  675. # locate the first of our associated tests
  676. input1 = Path(__file__).parent / "1444.dot"
  677. assert input1.exists(), "unexpectedly missing test case"
  678. # ask Graphviz to process it
  679. with subprocess.Popen(
  680. ["dot", "-Tsvg", input1],
  681. stdout=subprocess.PIPE,
  682. stderr=subprocess.PIPE,
  683. universal_newlines=True,
  684. ) as p:
  685. stdout1, stderr = p.communicate()
  686. assert p.returncode == 0, "failed to process a headport edge"
  687. stderr = remove_xtype_warnings(stderr).strip()
  688. assert stderr == "", "emitted an error for a legal graph"
  689. # now locate our second variant, that simply has the attributes swapped
  690. input2 = Path(__file__).parent / "1444-2.dot"
  691. assert input2.exists(), "unexpectedly missing test case"
  692. # process it identically
  693. with subprocess.Popen(
  694. ["dot", "-Tsvg", input2],
  695. stdout=subprocess.PIPE,
  696. stderr=subprocess.PIPE,
  697. universal_newlines=True,
  698. ) as p:
  699. stdout2, stderr = p.communicate()
  700. assert p.returncode == 0, "failed to process a headport edge"
  701. stderr = remove_xtype_warnings(stderr).strip()
  702. assert stderr == "", "emitted an error for a legal graph"
  703. assert stdout1 == stdout2, "swapping edge attributes altered the output graph"
  704. def test_1449():
  705. """
  706. using the SVG color scheme should not cause warnings
  707. https://gitlab.com/graphviz/graphviz/-/issues/1449
  708. """
  709. # start Graphviz
  710. with subprocess.Popen(
  711. ["dot", "-Tsvg", "-o", os.devnull],
  712. stdin=subprocess.PIPE,
  713. stderr=subprocess.PIPE,
  714. universal_newlines=True,
  715. ) as p:
  716. # pass it some input that uses the SVG color scheme
  717. _, stderr = p.communicate('graph g { colorscheme="svg"; }')
  718. assert p.returncode == 0, "Graphviz exited with non-zero status"
  719. assert stderr.strip() == "", "SVG color scheme use caused warnings"
  720. @pytest.mark.xfail(
  721. strict=platform.system() != "Linux",
  722. reason="https://gitlab.com/graphviz/graphviz/-/issues/1453",
  723. )
  724. def test_1453():
  725. """
  726. `splines=curved` should not result in segfaults
  727. https://gitlab.com/graphviz/graphviz/-/issues/1453
  728. """
  729. # locate our associated test case in this directory
  730. input = Path(__file__).parent / "1453.dot"
  731. assert input.exists(), "unexpectedly missing test case"
  732. # run it through Graphviz
  733. dot("svg", input)
  734. @pytest.mark.xfail(
  735. strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/1472"
  736. )
  737. def test_1472():
  738. """
  739. processing a malformed graph found by Google Autofuzz should not crash
  740. https://gitlab.com/graphviz/graphviz/-/issues/1472
  741. """
  742. # locate our associated test case in this directory
  743. input = Path(__file__).parent / "1472.dot"
  744. assert input.exists(), "unexpectedly missing test case"
  745. # run it through Graphviz
  746. dot("svg", input)
  747. def test_1474():
  748. """
  749. processing this input found by fuzzing should not trigger a buffer overflow
  750. https://gitlab.com/graphviz/graphviz/-/issues/1474
  751. """
  752. # locate our associated test case in this directory
  753. input = Path(__file__).parent / "1474.dot"
  754. assert input.exists(), "unexpectedly missing test case"
  755. # run this through Graphviz
  756. proc = subprocess.run(
  757. ["dot", "-o", os.devnull, input], stderr=subprocess.PIPE, check=False
  758. )
  759. assert proc.returncode != 0, "invalid input was not rejected"
  760. assert (
  761. re.search(rb"\bAddressSanitizer: heap-buffer-overflow\b", proc.stderr) is None
  762. ), "malformed input caused a buffer overflow"
  763. def test_1489():
  764. """
  765. processing this input found by fuzzing should not trigger an invalid read
  766. https://gitlab.com/graphviz/graphviz/-/issues/1489
  767. """
  768. # locate our associated test case in this directory
  769. input = Path(__file__).parent / "1489.dot"
  770. assert input.exists(), "unexpectedly missing test case"
  771. # run this through Graphviz
  772. proc = subprocess.run(
  773. ["dot", "-o", os.devnull, input], stderr=subprocess.PIPE, check=False
  774. )
  775. assert proc.returncode != 0, "invalid input was not rejected"
  776. assert (
  777. re.search(rb"\bAddressSanitizer: SEGV\b", proc.stderr) is None
  778. ), "malformed input caused an invalid memory access"
  779. def test_1554():
  780. """
  781. small distances between nodes should not cause a crash in majorization
  782. https://gitlab.com/graphviz/graphviz/-/issues/1554
  783. """
  784. # locate our associated test case in this directory
  785. input = Path(__file__).parent / "1554.dot"
  786. assert input.exists(), "unexpectedly missing test case"
  787. # run it through Graphviz
  788. output = dot("svg", input)
  789. # the output should not have NaN values, indicating out of bounds computation
  790. assert (
  791. re.search(r"\bnan\b", output, flags=re.IGNORECASE) is None
  792. ), "computation exceeded bounds"
  793. def test_1585():
  794. """
  795. clustering nodes should not reverse their horizontal layout
  796. https://gitlab.com/graphviz/graphviz/-/issues/1585
  797. """
  798. # locate our associated test cases in this directory
  799. no_cluster = Path(__file__).parent / "1585_0.dot"
  800. assert no_cluster.exists(), "unexpectedly missing test case"
  801. cluster = Path(__file__).parent / "1585_1.dot"
  802. assert cluster.exists(), "unexpectedly missing test case"
  803. def find_node_xs(svg_output: str) -> Iterator[float]:
  804. """
  805. yield 3 floats representing the X positions of nodes b, c, d in the
  806. given graph
  807. """
  808. # parse the SVG
  809. root = ET.fromstring(svg_output)
  810. # find `b`
  811. b = root.findall(
  812. ".//{http://www.w3.org/2000/svg}title[.='b']../{http://www.w3.org/2000/svg}ellipse"
  813. )
  814. assert len(b) == 1, "could not find node 'b'"
  815. yield float(b[0].attrib["cx"])
  816. # find `c`
  817. c = root.findall(
  818. ".//{http://www.w3.org/2000/svg}title[.='c']../{http://www.w3.org/2000/svg}ellipse"
  819. )
  820. assert len(c) == 1, "could not find node 'c'"
  821. yield float(c[0].attrib["cx"])
  822. # find `d`
  823. d = root.findall(
  824. ".//{http://www.w3.org/2000/svg}title[.='d']../{http://www.w3.org/2000/svg}ellipse"
  825. )
  826. assert len(d) == 1, "could not find node 'd'"
  827. yield float(d[0].attrib["cx"])
  828. # render the one without clusters and get its nodes’ X positions
  829. no_cluster_out = dot("svg", no_cluster)
  830. b, c, d = list(find_node_xs(no_cluster_out))
  831. # confirm we got a left → right ordering
  832. assert b < c, "unexpected horizontal node ordering"
  833. assert c < d, "unexpected horizontal node ordering"
  834. # now try the same thing with the clustered graph
  835. cluster_out = dot("svg", cluster)
  836. b, c, d = list(find_node_xs(cluster_out))
  837. assert b < c, "clustering altered nodes’ horizontal ordering"
  838. assert c < d, "clustering altered nodes’ horizontal ordering"
  839. @pytest.mark.skipif(which("gvpr") is None, reason="GVPR not available")
  840. def test_1594():
  841. """
  842. GVPR should give accurate line numbers in error messages
  843. https://gitlab.com/graphviz/graphviz/-/issues/1594
  844. """
  845. # locate our associated test case in this directory
  846. input = Path(__file__).parent / "1594.gvpr"
  847. # run GVPR with our (malformed) input program
  848. with subprocess.Popen(
  849. ["gvpr", "-f", input],
  850. stdin=subprocess.PIPE,
  851. stdout=subprocess.DEVNULL,
  852. stderr=subprocess.PIPE,
  853. universal_newlines=True,
  854. ) as p:
  855. _, stderr = p.communicate()
  856. assert p.returncode != 0, "GVPR did not reject malformed program"
  857. assert "line 3:" in stderr, "GVPR did not identify correct line of syntax error"
  858. @pytest.mark.parametrize("long,short", (("--help", "-?"), ("--version", "-V")))
  859. def test_1618(long: str, short: str):
  860. """
  861. Graphviz should understand `--help` and `--version`
  862. https://gitlab.com/graphviz/graphviz/-/issues/1618
  863. """
  864. # run Graphviz with the short form of the argument
  865. p1 = subprocess.run(
  866. ["dot", short], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True
  867. )
  868. # run it with the long form of the argument
  869. p2 = subprocess.run(
  870. ["dot", long], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True
  871. )
  872. # output from both should match
  873. assert (
  874. p1.stdout == p2.stdout
  875. ), f"`dot {long}` wrote differing output than `dot {short}`"
  876. assert (
  877. p1.stderr == p2.stderr
  878. ), f"`dot {long}` wrote differing output than `dot {short}`"
  879. @pytest.mark.parametrize(
  880. "test_case", ("1622_0.dot", "1622_1.dot", "1622_2.dot", "1622_3.dot")
  881. )
  882. def test_1622(test_case: str):
  883. """
  884. Narrow HTML table cells should not cause assertion failures
  885. https://gitlab.com/graphviz/graphviz/-/issues/1622
  886. """
  887. # locate our associated test case in this directory
  888. input = Path(__file__).parent / test_case
  889. assert input.exists(), "unexpectedly missing test case"
  890. # process it with Graphviz
  891. dot("png:cairo:cairo", input)
  892. def test_1624():
  893. """
  894. record shapes should be usable
  895. https://gitlab.com/graphviz/graphviz/-/issues/1624
  896. """
  897. # locate our associated test case in this directory
  898. input = Path(__file__).parent / "1624.dot"
  899. assert input.exists(), "unexpectedly missing test case"
  900. # process it with Graphviz
  901. dot("svg", input)
  902. def test_1644():
  903. """
  904. neato results should be deterministic
  905. https://gitlab.com/graphviz/graphviz/-/issues/1644
  906. """
  907. # get our baseline reference
  908. input = Path(__file__).parent / "1644.dot"
  909. assert input.exists(), "unexpectedly missing test case"
  910. ref = subprocess.check_output(["neato", input], universal_newlines=True)
  911. # now repeat this, expecting it not to change
  912. for _ in range(20):
  913. out = subprocess.check_output(["neato", input], universal_newlines=True)
  914. assert ref == out, "repeated rendering changed output"
  915. def test_1658():
  916. """
  917. the graph associated with this test case should not crash Graphviz
  918. https://gitlab.com/graphviz/graphviz/-/issues/1658
  919. """
  920. # locate our associated test case in this directory
  921. input = Path(__file__).parent / "1658.dot"
  922. assert input.exists(), "unexpectedly missing test case"
  923. # process it with Graphviz
  924. dot("png", input)
  925. def test_1676():
  926. """
  927. https://gitlab.com/graphviz/graphviz/-/issues/1676
  928. """
  929. # locate our associated test case in this directory
  930. input = Path(__file__).parent / "1676.dot"
  931. assert input.exists(), "unexpectedly missing test case"
  932. # run Graphviz with this input
  933. ret = subprocess.call(["dot", "-Tsvg", "-o", os.devnull, input])
  934. # this malformed input should not have caused Graphviz to crash
  935. assert ret != -signal.SIGSEGV, "Graphviz segfaulted"
  936. @pytest.mark.skipif(which("gvpr") is None, reason="GVPR not available")
  937. def test_1702():
  938. """
  939. GVPR library program `depath` should work on arbitrary examples
  940. https://gitlab.com/graphviz/graphviz/-/issues/1702
  941. """
  942. # locate our associated test case in this directory
  943. input = Path(__file__).parent / "1702.dot"
  944. assert input.exists(), "unexpectedly missing test case"
  945. # find the library program
  946. depath = Path(__file__).parents[1] / "cmd/gvpr/lib/depath"
  947. assert depath.exists(), "GVPR library program depath missing"
  948. # run GVPR
  949. gvpr_bin = which("gvpr")
  950. proc = subprocess.run(
  951. [gvpr_bin, "-c", "-o", os.devnull, "-f", depath, input],
  952. stderr=subprocess.PIPE,
  953. universal_newlines=True,
  954. check=True,
  955. )
  956. assert proc.stderr.strip() == "", "depath errored on tests/1702.dot"
  957. def test_1724():
  958. """
  959. passing malformed node and newrank should not cause segfaults
  960. https://gitlab.com/graphviz/graphviz/-/issues/1724
  961. """
  962. # locate our associated test case in this directory
  963. input = Path(__file__).parent / "1724.dot"
  964. assert input.exists(), "unexpectedly missing test case"
  965. # run Graphviz with this input
  966. ret = subprocess.call(["dot", "-Tsvg", "-o", os.devnull, input])
  967. assert ret != -signal.SIGSEGV, "Graphviz segfaulted"
  968. @pytest.mark.skipif(
  969. is_static_build(),
  970. reason="dynamic libraries are unavailable to link against in static builds",
  971. )
  972. def test_1767():
  973. """
  974. using the Pango plugin multiple times should produce consistent results
  975. https://gitlab.com/graphviz/graphviz/-/issues/1767
  976. """
  977. # find co-located test source
  978. c_src = (Path(__file__).parent / "1767.c").resolve()
  979. assert c_src.exists(), "missing test case"
  980. # find our co-located dot input
  981. src = (Path(__file__).parent / "1767.dot").resolve()
  982. assert src.exists(), "missing test case"
  983. stdout, _ = run_c(c_src, [src], link=["cgraph", "gvc"])
  984. assert stdout.splitlines() == [
  985. "Loaded graph:clusters",
  986. "cluster_0 contains 5 nodes",
  987. "cluster_1 contains 1 nodes",
  988. "cluster_2 contains 3 nodes",
  989. "cluster_3 contains 3 nodes",
  990. "Loaded graph:clusters",
  991. "cluster_0 contains 5 nodes",
  992. "cluster_1 contains 1 nodes",
  993. "cluster_2 contains 3 nodes",
  994. "cluster_3 contains 3 nodes",
  995. ]
  996. @pytest.mark.skipif(which("gvpr") is None, reason="GVPR not available")
  997. @pytest.mark.skipif(platform.system() != "Windows", reason="only relevant on Windows")
  998. def test_1780():
  999. """
  1000. GVPR should accept programs at absolute paths
  1001. https://gitlab.com/graphviz/graphviz/-/issues/1780
  1002. """
  1003. # get absolute path to an arbitrary GVPR program
  1004. clustg = Path(__file__).resolve().parent.parent / "cmd/gvpr/lib/clustg"
  1005. # GVPR should not fail when given this path
  1006. gvpr(clustg)
  1007. def test_1783():
  1008. """
  1009. Graphviz should not segfault when passed large edge weights
  1010. https://gitlab.com/graphviz/graphviz/-/issues/1783
  1011. """
  1012. # locate our associated test case in this directory
  1013. input = Path(__file__).parent / "1783.dot"
  1014. assert input.exists(), "unexpectedly missing test case"
  1015. # run Graphviz with this input
  1016. ret = subprocess.call(["dot", "-Tsvg", "-o", os.devnull, input])
  1017. assert ret != 0, "Graphviz accepted illegal edge weight"
  1018. assert ret != -signal.SIGSEGV, "Graphviz segfaulted"
  1019. @pytest.mark.skipif(which("gvedit") is None, reason="Gvedit not available")
  1020. def test_1813():
  1021. """
  1022. gvedit -? should show usage
  1023. https://gitlab.com/graphviz/graphviz/-/issues/1813
  1024. """
  1025. environ_copy = os.environ.copy()
  1026. environ_copy.pop("DISPLAY", None)
  1027. output = subprocess.check_output(
  1028. ["gvedit", "-?"], env=environ_copy, universal_newlines=True
  1029. )
  1030. assert "Usage" in output, "gvedit -? did not show usage"
  1031. def test_1845():
  1032. """
  1033. rendering sequential graphs to PS should not segfault
  1034. https://gitlab.com/graphviz/graphviz/-/issues/1845
  1035. """
  1036. # locate our associated test case in this directory
  1037. input = Path(__file__).parent / "1845.dot"
  1038. assert input.exists(), "unexpectedly missing test case"
  1039. # generate a multipage PS file from this input
  1040. dot("ps", input)
  1041. @pytest.mark.xfail(strict=True) # FIXME
  1042. def test_1856():
  1043. """
  1044. headports and tailports should be respected
  1045. https://gitlab.com/graphviz/graphviz/-/issues/1856
  1046. """
  1047. # locate our associated test case in this directory
  1048. input = Path(__file__).parent / "1856.dot"
  1049. assert input.exists(), "unexpectedly missing test case"
  1050. # process it into JSON
  1051. out = dot("json", input)
  1052. data = json.loads(out)
  1053. # find the two nodes, “3” and “5”
  1054. three = [x for x in data["objects"] if x["name"] == "3"][0]
  1055. five = [x for x in data["objects"] if x["name"] == "5"][0]
  1056. # find the edge from “3” to “5”
  1057. edge = [
  1058. x
  1059. for x in data["edges"]
  1060. if x["tail"] == three["_gvid"] and x["head"] == five["_gvid"]
  1061. ][0]
  1062. # The edge should look something like:
  1063. #
  1064. # ┌─┐
  1065. # │3│
  1066. # └┬┘
  1067. # ┌────┘
  1068. # ┌┴┐
  1069. # │5│
  1070. # └─┘
  1071. #
  1072. # but a bug causes port constraints to not be respected and the edge comes out
  1073. # more like:
  1074. #
  1075. # ┌─┐
  1076. # │3│
  1077. # └┬┘
  1078. # │
  1079. # ┌─┐ │
  1080. # ├5̶┼───┘
  1081. # └─┘
  1082. #
  1083. # So validate that the edge’s path does not dip below the top of the “5” node.
  1084. top_of_five = max(y for _, y in five["_draw_"][1]["points"])
  1085. waypoints_y = [y for _, y in edge["_draw_"][1]["points"]]
  1086. assert all(y >= top_of_five for y in waypoints_y), "edge dips below 5"
  1087. @pytest.mark.skipif(which("fdp") is None, reason="fdp not available")
  1088. def test_1865():
  1089. """
  1090. fdp should not read out of bounds when processing node names
  1091. https://gitlab.com/graphviz/graphviz/-/issues/1865
  1092. Note, the crash this test tries to provoke may only occur when run under
  1093. Address Sanitizer or Valgrind
  1094. """
  1095. # locate our associated test case in this directory
  1096. input = Path(__file__).parent / "1865.dot"
  1097. assert input.exists(), "unexpectedly missing test case"
  1098. # fdp should not crash when processing this file
  1099. subprocess.check_call(["fdp", "-o", os.devnull, input])
  1100. @pytest.mark.skipif(which("gv2gml") is None, reason="gv2gml not available")
  1101. @pytest.mark.skipif(which("gml2gv") is None, reason="gml2gv not available")
  1102. @pytest.mark.parametrize(
  1103. "penwidth",
  1104. (pytest.param("1.0", id="penwidth=1.0"), pytest.param("1", id="pendwidth=1")),
  1105. )
  1106. def test_1871(penwidth: str):
  1107. """
  1108. round tripping something with either an integer or real `penwidth` through
  1109. gv2gml→gml2gv should return the correct `penwidth`
  1110. """
  1111. # a trivial graph
  1112. input = f"graph {{ a [penwidth={penwidth}] }}"
  1113. # pass it through gv2gml
  1114. gv = subprocess.check_output(["gv2gml"], input=input, universal_newlines=True)
  1115. # pass this through gml2gv
  1116. gml = subprocess.check_output(["gml2gv"], input=gv, universal_newlines=True)
  1117. # the result should have a `penwidth` of 1
  1118. has_1 = re.search(r"\bpenwidth\s*=\s*1[^\.]", gml) is not None
  1119. has_1_0 = re.search(r"\bpenwidth\s*=\s*1\.0\b", gml) is not None
  1120. assert (
  1121. has_1 or has_1_0
  1122. ), f"incorrect penwidth from round tripping through GML (output {gml})"
  1123. @pytest.mark.skipif(which("fdp") is None, reason="fdp not available")
  1124. def test_1876():
  1125. """
  1126. fdp should not rename nodes with internal names
  1127. https://gitlab.com/graphviz/graphviz/-/issues/1876
  1128. """
  1129. # a trivial graph to provoke this issue
  1130. input = "graph { a }"
  1131. # process this with fdp
  1132. try:
  1133. output = subprocess.check_output(["fdp"], input=input, universal_newlines=True)
  1134. except subprocess.CalledProcessError as e:
  1135. raise RuntimeError("fdp failed to process trivial graph") from e
  1136. # we should not see any internal names like "%3"
  1137. assert "%" not in output, "internal name in fdp output"
  1138. @pytest.mark.skipif(which("fdp") is None, reason="fdp not available")
  1139. def test_1877():
  1140. """
  1141. fdp should not fail an assertion when processing cluster edges
  1142. https://gitlab.com/graphviz/graphviz/-/issues/1877
  1143. """
  1144. # simple input with a cluster edge
  1145. input = "graph {subgraph cluster_a {}; cluster_a -- b}"
  1146. # fdp should be able to process this
  1147. subprocess.run(
  1148. ["fdp", "-o", os.devnull], input=input, check=True, universal_newlines=True
  1149. )
  1150. def test_1880():
  1151. """
  1152. parsing a particular graph should not cause a Trapezoid-table overflow
  1153. assertion failure
  1154. https://gitlab.com/graphviz/graphviz/-/issues/1880
  1155. """
  1156. # locate our associated test case in this directory
  1157. input = Path(__file__).parent / "1880.dot"
  1158. assert input.exists(), "unexpectedly missing test case"
  1159. # process it with Graphviz
  1160. dot("png", input)
  1161. @pytest.mark.xfail(
  1162. strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/1887"
  1163. )
  1164. def test_1887():
  1165. """
  1166. empty strings as labels should be propagated to dot output
  1167. https://gitlab.com/graphviz/graphviz/-/issues/1887
  1168. """
  1169. # find co-located test source
  1170. c_src = (Path(__file__).parent / "1887.c").resolve()
  1171. assert c_src.exists(), "missing test case"
  1172. # generate a graph and pass it through dot
  1173. stdout, _ = run_c(c_src, link=["cgraph"])
  1174. assert (
  1175. re.search(r'label\s*=\s*""', stdout) is not None
  1176. ), "empty label missing in output"
  1177. def test_1898():
  1178. """
  1179. test a segfault from https://gitlab.com/graphviz/graphviz/-/issues/1898 has
  1180. not reappeared
  1181. """
  1182. # locate our associated test case in this directory
  1183. input = Path(__file__).parent / "1898.dot"
  1184. assert input.exists(), "unexpectedly missing test case"
  1185. # ask Graphviz to process it, which should generate a segfault if this bug
  1186. # has been reintroduced
  1187. dot("svg", input)
  1188. def test_1902():
  1189. """
  1190. test a segfault from https://gitlab.com/graphviz/graphviz/-/issues/1902 has
  1191. not reappeared
  1192. """
  1193. # locate our associated test case in this directory
  1194. input = Path(__file__).parent / "1902.dot"
  1195. assert input.exists(), "unexpectedly missing test case"
  1196. # ask Graphviz to process it, which should generate a segfault if this bug
  1197. # has been reintroduced
  1198. dot("svg", input)
  1199. # root directory of this checkout
  1200. ROOT = Path(__file__).parent.parent.resolve()
  1201. def test_1855():
  1202. """
  1203. SVGs should have a scale with sufficient precision
  1204. https://gitlab.com/graphviz/graphviz/-/issues/1855
  1205. """
  1206. # locate our associated test case in this directory
  1207. src = Path(__file__).parent / "1855.dot"
  1208. assert src.exists(), "unexpectedly missing test case"
  1209. # run it through Graphviz
  1210. svg = dot("svg", src)
  1211. # find the graph element
  1212. root = ET.fromstring(svg)
  1213. graph = root[0]
  1214. assert graph.get("class") == "graph", "could not find graph element"
  1215. # extract its `transform` attribute
  1216. transform = graph.get("transform")
  1217. # this should begin with a scale directive
  1218. m = re.match(r"scale\((?P<x>\d+(\.\d*)?) (?P<y>\d+(\.\d*))\)", transform)
  1219. assert m is not None, f"failed to find 'scale' in '{transform}'"
  1220. x = m.group("x")
  1221. y = m.group("y")
  1222. # the scale should be somewhere in reasonable range of what is expected
  1223. assert float(x) >= 0.32 and float(x) <= 0.34, "inaccurate x scale"
  1224. assert float(y) >= 0.32 and float(y) <= 0.34, "inaccurate y scale"
  1225. # two digits of precision are insufficient for this example, so require a
  1226. # greater number of digits in both scale components
  1227. assert len(x) > 4, "insufficient precision in x scale"
  1228. assert len(y) > 4, "insufficient precision in y scale"
  1229. @pytest.mark.parametrize("variant", [1, 2])
  1230. @pytest.mark.skipif(which("gml2gv") is None, reason="gml2gv not available")
  1231. def test_1869(variant: int):
  1232. """
  1233. gml2gv should be able to parse the style, outlineStyle, width and
  1234. outlineWidth GML attributes and map them to the DOT attributes
  1235. style and penwidth respectively
  1236. https://gitlab.com/graphviz/graphviz/-/issues/1869
  1237. """
  1238. # locate our associated test case in this directory
  1239. input = Path(__file__).parent / f"1869-{variant}.gml"
  1240. assert input.exists(), "unexpectedly missing test case"
  1241. # ask gml2gv to translate it to DOT
  1242. output = subprocess.check_output(["gml2gv", input], universal_newlines=True)
  1243. assert "style=dashed" in output, "style=dashed not found in DOT output"
  1244. assert "penwidth=2" in output, "penwidth=2 not found in DOT output"
  1245. def test_1879():
  1246. """https://gitlab.com/graphviz/graphviz/-/issues/1879"""
  1247. # locate our associated test case in this directory
  1248. input = Path(__file__).parent / "1879.dot"
  1249. assert input.exists(), "unexpectedly missing test case"
  1250. # process it with DOT
  1251. stdout = subprocess.check_output(
  1252. ["dot", "-Tsvg", "-o", os.devnull, input],
  1253. cwd=Path(__file__).parent,
  1254. stderr=subprocess.STDOUT,
  1255. universal_newlines=True,
  1256. )
  1257. # check we did not trigger an assertion failure
  1258. assert re.search(r"\bAssertion\b.*\bfailed\b", stdout) is None
  1259. def test_1879_2():
  1260. """
  1261. another variant of lhead/ltail + compound
  1262. https://gitlab.com/graphviz/graphviz/-/issues/1879
  1263. """
  1264. # locate our associated test case in this directory
  1265. input = Path(__file__).parent / "1879-2.dot"
  1266. assert input.exists(), "unexpectedly missing test case"
  1267. # process it with DOT
  1268. subprocess.check_call(["dot", "-Gmargin=0", "-Tpng", "-o", os.devnull, input])
  1269. def test_1893():
  1270. """
  1271. an HTML label containing just a ] should work
  1272. https://gitlab.com/graphviz/graphviz/-/issues/1893
  1273. """
  1274. # a graph containing a node with an HTML label with a ] in a table cell
  1275. input = "digraph { 0 [label=<<TABLE><TR><TD>]</TD></TR></TABLE>>] }"
  1276. # ask Graphviz to process this
  1277. dot("svg", source=input)
  1278. # we should be able to do the same with an escaped ]
  1279. input = "digraph { 0 [label=<<TABLE><TR><TD>&#93;</TD></TR></TABLE>>] }"
  1280. dot("svg", source=input)
  1281. def test_1906():
  1282. """
  1283. graphs that generate large rectangles should be accepted
  1284. https://gitlab.com/graphviz/graphviz/-/issues/1906
  1285. """
  1286. # one of the rtest graphs is sufficient to provoke this
  1287. input = Path(__file__).parent / "graphs/root.gv"
  1288. assert input.exists(), "unexpectedly missing test case"
  1289. # use Circo to translate it to DOT
  1290. subprocess.check_call(["dot", "-Kcirco", "-Tgv", "-o", os.devnull, input])
  1291. @pytest.mark.skipif(which("twopi") is None, reason="twopi not available")
  1292. def test_1907():
  1293. """
  1294. SVG edges should have title elements that match their names
  1295. https://gitlab.com/graphviz/graphviz/-/issues/1907
  1296. """
  1297. # a trivial graph to provoke this issue
  1298. input = "digraph { A -> B -> C }"
  1299. # generate an SVG from this input with twopi
  1300. output = subprocess.check_output(
  1301. ["twopi", "-Tsvg"], input=input, universal_newlines=True
  1302. )
  1303. assert "<title>A&#45;&gt;B</title>" in output, "element title not found in SVG"
  1304. @pytest.mark.skipif(which("gvpr") is None, reason="gvpr not available")
  1305. def test_1909():
  1306. """
  1307. GVPR should not output internal names
  1308. https://gitlab.com/graphviz/graphviz/-/issues/1909
  1309. """
  1310. # locate our associated test case in this directory
  1311. prog = Path(__file__).parent / "1909.gvpr"
  1312. graph = Path(__file__).parent / "1909.dot"
  1313. # run GVPR with the given input
  1314. output = subprocess.check_output(
  1315. ["gvpr", "-c", "-f", prog, graph], universal_newlines=True
  1316. )
  1317. # we should have produced this graph without names like "%2" in it
  1318. assert output == "// begin\n" "digraph bug {\n" " a -> b;\n" " b -> c;\n" "}\n"
  1319. @pytest.mark.skipif(
  1320. is_static_build(),
  1321. reason="dynamic libraries are unavailable to link against in static builds",
  1322. )
  1323. def test_1910():
  1324. """
  1325. Repeatedly using agmemread() should have consistent results
  1326. https://gitlab.com/graphviz/graphviz/-/issues/1910
  1327. """
  1328. # find co-located test source
  1329. c_src = (Path(__file__).parent / "1910.c").resolve()
  1330. assert c_src.exists(), "missing test case"
  1331. # run the test
  1332. _, _ = run_c(c_src, link=["cgraph", "gvc"])
  1333. def test_1913():
  1334. """
  1335. ALIGN attributes in <BR> tags should be parsed correctly
  1336. https://gitlab.com/graphviz/graphviz/-/issues/1913
  1337. """
  1338. # a template of a trivial graph using an ALIGN attribute
  1339. graph = (
  1340. "digraph {{\n"
  1341. ' table1[label=<<table><tr><td align="text">hello world'
  1342. '<br align="{}"/></td></tr></table>>];\n'
  1343. "}}"
  1344. )
  1345. def run(input):
  1346. """
  1347. run Dot with the given input and return its exit status and stderr
  1348. """
  1349. with subprocess.Popen(
  1350. ["dot", "-Tsvg", "-o", os.devnull],
  1351. stdin=subprocess.PIPE,
  1352. stderr=subprocess.PIPE,
  1353. universal_newlines=True,
  1354. ) as p:
  1355. _, stderr = p.communicate(input)
  1356. return p.returncode, remove_asan_summary(remove_xtype_warnings(stderr))
  1357. # Graphviz should accept all legal values for this attribute
  1358. for align in ("left", "right", "center"):
  1359. input = align
  1360. ret, stderr = run(graph.format(input))
  1361. assert ret == 0
  1362. assert stderr.strip() == ""
  1363. # these attributes should also be valid when title cased
  1364. input = f"{align[0].upper()}{align[1:]}"
  1365. ret, stderr = run(graph.format(input))
  1366. assert ret == 0
  1367. assert stderr.strip() == ""
  1368. # similarly, they should be valid when upper cased
  1369. input = align.upper()
  1370. ret, stderr = run(graph.format(input))
  1371. assert ret == 0
  1372. assert stderr.strip() == ""
  1373. # various invalid things that have the same prefix or suffix as a valid
  1374. # alignment should be rejected
  1375. for align in ("lamp", "deft", "round", "might", "circle", "venter"):
  1376. input = align
  1377. _, stderr = run(graph.format(input))
  1378. assert f"Warning: Illegal value {input} for ALIGN - ignored" in stderr
  1379. # these attributes should also fail when title cased
  1380. input = f"{align[0].upper()}{align[1:]}"
  1381. _, stderr = run(graph.format(input))
  1382. assert f"Warning: Illegal value {input} for ALIGN - ignored" in stderr
  1383. # similarly, they should fail when upper cased
  1384. input = align.upper()
  1385. _, stderr = run(graph.format(input))
  1386. assert f"Warning: Illegal value {input} for ALIGN - ignored" in stderr
  1387. @pytest.mark.skipif(which("gvpr") is None, reason="GVPR not available")
  1388. @pytest.mark.xfail(
  1389. strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/1925"
  1390. )
  1391. def test_1925():
  1392. """
  1393. GVPR `hasAttr` should work accurately
  1394. https://gitlab.com/graphviz/graphviz/-/issues/1925
  1395. """
  1396. # locate our associated test case in this directory
  1397. input = Path(__file__).parent / "1925.dot"
  1398. assert input.exists(), "unexpectedly missing test case"
  1399. script = Path(__file__).parent / "1925.gvpr"
  1400. assert script.exists(), "unexpectedly missing test case"
  1401. # run GVPR
  1402. gvpr_bin = which("gvpr")
  1403. stdout = subprocess.check_output(
  1404. [gvpr_bin, "-c", "-f", script, input], universal_newlines=True
  1405. )
  1406. # check we got expected results
  1407. styled = set(["L"])
  1408. minlened = set(["S->T"])
  1409. active = None
  1410. for line in stdout.split("\n"):
  1411. if m := re.match("// (NODE|EDGE): (?P<name>.*)$", line):
  1412. active = m.group("name")
  1413. continue
  1414. if m := re.match(r"//\s+style :: (?P<value>0|1)$", line):
  1415. assert active is not None, "style line with no known node/edge"
  1416. if m.group("value") == "0":
  1417. assert (
  1418. active not in styled
  1419. ), f"{active} incorrectly considered to have 'style' attribute"
  1420. else:
  1421. assert (
  1422. active in styled
  1423. ), f"{active} incorrectly considered to not have 'style' attribute"
  1424. if m := re.match(r"//\s+minlen :: (?P<value>0|1)$", line):
  1425. assert active is not None, "minlen line with no known node/edge"
  1426. if m.group("value") == "0":
  1427. assert (
  1428. active not in minlened
  1429. ), f"{active} incorrectly considered to have 'minlen' attribute"
  1430. else:
  1431. assert (
  1432. active in minlened
  1433. ), f"{active} incorrectly considered to not have 'minlen' attribute"
  1434. def test_1931():
  1435. """
  1436. New lines within strings should not be discarded during parsing
  1437. https://gitlab.com/graphviz/graphviz/-/issues/1931
  1438. """
  1439. # a graph with \n inside of strings
  1440. graph = (
  1441. "graph {\n"
  1442. ' node1 [label="line 1\n'
  1443. "line 2\n"
  1444. '"];\n'
  1445. ' node2 [label="line 3\n'
  1446. 'line 4"];\n'
  1447. " node1 -- node2\n"
  1448. ' node2 -- "line 5\n'
  1449. 'line 6"\n'
  1450. "}"
  1451. )
  1452. # ask Graphviz to process this to dot output
  1453. xdot = dot("xdot", source=graph)
  1454. # all new lines in strings should have been preserved
  1455. assert "line 1\nline 2\n" in xdot
  1456. assert "line 3\nline 4" in xdot
  1457. assert "line 5\nline 6" in xdot
  1458. @pytest.mark.xfail(
  1459. strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/1939"
  1460. )
  1461. def test_1939():
  1462. """
  1463. clustering should not cause “trouble in init_rank” errors
  1464. https://gitlab.com/graphviz/graphviz/-/issues/1939
  1465. """
  1466. # locate our associated test case in this directory
  1467. input = Path(__file__).parent / "1939.dot"
  1468. assert input.exists(), "unexpectedly missing test case"
  1469. # run it through Graphviz
  1470. dot("svg", input)
  1471. @pytest.mark.xfail() # FIXME
  1472. def test_1949():
  1473. """
  1474. rankdir=LR + compound=true should not lead to an assertion failure
  1475. https://gitlab.com/graphviz/graphviz/-/issues/1949
  1476. """
  1477. # locate our associated test case in this directory
  1478. input = Path(__file__).parent / "1949.dot"
  1479. assert input.exists(), "unexpectedly missing test case"
  1480. # run it through Graphviz
  1481. dot("png", input)
  1482. @pytest.mark.skipif(which("edgepaint") is None, reason="edgepaint not available")
  1483. def test_1971():
  1484. """
  1485. edgepaint should reject invalid command line options
  1486. https://gitlab.com/graphviz/graphviz/-/issues/1971
  1487. """
  1488. # a basic graph that edgepaint can process
  1489. input = (
  1490. "digraph {\n"
  1491. ' graph [bb="0,0,54,108"];\n'
  1492. ' node [label="\\N"];\n'
  1493. " a [height=0.5,\n"
  1494. ' pos="27,90",\n'
  1495. " width=0.75];\n"
  1496. " b [height=0.5,\n"
  1497. ' pos="27,18",\n'
  1498. " width=0.75];\n"
  1499. ' a -> b [pos="e,27,36.104 27,71.697 27,63.983 27,54.712 27,46.112"];\n'
  1500. "}"
  1501. )
  1502. # run edgepaint with an invalid option, `-rabbit`, that happens to have the
  1503. # same first character as valid options
  1504. args = ["edgepaint", "-rabbit"]
  1505. with subprocess.Popen(args, stdin=subprocess.PIPE, universal_newlines=True) as p:
  1506. p.communicate(input)
  1507. assert p.returncode != 0, "edgepaint incorrectly accepted '-rabbit'"
  1508. def test_1990():
  1509. """
  1510. using ortho and circo in combination should not cause an assertion failure
  1511. https://gitlab.com/graphviz/graphviz/-/issues/14
  1512. """
  1513. # locate our associated test case in this directory
  1514. input = Path(__file__).parent / "1990.dot"
  1515. assert input.exists(), "unexpectedly missing test case"
  1516. # process it with Graphviz
  1517. subprocess.check_call(["circo", "-Tsvg", "-o", os.devnull, input])
  1518. @pytest.mark.skipif(
  1519. is_static_build(),
  1520. reason="dynamic libraries are unavailable to link against in static builds",
  1521. )
  1522. def test_2057():
  1523. """
  1524. gvToolTred should be usable by user code
  1525. https://gitlab.com/graphviz/graphviz/-/issues/2057
  1526. """
  1527. # find co-located test source
  1528. c_src = (Path(__file__).parent / "2057.c").resolve()
  1529. assert c_src.exists(), "missing test case"
  1530. # run the test
  1531. _, _ = run_c(c_src, link=["gvc"])
  1532. def test_2078():
  1533. """
  1534. Incorrectly using the "layout" attribute on a subgraph should result in a
  1535. sensible error.
  1536. https://gitlab.com/graphviz/graphviz/-/issues/2078
  1537. """
  1538. # our sample graph that incorrectly uses layout
  1539. input = "graph {\n subgraph {\n layout=osage\n }\n}"
  1540. # run it through Graphviz
  1541. with subprocess.Popen(
  1542. ["dot", "-Tcanon", "-o", os.devnull],
  1543. stdin=subprocess.PIPE,
  1544. stderr=subprocess.PIPE,
  1545. universal_newlines=True,
  1546. ) as p:
  1547. _, stderr = p.communicate(input)
  1548. assert p.returncode != 0, "layout on subgraph was incorrectly accepted"
  1549. assert (
  1550. "layout attribute is invalid except on the root graph" in stderr
  1551. ), "expected warning not found"
  1552. # a graph that correctly uses layout
  1553. input = "graph {\n layout=osage\n subgraph {\n }\n}"
  1554. # ensure this one does not trigger warnings
  1555. with subprocess.Popen(
  1556. ["dot", "-Tcanon", "-o", os.devnull],
  1557. stdin=subprocess.PIPE,
  1558. stdout=subprocess.PIPE,
  1559. stderr=subprocess.PIPE,
  1560. universal_newlines=True,
  1561. ) as p:
  1562. stdout, stderr = p.communicate(input)
  1563. assert p.returncode == 0, "correct layout use was rejected"
  1564. assert stdout.strip() == "", "unexpected output"
  1565. assert (
  1566. "layout attribute is invalid except on the root graph" not in stderr
  1567. ), "incorrect warning output"
  1568. def test_2082():
  1569. """
  1570. Check a bug in inside_polygon has not been reintroduced.
  1571. https://gitlab.com/graphviz/graphviz/-/issues/2082
  1572. """
  1573. # locate our associated test case in this directory
  1574. input = Path(__file__).parent / "2082.dot"
  1575. assert input.exists(), "unexpectedly missing test case"
  1576. # ask Graphviz to process it, which should generate an assertion failure if
  1577. # this bug has been reintroduced
  1578. dot("png", input)
  1579. def test_2087():
  1580. """
  1581. spline routing should be aware of and ignore concentrated edges
  1582. https://gitlab.com/graphviz/graphviz/-/issues/2087
  1583. """
  1584. # locate our associated test case in this directory
  1585. input = Path(__file__).parent / "2087.dot"
  1586. assert input.exists(), "unexpectedly missing test case"
  1587. # process it with Graphviz
  1588. warnings = subprocess.check_output(
  1589. ["dot", "-Tpng", "-o", os.devnull, input],
  1590. stderr=subprocess.STDOUT,
  1591. universal_newlines=True,
  1592. )
  1593. # work around macOS warnings
  1594. warnings = remove_xtype_warnings(warnings).strip()
  1595. # work around ASan informational printing
  1596. warnings = remove_asan_summary(warnings)
  1597. # no warnings should have been printed
  1598. assert (
  1599. warnings == ""
  1600. ), "warnings were printed when processing concentrated duplicate edges"
  1601. @pytest.mark.xfail(strict=True)
  1602. @pytest.mark.parametrize("html_like_first", (False, True))
  1603. def test_2089(html_like_first: bool): # FIXME
  1604. """
  1605. HTML-like and non-HTML-like strings should peacefully coexist
  1606. https://gitlab.com/graphviz/graphviz/-/issues/2089
  1607. """
  1608. # a graph using an HTML-like string and a non-HTML-like string
  1609. if html_like_first:
  1610. graph = 'graph {\n a[label=<foo>];\n b[label="foo"];\n}'
  1611. else:
  1612. graph = 'graph {\n a[label="foo"];\n b[label=<foo>];\n}'
  1613. # normalize the graph
  1614. canonical = dot("dot", source=graph)
  1615. assert "label=foo" in canonical, "non-HTML-like label not found"
  1616. assert "label=<foo>" in canonical, "HTML-like label not found"
  1617. @pytest.mark.xfail(strict=True) # FIXME
  1618. def test_2089_2():
  1619. """
  1620. HTML-like and non-HTML-like strings should peacefully coexist
  1621. https://gitlab.com/graphviz/graphviz/-/issues/2089
  1622. """
  1623. # find co-located test source
  1624. c_src = (Path(__file__).parent / "2089.c").resolve()
  1625. assert c_src.exists(), "missing test case"
  1626. # run it
  1627. _, _ = run_c(c_src, link=["cgraph"])
  1628. @pytest.mark.skipif(which("dot2gxl") is None, reason="dot2gxl not available")
  1629. def test_2092():
  1630. """
  1631. an empty node ID should not cause a dot2gxl NULL pointer dereference
  1632. https://gitlab.com/graphviz/graphviz/-/issues/2092
  1633. """
  1634. p = subprocess.run(["dot2gxl", "-d"], input='<node id="">', universal_newlines=True)
  1635. assert p.returncode != 0, "dot2gxl accepted invalid input"
  1636. assert p.returncode == 1, "dot2gxl crashed"
  1637. @pytest.mark.skipif(which("dot2gxl") is None, reason="dot2gxl not available")
  1638. def test_2093():
  1639. """
  1640. dot2gxl should handle elements with no ID
  1641. https://gitlab.com/graphviz/graphviz/-/issues/2093
  1642. """
  1643. with subprocess.Popen(
  1644. ["dot2gxl", "-d"], stdin=subprocess.PIPE, universal_newlines=True
  1645. ) as p:
  1646. p.communicate('<graph x="">')
  1647. assert p.returncode == 1, "dot2gxl did not reject missing ID"
  1648. @pytest.mark.skipif(which("dot2gxl") is None, reason="dot2gxl not available")
  1649. def test_2094():
  1650. """
  1651. dot2gxl should not crash when decoding a closing node tag after a closing
  1652. graph tag
  1653. https://gitlab.com/graphviz/graphviz/-/issues/2094
  1654. """
  1655. # locate our associated test case in this directory
  1656. input = Path(__file__).parent / "2094.xml"
  1657. assert input.exists(), "unexpectedly missing test case"
  1658. dot2gxl = which("dot2gxl")
  1659. ret = subprocess.call([dot2gxl, "-d", input])
  1660. assert ret in (
  1661. 0,
  1662. 1,
  1663. ), "dot2gxl crashed when processing a closing node tag after a closing graph tag"
  1664. assert ret == 1, "dot2gxl did not reject malformed XML"
  1665. def test_2095():
  1666. """
  1667. Exceeding 1000 boxes during computation should not cause a crash
  1668. https://gitlab.com/graphviz/graphviz/-/issues/2095
  1669. """
  1670. # locate our associated test case in this directory
  1671. input = Path(__file__).parent / "2095.dot"
  1672. assert input.exists(), "unexpectedly missing test case"
  1673. # ask Graphviz to process it
  1674. dot("pdf", input)
  1675. @pytest.mark.skipif(which("gv2gml") is None, reason="gv2gml not available")
  1676. def test_2131():
  1677. """
  1678. gv2gml should be able to process basic Graphviz input
  1679. https://gitlab.com/graphviz/graphviz/-/issues/2131
  1680. """
  1681. # a trivial graph
  1682. input = "digraph { a -> b; }"
  1683. # ask gv2gml what it thinks of this
  1684. try:
  1685. subprocess.run(["gv2gml"], input=input, check=True, universal_newlines=True)
  1686. except subprocess.CalledProcessError as e:
  1687. raise RuntimeError("gv2gml rejected a basic graph") from e
  1688. @pytest.mark.skipif(which("gvpr") is None, reason="gvpr not available")
  1689. @pytest.mark.parametrize("examine", ("indices", "tokens"))
  1690. def test_2138(examine: str):
  1691. """
  1692. gvpr splitting and tokenizing should not result in trailing garbage
  1693. https://gitlab.com/graphviz/graphviz/-/issues/2138
  1694. """
  1695. # find our co-located GVPR program
  1696. script = (Path(__file__).parent / "2138.gvpr").resolve()
  1697. assert script.exists(), "missing test case"
  1698. # run it with NUL input
  1699. out = subprocess.check_output(["gvpr", "-f", script], stdin=subprocess.DEVNULL)
  1700. # Decode into text. We do this instead of `universal_newlines=True` above
  1701. # because the trailing garbage can contain invalid UTF-8 data causing cryptic
  1702. # failures. We want to correctly surface this as trailing garbage, not an
  1703. # obscure UTF-8 decoding error.
  1704. result = out.decode("utf-8", "replace")
  1705. if examine == "indices":
  1706. # check no indices are miscalculated
  1707. index_re = (
  1708. r"^// index of space \(st\) :\s*(?P<index>-?\d+)\s*<< must "
  1709. r"NOT be less than -1$"
  1710. )
  1711. for m in re.finditer(index_re, result, flags=re.MULTILINE):
  1712. index = int(m.group("index"))
  1713. assert index >= -1, "illegal index computed"
  1714. if examine == "tokens":
  1715. # check for text the author of 2138.gvpr expected to find
  1716. assert (
  1717. "// tok[3] >>3456789<< should NOT include trailing spaces or "
  1718. "junk chars" in result
  1719. ), "token 3456789 not found or has trailing garbage"
  1720. assert (
  1721. "// tok[7] >>012<< should NOT include trailing spaces or "
  1722. "junk chars" in result
  1723. ), "token 012 not found or has trailing garbage"
  1724. def test_2159():
  1725. """
  1726. space for HTML TDs should be allocated equally when expanding to fill a TR
  1727. https://gitlab.com/graphviz/graphviz/-/issues/2159
  1728. """
  1729. # locate our associated test case in this directory
  1730. input = Path(__file__).parent / "2159.dot"
  1731. assert input.exists(), "unexpectedly missing test case"
  1732. # translate this to SVG
  1733. svg = dot("svg", input)
  1734. # load it as XML
  1735. root = ET.fromstring(svg)
  1736. # the first node is expected to contain:
  1737. # • 1 polygon for the top column-spanning cell
  1738. # • 5 polygons for the bottom row’s cells
  1739. # • 1 polygon for the outer table border
  1740. polygons = root.findall(
  1741. ".//{http://www.w3.org/2000/svg}title[.='node1']/../{http://www.w3.org/2000/svg}polygon"
  1742. )
  1743. assert len(polygons) == 7
  1744. # extract the points delimiting the top row
  1745. top_row = polygons[0]
  1746. points = [
  1747. [float(n) for n in p.split(",")] for p in top_row.get("points").split(" ")
  1748. ]
  1749. assert len(points) == 5, "polygon not rectangular"
  1750. (ul_x, _), (ll_x, _), (lr_x, _), (ur_x, _), (orig_x, _) = points
  1751. assert ul_x == ll_x, "polygon left edge is not vertical"
  1752. assert lr_x == ur_x, "polygon right edge is not vertical"
  1753. assert orig_x == ul_x, "polygon is not closed"
  1754. left = ul_x
  1755. right = ur_x
  1756. # extract the points for each cell in the bottom row
  1757. bottom_row = []
  1758. for cell in polygons[1:-1]:
  1759. bottom_row += [
  1760. [[float(n) for n in p.split(",")] for p in cell.get("points").split(" ")]
  1761. ]
  1762. assert len(bottom_row[-1]) == 5, "polygon not rectangular"
  1763. # extract the widths of each cell in the bottom row
  1764. widths = []
  1765. for cell in bottom_row:
  1766. (ul_x, _), _, _, (ur_x, _), _ = cell
  1767. widths += [ur_x - ul_x]
  1768. # these should approximately sum to the width of the top row
  1769. assert math.isclose(
  1770. sum(widths), right - left, abs_tol=10
  1771. ), "bottom row not expanded to fill the space"
  1772. # the width of each cell should be approximately equal
  1773. for width in widths[1:]:
  1774. assert math.isclose(width, widths[0], abs_tol=5), "cells not evenly expanded"
  1775. def test_2168():
  1776. """
  1777. using spline routing should not cause fdp/neato to infinite loop
  1778. https://gitlab.com/graphviz/graphviz/-/issues/2168
  1779. """
  1780. # locate our associated test case in this directory
  1781. input = Path(__file__).parent / "2168.dot"
  1782. assert input.exists(), "unexpectedly missing test case"
  1783. subprocess.check_call(["fdp", "-o", os.devnull, input], timeout=5)
  1784. def test_2168_1():
  1785. """
  1786. using spline routing should not cause fdp/neato to infinite loop
  1787. https://gitlab.com/graphviz/graphviz/-/issues/2168
  1788. """
  1789. # locate our associated test case in this directory
  1790. input = Path(__file__).parent / "2168_1.dot"
  1791. assert input.exists(), "unexpectedly missing test case"
  1792. subprocess.check_call(["fdp", "-o", os.devnull, input], timeout=5)
  1793. def test_2168_2():
  1794. """
  1795. using spline routing should not cause fdp/neato to infinite loop
  1796. https://gitlab.com/graphviz/graphviz/-/issues/2168
  1797. """
  1798. # locate our associated test case in this directory
  1799. input = Path(__file__).parent / "2168_2.dot"
  1800. assert input.exists(), "unexpectedly missing test case"
  1801. subprocess.check_call(["fdp", "-o", os.devnull, input], timeout=5)
  1802. def test_2168_3():
  1803. """
  1804. using spline routing should not cause fdp/neato to infinite loop
  1805. https://gitlab.com/graphviz/graphviz/-/issues/2168
  1806. """
  1807. # locate our associated test case in this directory
  1808. input = Path(__file__).parent / "2168_3.dot"
  1809. assert input.exists(), "unexpectedly missing test case"
  1810. subprocess.check_call(["fdp", "-o", os.devnull, input], timeout=5)
  1811. def test_2168_4():
  1812. """
  1813. using spline routing should not cause fdp/neato to infinite loop
  1814. https://gitlab.com/graphviz/graphviz/-/issues/2168
  1815. """
  1816. # locate our associated test case in this directory
  1817. input = Path(__file__).parent / "2168_4.dot"
  1818. assert input.exists(), "unexpectedly missing test case"
  1819. subprocess.check_call(["fdp", "-o", os.devnull, input], timeout=5)
  1820. def test_2168_5():
  1821. """
  1822. using spline routing should not cause fdp/neato to infinite loop
  1823. https://gitlab.com/graphviz/graphviz/-/issues/2168
  1824. """
  1825. # locate our associated test case in this directory
  1826. input = Path(__file__).parent / "2168_5.dot"
  1827. assert input.exists(), "unexpectedly missing test case"
  1828. out = subprocess.check_output(
  1829. ["fdp", "-o", os.devnull, input],
  1830. stderr=subprocess.STDOUT,
  1831. universal_newlines=True,
  1832. )
  1833. assert (
  1834. "Warning: the bounding boxes of some nodes touch - falling back to straight line edges"
  1835. in out
  1836. )
  1837. def test_2179():
  1838. """
  1839. processing a label with an empty line should not yield a warning
  1840. https://gitlab.com/graphviz/graphviz/-/issues/2179
  1841. """
  1842. # a graph containing a label with an empty line
  1843. input = 'digraph "" {\n 0 -> 1 [fontname="Lato",label=<<br/>1>]\n}'
  1844. # run a graph with an empty label through Graphviz
  1845. with subprocess.Popen(
  1846. ["dot", "-Tsvg", "-o", os.devnull],
  1847. stdin=subprocess.PIPE,
  1848. stderr=subprocess.PIPE,
  1849. universal_newlines=True,
  1850. ) as p:
  1851. _, stderr = p.communicate(input)
  1852. assert p.returncode == 0
  1853. assert (
  1854. "Warning: no hard-coded metrics for" not in stderr
  1855. ), "incorrect warning triggered"
  1856. def test_2179_1():
  1857. """
  1858. processing a label with a line containing only a space should not yield a
  1859. warning
  1860. https://gitlab.com/graphviz/graphviz/-/issues/2179
  1861. """
  1862. # a graph containing a label with a line containing only a space
  1863. input = 'digraph "" {\n 0 -> 1 [fontname="Lato",label=< <br/>1>]\n}'
  1864. # run a graph with an empty label through Graphviz
  1865. with subprocess.Popen(
  1866. ["dot", "-Tsvg", "-o", os.devnull],
  1867. stdin=subprocess.PIPE,
  1868. stderr=subprocess.PIPE,
  1869. universal_newlines=True,
  1870. ) as p:
  1871. _, stderr = p.communicate(input)
  1872. assert p.returncode == 0
  1873. assert (
  1874. "Warning: no hard-coded metrics for" not in stderr
  1875. ), "incorrect warning triggered"
  1876. def test_2183():
  1877. """
  1878. processing `splines=ortho`, `concentrate=true` should not crash
  1879. https://gitlab.com/graphviz/graphviz/-/issues/2183
  1880. """
  1881. # locate our associated test case in this directory
  1882. input = Path(__file__).parent / "2183.dot"
  1883. assert input.exists(), "unexpectedly missing test case"
  1884. subprocess.check_call(["dot", "-Tsvg", "-G8.5,11!", "-o", os.devnull, input])
  1885. @pytest.mark.skipif(which("nop") is None, reason="nop not available")
  1886. def test_2184_1():
  1887. """
  1888. nop should not reposition labelled graph nodes
  1889. https://gitlab.com/graphviz/graphviz/-/issues/2184
  1890. """
  1891. # run `nop` on a sample with a labelled graph node at the end
  1892. source = Path(__file__).parent / "2184.dot"
  1893. assert source.exists(), "missing test case"
  1894. nopped = subprocess.check_output(["nop", source], universal_newlines=True)
  1895. # the normalized output should have a graph with no label within
  1896. # `clusterSurround1`
  1897. m = re.search(
  1898. r"\bclusterSurround1\b.*\bgraph\b.*\bcluster1\b", nopped, flags=re.DOTALL
  1899. )
  1900. assert m is not None, "nop rearranged a graph in a not-semantically-preserving way"
  1901. def test_2184_2():
  1902. """
  1903. canonicalization should not reposition labelled graph nodes
  1904. https://gitlab.com/graphviz/graphviz/-/issues/2184
  1905. """
  1906. # canonicalize a sample with a labelled graph node at the end
  1907. source = Path(__file__).parent / "2184.dot"
  1908. assert source.exists(), "missing test case"
  1909. canonicalized = dot("canon", source)
  1910. # the canonicalized output should have a graph with no label within
  1911. # `clusterSurround1`
  1912. m = re.search(
  1913. r"\bclusterSurround1\b.*\bgraph\b.*\bcluster1\b", canonicalized, flags=re.DOTALL
  1914. )
  1915. assert (
  1916. m is not None
  1917. ), "`dot -Tcanon` rearranged a graph in a not-semantically-preserving way"
  1918. def test_2185_1():
  1919. """
  1920. GVPR should deal with strings correctly
  1921. https://gitlab.com/graphviz/graphviz/-/issues/2185
  1922. """
  1923. # find our collocated GVPR program
  1924. script = Path(__file__).parent / "2185.gvpr"
  1925. assert script.exists(), "missing test case"
  1926. # run this with NUL input, checking output is valid UTF-8
  1927. gvpr(script)
  1928. def test_2185_2():
  1929. """
  1930. GVPR should deal with strings correctly
  1931. https://gitlab.com/graphviz/graphviz/-/issues/2185
  1932. """
  1933. # find our collocated GVPR program
  1934. script = Path(__file__).parent / "2185.gvpr"
  1935. assert script.exists(), "missing test case"
  1936. # run this with NUL input
  1937. out = subprocess.check_output(["gvpr", "-f", script], stdin=subprocess.DEVNULL)
  1938. # decode output in a separate step to gracefully cope with garbage unicode
  1939. out = out.decode("utf-8", "replace")
  1940. # deal with Windows eccentricities
  1941. eol = "\r\n" if platform.system() == "Windows" else "\n"
  1942. expected = f"one two three{eol}"
  1943. # check the first line is as expected
  1944. assert out.startswith(expected), "incorrect GVPR interpretation"
  1945. def test_2185_3():
  1946. """
  1947. GVPR should deal with strings correctly
  1948. https://gitlab.com/graphviz/graphviz/-/issues/2185
  1949. """
  1950. # find our collocated GVPR program
  1951. script = Path(__file__).parent / "2185.gvpr"
  1952. assert script.exists(), "missing test case"
  1953. # run this with NUL input
  1954. out = subprocess.check_output(["gvpr", "-f", script], stdin=subprocess.DEVNULL)
  1955. # decode output in a separate step to gracefully cope with garbage unicode
  1956. out = out.decode("utf-8", "replace")
  1957. # deal with Windows eccentricities
  1958. eol = "\r\n" if platform.system() == "Windows" else "\n"
  1959. expected = f"one two three{eol}one five three{eol}"
  1960. # check the first two lines are as expected
  1961. assert out.startswith(expected), "incorrect GVPR interpretation"
  1962. def test_2185_4():
  1963. """
  1964. GVPR should deal with strings correctly
  1965. https://gitlab.com/graphviz/graphviz/-/issues/2185
  1966. """
  1967. # find our collocated GVPR program
  1968. script = Path(__file__).parent / "2185.gvpr"
  1969. assert script.exists(), "missing test case"
  1970. # run this with NUL input
  1971. out = subprocess.check_output(["gvpr", "-f", script], stdin=subprocess.DEVNULL)
  1972. # decode output in a separate step to gracefully cope with garbage unicode
  1973. out = out.decode("utf-8", "replace")
  1974. # deal with Windows eccentricities
  1975. eol = "\r\n" if platform.system() == "Windows" else "\n"
  1976. expected = f"one two three{eol}one five three{eol}99{eol}"
  1977. # check the first three lines are as expected
  1978. assert out.startswith(expected), "incorrect GVPR interpretation"
  1979. def test_2185_5():
  1980. """
  1981. GVPR should deal with strings correctly
  1982. https://gitlab.com/graphviz/graphviz/-/issues/2185
  1983. """
  1984. # find our collocated GVPR program
  1985. script = Path(__file__).parent / "2185.gvpr"
  1986. assert script.exists(), "missing test case"
  1987. # run this with NUL input
  1988. out = subprocess.check_output(["gvpr", "-f", script], stdin=subprocess.DEVNULL)
  1989. # decode output in a separate step to gracefully cope with garbage unicode
  1990. out = out.decode("utf-8", "replace")
  1991. # deal with Windows eccentricities
  1992. eol = "\r\n" if platform.system() == "Windows" else "\n"
  1993. expected = f"one two three{eol}one five three{eol}99{eol}Constant{eol}"
  1994. # check the first four lines are as expected
  1995. assert out.startswith(expected), "incorrect GVPR interpretation"
  1996. @pytest.mark.xfail(strict=True) # FIXME
  1997. def test_2193():
  1998. """
  1999. the canonical format should be stable
  2000. https://gitlab.com/graphviz/graphviz/-/issues/2193
  2001. """
  2002. # find our collocated test case
  2003. input = Path(__file__).parent / "2193.dot"
  2004. assert input.exists(), "unexpectedly missing test case"
  2005. # derive the initial canonicalization
  2006. canonical = dot("canon", input)
  2007. # now canonicalize this again to see if it changes
  2008. new = dot("canon", source=canonical)
  2009. assert canonical == new, "canonical translation is not stable"
  2010. @pytest.mark.skipif(which("gvpr") is None, reason="GVPR not available")
  2011. def test_2211():
  2012. """
  2013. GVPR’s `index` function should return correct results
  2014. https://gitlab.com/graphviz/graphviz/-/issues/2211
  2015. """
  2016. # find our collocated test case
  2017. program = Path(__file__).parent / "2211.gvpr"
  2018. assert program.exists(), "unexpectedly missing test case"
  2019. # run it through GVPR
  2020. output = gvpr(program)
  2021. # it should have found the right string indices for characters
  2022. assert (
  2023. output == "index: 9 should be 9\n"
  2024. "index: 3 should be 3\n"
  2025. "index: -1 should be -1\n"
  2026. )
  2027. def test_2215():
  2028. """
  2029. Graphviz should not crash with `-v`
  2030. https://gitlab.com/graphviz/graphviz/-/issues/2215
  2031. """
  2032. # try it on a simple graph
  2033. input = "graph g { a -- b; }"
  2034. subprocess.run(["dot", "-v"], input=input, check=True, universal_newlines=True)
  2035. # try the same on a labelled version of this graph
  2036. input = 'graph g { node[label=""] a -- b; }'
  2037. subprocess.run(["dot", "-v"], input=input, check=True, universal_newlines=True)
  2038. @pytest.mark.xfail(
  2039. is_rocky(),
  2040. strict=True,
  2041. reason="https://gitlab.com/graphviz/graphviz/-/issues/2241",
  2042. )
  2043. def test_2241():
  2044. """
  2045. a graph with two nodes and one edge in each direction should be rendered
  2046. with two visually distinct edges when using the neato engine and
  2047. splines=true, not two edges on top of each other, visually looking like a
  2048. single edge with both head and tail arrowheads.
  2049. https://gitlab.com/graphviz/graphviz/-/issues/2241
  2050. """
  2051. # find our collocated test case
  2052. input = Path(__file__).parent / "2241.dot"
  2053. assert input.exists(), "unexpectedly missing test case"
  2054. # run it through Graphviz
  2055. svg = dot("svg", input)
  2056. # load this as XML
  2057. root = ET.fromstring(svg)
  2058. # the output is expected to contain two paths which are well separated
  2059. paths = root.findall(".//{http://www.w3.org/2000/svg}path")
  2060. assert len(paths) == 2, "expected two paths in output"
  2061. ellipses = root.findall(".//{http://www.w3.org/2000/svg}ellipse")
  2062. assert len(ellipses) == 2, "expected two ellipses in output"
  2063. # calculate the x coordinate of a vertical line which is equidistant from the two nodes
  2064. x = statistics.mean(float(ellipse.get("cx")) for ellipse in ellipses)
  2065. # for each edge path, get the y coordinate of a point on a line between the edge's endpoints
  2066. # where the line intersects the node equidistant vertical line
  2067. y_coordinates = []
  2068. for path in paths:
  2069. d_attribute = path.get("d")
  2070. points_str = re.split("[ C]", d_attribute.replace("M", ""))
  2071. assert (
  2072. len(points_str) == 4
  2073. ), "expected four points in the 'd' attribute of the 'path' element"
  2074. points = [
  2075. (float(x_str), float(y_str))
  2076. for x_str, y_str in [point_str.split(",") for point_str in points_str]
  2077. ]
  2078. dx = points[3][0] - points[0][0]
  2079. dy = points[3][1] - points[0][1]
  2080. y = points[0][1] + dy / dx * (x - points[0][0])
  2081. y_coordinates.append(y)
  2082. # check that the lines are well separated vertically where they intersect the node equidistant
  2083. # vertical line
  2084. y_coordinates_abs_difference = abs(y_coordinates[1] - y_coordinates[0])
  2085. y_coordinates_abs_difference_when_ok = 11.5004437538844
  2086. y_coordinates_abs_difference_when_not_ok = 0.00658290568043185
  2087. min_y_coordinates_abs_difference = (
  2088. y_coordinates_abs_difference_when_ok + y_coordinates_abs_difference_when_not_ok
  2089. ) / 2
  2090. assert y_coordinates_abs_difference > min_y_coordinates_abs_difference
  2091. def test_2242():
  2092. """
  2093. repeated runs of a graph with subgraphs should yield a stable result
  2094. https://gitlab.com/graphviz/graphviz/-/issues/2242
  2095. """
  2096. # get our baseline reference
  2097. input = Path(__file__).parent / "2242.dot"
  2098. assert input.exists(), "unexpectedly missing test case"
  2099. ref = dot("png", input)
  2100. # now repeat this, expecting it not to change
  2101. for _ in range(20):
  2102. png = dot("png", input)
  2103. assert ref == png, "repeated rendering changed output"
  2104. def test_2342():
  2105. """
  2106. using an arrow with size 0 should not trigger an assertion failure
  2107. https://gitlab.com/graphviz/graphviz/-/issues/2342
  2108. """
  2109. # find our collocated test case
  2110. input = Path(__file__).parent / "2342.dot"
  2111. assert input.exists(), "unexpectedly missing test case"
  2112. # run it through Graphviz
  2113. dot("svg", input)
  2114. @pytest.mark.skipif(
  2115. is_static_build(),
  2116. reason="dynamic libraries are unavailable to link against in static builds",
  2117. )
  2118. def test_2356():
  2119. """
  2120. Using `mindist` programmatically in a loop should not cause Windows crashes
  2121. https://gitlab.com/graphviz/graphviz/-/issues/2356
  2122. """
  2123. # find co-located test source
  2124. c_src = (Path(__file__).parent / "2356.c").resolve()
  2125. assert c_src.exists(), "missing test case"
  2126. # run the test
  2127. run_c(c_src, link=["cgraph", "gvc"])
  2128. def test_2361():
  2129. """
  2130. using `ortho` and `concentrate` in combination should not cause a crash
  2131. https://gitlab.com/graphviz/graphviz/-/issues/2361
  2132. """
  2133. # find our collocated test case
  2134. input = Path(__file__).parent / "2361.dot"
  2135. assert input.exists(), "unexpectedly missing test case"
  2136. # run it through Graphviz
  2137. dot("png", input)
  2138. @pytest.mark.xfail(
  2139. strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/2295"
  2140. )
  2141. def test_2295():
  2142. """
  2143. tooltips should work in PDFs
  2144. https://gitlab.com/graphviz/graphviz/-/issues/2295
  2145. """
  2146. # find our collocated test case
  2147. input = Path(__file__).parent / "2295.dot"
  2148. assert input.exists(), "unexpectedly missing test case"
  2149. # translate it to PDF
  2150. pdf = dot("pdf", input)
  2151. assert re.search(rb"\bhi mom\b", pdf) is not None, "tooltip not propagated to PDF"
  2152. @pytest.mark.parametrize("arg", ("--filepath", "-Gimagepath"))
  2153. def test_2396(arg: str):
  2154. """
  2155. `--filepath` should work as a replacement for `$GV_FILE_PATH`
  2156. https://gitlab.com/graphviz/graphviz/-/issues/2396
  2157. """
  2158. # use an arbitrary image we have in the tree
  2159. image = Path(__file__).parent / "../cmd/gvedit/images/save.png"
  2160. assert image.exists(), "missing test data"
  2161. # a graph that tries to use the image by relative path
  2162. slash = "/" if arg == "--filepath" else ""
  2163. source = f'graph {{ N[image="{slash}save.png"]; }}'
  2164. # run this through Graphviz
  2165. proc = subprocess.run(
  2166. ["dot", "-Tsvg", f"{arg}={image.parent}"],
  2167. stdout=subprocess.PIPE,
  2168. stderr=subprocess.PIPE,
  2169. input=source,
  2170. cwd=Path(__file__).parent,
  2171. universal_newlines=True,
  2172. check=True,
  2173. )
  2174. # work around macOS warnings
  2175. stderr = remove_xtype_warnings(proc.stderr).strip()
  2176. # work around ASan informational printing
  2177. stderr = remove_asan_summary(stderr)
  2178. assert stderr == "", "loading an image by relative path produced warnings"
  2179. # whether we used `imagepath` or `filepath` should affect whether we get a leading
  2180. # slash
  2181. if arg == "-Gimagepath":
  2182. assert '"save.png"' in proc.stdout, "incorrect relative path in output"
  2183. else:
  2184. assert '"/save.png"' in proc.stdout, "incorrect relative path in output"
  2185. def test_2481():
  2186. """
  2187. `dot` should not exit with a syntax error if keywords are mixed-case
  2188. https://gitlab.com/graphviz/graphviz/-/issues/2481
  2189. """
  2190. # try a simple graph with uppercase characters in 'digraph'
  2191. input = "diGraph { }"
  2192. # ensure this does not trigger warnings
  2193. with subprocess.Popen(
  2194. ["dot"],
  2195. stdin=subprocess.PIPE,
  2196. stderr=subprocess.PIPE,
  2197. universal_newlines=True,
  2198. ) as p:
  2199. _, stderr = p.communicate(input)
  2200. assert p.returncode == 0, "mixed-case keyword was rejected"
  2201. assert "syntax error" not in stderr, "dot displayed a syntax error message"
  2202. @pytest.mark.skipif(
  2203. is_static_build(),
  2204. reason="dynamic libraries are unavailable to link against in static builds",
  2205. )
  2206. def test_2484():
  2207. """
  2208. Graphviz context should not preserve state across calls
  2209. https://gitlab.com/graphviz/graphviz/-/issues/2484
  2210. """
  2211. # find our co-located driver
  2212. c_src = (Path(__file__).parent / "2484.c").resolve()
  2213. assert c_src.exists(), "missing test case"
  2214. # find co-located input to the driver
  2215. dot_src = (Path(__file__).parent / "2484.dot").resolve()
  2216. assert dot_src.exists(), "missing test case"
  2217. # compile and run it
  2218. run_c(
  2219. c_src,
  2220. ["-Kdot", "-Tpng", str(dot_src), "-o", os.devnull],
  2221. link=["cgraph", "gvc"],
  2222. )
  2223. @pytest.mark.xfail(
  2224. strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/2592"
  2225. )
  2226. def test_2592():
  2227. """
  2228. pack modes should not remove xlabels
  2229. https://gitlab.com/graphviz/graphviz/-/issues/2592
  2230. """
  2231. # find our collocated test case
  2232. input = Path(__file__).parent / "2592.dot"
  2233. assert input.exists(), "unexpectedly missing test case"
  2234. # run it through Graphviz
  2235. svg = dot("svg", input)
  2236. assert "comment not included" in svg, "missing xlabel in packed graph"
  2237. def test_package_version():
  2238. """
  2239. The graphviz_version.h header should define a non-empty PACKAGE_VERSION
  2240. """
  2241. # find co-located test source
  2242. c_src = (Path(__file__).parent / "get-package-version.c").resolve()
  2243. assert c_src.exists(), "missing test case"
  2244. # run the test
  2245. package_version, _ = run_c(c_src)
  2246. assert (
  2247. package_version.strip() != ""
  2248. ), "invalid PACKAGE_VERSION in graphviz_version.h"
  2249. def test_user_shapes():
  2250. """
  2251. Graphviz should understand how to embed a custom SVG image as a node’s shape
  2252. """
  2253. # find our collocated test case
  2254. input = Path(__file__).parent / "usershape.dot"
  2255. assert input.exists(), "unexpectedly missing test case"
  2256. # ask Graphviz to translate this to SVG
  2257. output = subprocess.check_output(
  2258. ["dot", "-Tsvg", input], cwd=os.path.dirname(__file__), universal_newlines=True
  2259. )
  2260. # the external SVG should have been parsed and is now referenced
  2261. assert '<image xlink:href="usershape.svg" width="62px" height="44px" ' in output
  2262. def test_xdot_json():
  2263. """
  2264. check the output of xdot’s JSON API
  2265. """
  2266. # find our collocated C helper
  2267. c_src = Path(__file__).parent / "xdot2json.c"
  2268. # some valid xdot commands to process
  2269. input = "c 9 -#fffffe00 C 7 -#ffffff P 4 0 0 0 36 54 36 54 0"
  2270. # ask our C helper to process this
  2271. output, err = run_c(c_src, input=input, link=["xdot"])
  2272. assert err == ""
  2273. # confirm the output was what we expected
  2274. data = json.loads(output)
  2275. assert data == [
  2276. {"c": "#fffffe00"},
  2277. {"C": "#ffffff"},
  2278. {"P": [0.0, 0.0, 0.0, 36.0, 54.0, 36.0, 54.0, 0.0]},
  2279. ]
  2280. @pytest.mark.skipif(which("gvmap") is None, reason="gvmap not available")
  2281. def test_gvmap_fclose():
  2282. """
  2283. gvmap should not attempt to fclose(NULL). This example will trigger a crash if
  2284. this bug has been reintroduced and Graphviz is built with ASan support.
  2285. """
  2286. # a reasonable input graph
  2287. input = (
  2288. 'graph "Alík: Na vlastní oči" {\n'
  2289. ' graph [bb="0,0,128.9,36",\n'
  2290. " concentrate=true,\n"
  2291. " overlap=prism,\n"
  2292. " start=3\n"
  2293. " ];\n"
  2294. ' node [label="\\N"];\n'
  2295. " {\n"
  2296. " bob [height=0.5,\n"
  2297. ' pos="100.95,18",\n'
  2298. " width=0.77632];\n"
  2299. " }\n"
  2300. " {\n"
  2301. " alice [height=0.5,\n"
  2302. ' pos="32.497,18",\n'
  2303. " width=0.9027];\n"
  2304. " }\n"
  2305. ' alice -- bob [pos="65.119,18 67.736,18 70.366,18 72.946,18"];\n'
  2306. " bob -- alice;\n"
  2307. "}"
  2308. )
  2309. # pass this through gvmap
  2310. proc = subprocess.run(["gvmap"], input=input.encode("utf-8"))
  2311. assert proc.returncode in (0, 1), "gvmap crashed"
  2312. @pytest.mark.skipif(which("gvpr") is None, reason="gvpr not available")
  2313. def test_gvpr_usage():
  2314. """
  2315. gvpr usage information should be included when erroring on a malformed command
  2316. """
  2317. # create a temporary directory, under which we know no files will exist
  2318. with tempfile.TemporaryDirectory() as tmp:
  2319. # ask GVPR to process a non-existent file
  2320. with subprocess.Popen(
  2321. ["gvpr", "-f", "nofile"],
  2322. stderr=subprocess.PIPE,
  2323. cwd=tmp,
  2324. universal_newlines=True,
  2325. ) as p:
  2326. _, stderr = p.communicate()
  2327. assert p.returncode != 0, "GVPR accepted a non-existent file"
  2328. # the stderr output should have contained full usage instructions
  2329. assert (
  2330. "-o <ofile> - write output to <ofile>; stdout by default" in stderr
  2331. ), "truncated or malformed GVPR usage information"
  2332. def test_2225():
  2333. """
  2334. sfdp should not segfault with curved splines
  2335. https://gitlab.com/graphviz/graphviz/-/issues/2225
  2336. """
  2337. # locate our associated test case in this directory
  2338. input = Path(__file__).parent / "2225.dot"
  2339. assert input.exists(), "unexpectedly missing test case"
  2340. # run this through sfdp
  2341. p = subprocess.run(
  2342. ["sfdp", "-Gsplines=curved", "-o", os.devnull, input],
  2343. stderr=subprocess.PIPE,
  2344. universal_newlines=True,
  2345. )
  2346. # if sfdp was built without libgts, it will not handle anything non-trivial
  2347. no_gts_error = "remove_overlap: Graphviz not built with triangulation library"
  2348. if no_gts_error in p.stderr:
  2349. assert p.returncode != 0, "sfdp returned success after an error message"
  2350. return
  2351. p.check_returncode()
  2352. def test_2257():
  2353. """
  2354. `$GV_FILE_PATH` being set should prevent Graphviz from running
  2355. `$GV_FILE_PATH` was an environment variable formerly used to implement a file
  2356. system sandboxing policy when Graphviz was exposed to the internet via a web
  2357. server. These days, there are safer and more robust techniques to sandbox
  2358. Graphviz and so `$GV_FILE_PATH` usage has been removed. But if someone
  2359. attempts to use this legacy mechanism, we do not want Graphviz to
  2360. “fail-open,” starting anyway and silently ignoring `$GV_FILE_PATH` giving
  2361. the user the false impression the sandboxing is in force.
  2362. https://gitlab.com/graphviz/graphviz/-/issues/2257
  2363. """
  2364. # locate our associated test case in this directory
  2365. input = Path(__file__).parent / "2257.dot"
  2366. assert input.exists(), "unexpectedly missing test case"
  2367. env = os.environ.copy()
  2368. env["GV_FILE_PATH"] = "/tmp"
  2369. # Graphviz should refuse to process an input file
  2370. with pytest.raises(subprocess.CalledProcessError):
  2371. subprocess.check_call(["dot", "-Tsvg", input, "-o", os.devnull], env=env)
  2372. def test_2258():
  2373. """
  2374. 'id' attribute should be propagated to all graph children in output
  2375. https://gitlab.com/graphviz/graphviz/-/issues/2258
  2376. """
  2377. # locate our associated test case in this directory
  2378. input = Path(__file__).parent / "2258.dot"
  2379. assert input.exists(), "unexpectedly missing test case"
  2380. # translate this to SVG
  2381. svg = dot("svg", input)
  2382. # load this as XML
  2383. root = ET.fromstring(svg)
  2384. # the output is expected to contain a number of linear gradients, all of which
  2385. # are semantic children of graph marked `id = "G2"`
  2386. gradients = root.findall(".//{http://www.w3.org/2000/svg}linearGradient")
  2387. assert len(gradients) > 0, "no gradients in output"
  2388. for gradient in gradients:
  2389. assert "G2" in gradient.get("id"), "ID was not applied to linear gradients"
  2390. def test_2270(tmp_path: Path):
  2391. """
  2392. `-O` should result in the expected output filename
  2393. https://gitlab.com/graphviz/graphviz/-/issues/2270
  2394. """
  2395. # write a simple graph
  2396. input = tmp_path / "hello.gv"
  2397. input.write_text("digraph { hello -> world }", encoding="utf-8")
  2398. # process it with Graphviz
  2399. subprocess.check_call(
  2400. ["dot", "-T", "plain:dot:core", "-O", "hello.gv"], cwd=tmp_path
  2401. )
  2402. # it should have produced output in the expected location
  2403. output = tmp_path / "hello.gv.core.dot.plain"
  2404. assert output.exists(), "-O resulted in an unexpected output filename"
  2405. @pytest.mark.skipif(
  2406. is_static_build(),
  2407. reason="dynamic libraries are unavailable to link against in static builds",
  2408. )
  2409. def test_2272():
  2410. """
  2411. using `agmemread` with an unterminated string should not fail assertions
  2412. https://gitlab.com/graphviz/graphviz/-/issues/2272
  2413. """
  2414. # find co-located test source
  2415. c_src = (Path(__file__).parent / "2272.c").resolve()
  2416. assert c_src.exists(), "missing test case"
  2417. # run the test
  2418. run_c(c_src, link=["cgraph", "gvc"])
  2419. def test_2272_2():
  2420. """
  2421. An unterminated string in the source should not crash Graphviz. Variant of
  2422. `test_2272`.
  2423. """
  2424. # a graph with an open string
  2425. graph = 'graph { a[label="abc'
  2426. # process it with Graphviz, which should not crash
  2427. p = subprocess.run(["dot", "-o", os.devnull], input=graph, universal_newlines=True)
  2428. assert p.returncode != 0, "dot accepted invalid input"
  2429. assert p.returncode == 1, "dot crashed"
  2430. def test_2282():
  2431. """
  2432. using the `fdp` layout with JSON output should result in valid JSON
  2433. https://gitlab.com/graphviz/graphviz/-/issues/2282
  2434. """
  2435. # locate our associated test case in this directory
  2436. input = Path(__file__).parent / "2282.dot"
  2437. assert input.exists(), "unexpectedly missing test case"
  2438. # translate this to JSON
  2439. output = dot("json", input)
  2440. # confirm this is valid JSON
  2441. json.loads(output)
  2442. def test_2283():
  2443. """
  2444. `beautify=true` should correctly space nodes
  2445. https://gitlab.com/graphviz/graphviz/-/issues/2283
  2446. """
  2447. # find our collocated test case
  2448. input = Path(__file__).parent / "2283.dot"
  2449. assert input.exists(), "unexpectedly missing test case"
  2450. # translate this to SVG
  2451. p = subprocess.run(
  2452. ["dot", "-Tsvg", input],
  2453. stdout=subprocess.PIPE,
  2454. stderr=subprocess.PIPE,
  2455. universal_newlines=True,
  2456. )
  2457. # if sfdp was built without libgts, it will not handle anything non-trivial
  2458. no_gts_error = "remove_overlap: Graphviz not built with triangulation library"
  2459. if no_gts_error in p.stderr:
  2460. assert p.returncode != 0, "sfdp returned success after an error message"
  2461. return
  2462. p.check_returncode()
  2463. svg = p.stdout
  2464. # parse this into something we can inspect
  2465. root = ET.fromstring(svg)
  2466. # find node N0
  2467. n0s = root.findall(
  2468. ".//{http://www.w3.org/2000/svg}title[.='N0']/../{http://www.w3.org/2000/svg}ellipse"
  2469. )
  2470. assert len(n0s) == 1, "failed to locate node N0"
  2471. n0 = n0s[0]
  2472. # find node N1
  2473. n1s = root.findall(
  2474. ".//{http://www.w3.org/2000/svg}title[.='N1']/../{http://www.w3.org/2000/svg}ellipse"
  2475. )
  2476. assert len(n1s) == 1, "failed to locate node N1"
  2477. n1 = n1s[0]
  2478. # find node N6
  2479. n6s = root.findall(
  2480. ".//{http://www.w3.org/2000/svg}title[.='N6']/../{http://www.w3.org/2000/svg}ellipse"
  2481. )
  2482. assert len(n6s) == 1, "failed to locate node N6"
  2483. n6 = n6s[0]
  2484. # N1 and N6 should not have been drawn on top of each other
  2485. n1_x = float(n1.attrib["cx"])
  2486. n1_y = float(n1.attrib["cy"])
  2487. n6_x = float(n6.attrib["cx"])
  2488. n6_y = float(n6.attrib["cy"])
  2489. def sameish(a: float, b: float) -> bool:
  2490. EPSILON = 0.2
  2491. return -EPSILON < abs(a - b) < EPSILON
  2492. assert not (
  2493. sameish(n1_x, n6_x) and sameish(n1_y, n6_y)
  2494. ), "N1 and N6 placed identically"
  2495. # use the Law of Cosines to compute the angle between N0→N1 and N0→N6
  2496. n0_x = float(n0.attrib["cx"])
  2497. n0_y = float(n0.attrib["cy"])
  2498. n0_n1_dist = math.dist((n0_x, n0_y), (n1_x, n1_y))
  2499. n0_n6_dist = math.dist((n0_x, n0_y), (n6_x, n6_y))
  2500. n1_n6_dist = math.dist((n1_x, n1_y), (n6_x, n6_y))
  2501. angle = math.acos(
  2502. (n0_n1_dist**2 + n0_n6_dist**2 - n1_n6_dist**2) / (2 * n0_n1_dist * n0_n6_dist)
  2503. )
  2504. number_of_radial_nodes = 6
  2505. assert sameish(
  2506. angle, 2 * math.pi / number_of_radial_nodes
  2507. ), "nodes not placed evenly"
  2508. def test_2285():
  2509. """
  2510. using the `svg_inline` output should result in SVG you can inline to HTML
  2511. https://gitlab.com/graphviz/graphviz/-/issues/2285
  2512. """
  2513. # locate our associated test case in this directory
  2514. input = Path(__file__).parent / "2285.dot"
  2515. assert input.exists(), "unexpectedly missing test case"
  2516. # translate this to JSON
  2517. output = dot("svg_inline", input)
  2518. assert "<?xml" not in output, "<?xml in output"
  2519. assert "<!DOCTYPE" not in output, "<?xml in output"
  2520. assert "xmlns" not in output, "xmlns in output"
  2521. assert "<svg" in output, "<svg not in output"
  2522. @pytest.mark.skipif(which("gxl2gv") is None, reason="gxl2gv not available")
  2523. def test_2300_1():
  2524. """
  2525. translating GXL with an attribute `name` should not crash
  2526. https://gitlab.com/graphviz/graphviz/-/issues/2300
  2527. """
  2528. # locate our associated test case containing a node attribute `name`
  2529. input = Path(__file__).parent / "2300.gxl"
  2530. assert input.exists(), "unexpectedly missing test case"
  2531. # ask `gxl2gv` to process this
  2532. subprocess.check_call(["gxl2gv", input])
  2533. def test_2307():
  2534. """
  2535. 'id' attribute should be propagated to 'url' links in SVG output
  2536. https://gitlab.com/graphviz/graphviz/-/issues/2307
  2537. """
  2538. # locate our associated test case in this directory
  2539. input = Path(__file__).parent / "2258.dot"
  2540. assert input.exists(), "unexpectedly missing test case"
  2541. # translate this to SVG
  2542. svg = dot("svg", input)
  2543. # load this as XML
  2544. root = ET.fromstring(svg)
  2545. # the output is expected to contain a number of polygons, any of which have
  2546. # `url` fills should include the ID “G2”
  2547. polygons = root.findall(".//{http://www.w3.org/2000/svg}polygon")
  2548. assert len(polygons) > 0, "no polygons in output"
  2549. for polygon in polygons:
  2550. m = re.match(r"url\((?P<url>.*)\)$", polygon.get("fill"))
  2551. if m is None:
  2552. continue
  2553. assert (
  2554. re.search(r"\bG2_", m.group("url")) is not None
  2555. ), "ID G2 was not applied to polygon fill url"
  2556. def test_2325():
  2557. """
  2558. using more than 63 styles and/or more than 128 style bytes should not trigger
  2559. an out-of-bounds memory read
  2560. https://gitlab.com/graphviz/graphviz/-/issues/2325
  2561. """
  2562. # locate our associated test case in this directory
  2563. input = Path(__file__).parent / "2325.dot"
  2564. assert input.exists(), "unexpectedly missing test case"
  2565. # run it through Graphviz
  2566. dot("svg", input)
  2567. @pytest.mark.skipif(shutil.which("groff") is None, reason="groff not available")
  2568. def test_2341():
  2569. """
  2570. PIC backend should generate correct comments
  2571. https://gitlab.com/graphviz/graphviz/-/issues/2341
  2572. """
  2573. # a simple graph
  2574. source = "digraph { a -> b; }"
  2575. # generate PIC from this
  2576. pic = dot("pic", source=source)
  2577. # run this through groff
  2578. groffed = subprocess.check_output(
  2579. ["groff", "-Tascii", "-p"], input=pic, universal_newlines=True
  2580. )
  2581. # it should not contain any comments
  2582. assert (
  2583. re.search(r"^\s*#", groffed) is None
  2584. ), "Graphviz comment remains in groff output"
  2585. def test_2352():
  2586. """
  2587. referencing an all-one-line external SVG file should work
  2588. https://gitlab.com/graphviz/graphviz/-/issues/2352
  2589. """
  2590. # locate our associated test case in this directory
  2591. input = Path(__file__).parent / "2352.dot"
  2592. assert input.exists(), "unexpectedly missing test case"
  2593. # translate it to SVG
  2594. svg = subprocess.check_output(
  2595. ["dot", "-Tsvg", input], cwd=Path(__file__).parent, universal_newlines=True
  2596. )
  2597. assert '<image xlink:href="EDA.svg" ' in svg, "external file reference missing"
  2598. def test_2352_1():
  2599. """
  2600. variant of 2352 with a leading space in front of `<svg`
  2601. https://gitlab.com/graphviz/graphviz/-/issues/2352
  2602. """
  2603. # locate our associated test case in this directory
  2604. input = Path(__file__).parent / "2352_1.dot"
  2605. assert input.exists(), "unexpectedly missing test case"
  2606. # translate it to SVG
  2607. svg = subprocess.check_output(
  2608. ["dot", "-Tsvg", input], cwd=Path(__file__).parent, universal_newlines=True
  2609. )
  2610. assert '<image xlink:href="EDA_1.svg" ' in svg, "external file reference missing"
  2611. def test_2352_2():
  2612. """
  2613. variant of 2352 that spaces viewBox such that it is on a 200-character line
  2614. boundary
  2615. https://gitlab.com/graphviz/graphviz/-/issues/2352
  2616. """
  2617. # locate our associated test case in this directory
  2618. input = Path(__file__).parent / "2352_2.dot"
  2619. assert input.exists(), "unexpectedly missing test case"
  2620. # translate it to SVG
  2621. svg = subprocess.check_output(
  2622. ["dot", "-Tsvg", input], cwd=Path(__file__).parent, universal_newlines=True
  2623. )
  2624. assert '<image xlink:href="EDA_2.svg" ' in svg, "external file reference missing"
  2625. def test_2355():
  2626. """
  2627. Using >127 layers should not crash Graphviz
  2628. https://gitlab.com/graphviz/graphviz/-/issues/2355
  2629. """
  2630. # construct a graph with 128 layers
  2631. graph = io.StringIO()
  2632. graph.write("digraph {\n")
  2633. layers = ":".join(f"l{i}" for i in range(128))
  2634. graph.write(f' layers="{layers}";\n')
  2635. for i in range(128):
  2636. graph.write(f' n{i}[layer="l{i}"];\n')
  2637. graph.write("}\n")
  2638. # process this with dot
  2639. dot("svg", source=graph.getvalue())
  2640. @pytest.mark.xfail(strict=True) # FIXME
  2641. def test_2368():
  2642. """
  2643. routesplines should not corrupt its `prev` and `next` indices
  2644. https://gitlab.com/graphviz/graphviz/-/issues/2368
  2645. """
  2646. # locate our associated test case in this directory
  2647. input = Path(__file__).parent / "2368.dot"
  2648. assert input.exists(), "unexpectedly missing test case"
  2649. # run it through Graphviz
  2650. dot("svg", input)
  2651. @pytest.mark.skipif(shutil.which("tclsh") is None, reason="tclsh not available")
  2652. def test_2370():
  2653. """
  2654. tcldot should have a version number TCL accepts
  2655. https://gitlab.com/graphviz/graphviz/-/issues/2370
  2656. """
  2657. # if this appears to be an ASan-enabled CI job, teach `tclsh` to load ASan’s
  2658. # supporting library because it is otherwise unaware that Tcldot depends on this
  2659. # being loaded first
  2660. env = os.environ.copy()
  2661. dot_exe = which("dot")
  2662. if is_asan_instrumented(dot_exe):
  2663. cc = os.environ.get("CC", "gcc")
  2664. libasan = subprocess.check_output(
  2665. [cc, "-print-file-name=libasan.so"], universal_newlines=True
  2666. ).strip()
  2667. print(f"setting LD_PRELOAD={libasan}")
  2668. env["LD_PRELOAD"] = libasan
  2669. # ask TCL to import the Graphviz package
  2670. response = subprocess.check_output(
  2671. ["tclsh"],
  2672. stderr=subprocess.STDOUT,
  2673. input="package require Tcldot;",
  2674. universal_newlines=True,
  2675. env=env,
  2676. )
  2677. assert (
  2678. "error reading package index file" not in response
  2679. ), "tcldot cannot be loaded by TCL"
  2680. def test_2371():
  2681. """
  2682. Large graphs should not cause rectangle area calculation overflows
  2683. https://gitlab.com/graphviz/graphviz/-/issues/2371
  2684. """
  2685. # locate our associated test case in this directory
  2686. input = Path(__file__).parent / "2371.dot"
  2687. assert input.exists(), "unexpectedly missing test case"
  2688. # run it through Graphviz
  2689. subprocess.check_call(["dot", "-Tsvg", "-Knop2", "-o", os.devnull, input])
  2690. @pytest.mark.skipif(
  2691. platform.system() == "Windows",
  2692. reason="gvplugin_list symbol is not exposed on Windows",
  2693. )
  2694. def test_2375():
  2695. """
  2696. `gvplugin_list` should return full plugin names
  2697. https://gitlab.com/graphviz/graphviz/-/issues/2375
  2698. """
  2699. # find co-located test source
  2700. c_src = (Path(__file__).parent / "2375.c").resolve()
  2701. assert c_src.exists(), "missing test case"
  2702. # run the test
  2703. run_c(c_src, link=["gvc"])
  2704. def test_2377():
  2705. """
  2706. 3 letter hex color codes should be accepted
  2707. https://gitlab.com/graphviz/graphviz/-/issues/2377
  2708. """
  2709. # run some 6 letter color input through Graphviz
  2710. input = 'digraph { n [color="#cc0000" fillcolor="#ffcc00" style=filled] }'
  2711. svg1 = dot("svg", source=input)
  2712. # try the equivalent with 3 letter colors
  2713. input = 'digraph { n [color="#c00" fillcolor="#fc0" style=filled] }'
  2714. svg2 = dot("svg", source=input)
  2715. assert svg1 == svg2, "3 letter hex colors were not translated correctly"
  2716. def test_2390():
  2717. """
  2718. using an out of range `xdotversion` should not crash Graphviz
  2719. https://gitlab.com/graphviz/graphviz/-/issues/2390
  2720. """
  2721. # some input with an invalid large `xdotversion`
  2722. input = 'graph { xdotversion=99; n[label="hello world"]; }'
  2723. # run this through Graphviz
  2724. dot("xdot", source=input)
  2725. def test_2391():
  2726. """
  2727. `nslimit1=0` should not cause Graphviz to crash
  2728. https://gitlab.com/graphviz/graphviz/-/issues/2391
  2729. """
  2730. # locate our associated test case in this directory
  2731. input = Path(__file__).parent / "2391.dot"
  2732. assert input.exists(), "unexpectedly missing test case"
  2733. # run it through Graphviz
  2734. dot("svg", input)
  2735. def test_2391_1():
  2736. """
  2737. `nslimit1=0` with a label should not cause Graphviz to crash
  2738. https://gitlab.com/graphviz/graphviz/-/issues/2391
  2739. """
  2740. # locate our associated test case in this directory
  2741. input = Path(__file__).parent / "2391_1.dot"
  2742. assert input.exists(), "unexpectedly missing test case"
  2743. # run it through Graphviz
  2744. dot("svg", input)
  2745. @pytest.mark.xfail(
  2746. platform.system() == "Windows" and not is_mingw(),
  2747. reason="cannot link Agdirected on Windows",
  2748. strict=True,
  2749. ) # FIXME
  2750. def test_2397():
  2751. """
  2752. escapes in strings should be handled correctly
  2753. https://gitlab.com/graphviz/graphviz/-/issues/2397
  2754. """
  2755. # find co-located test source
  2756. c_src = (Path(__file__).parent / "2397.c").resolve()
  2757. assert c_src.exists(), "missing test case"
  2758. # run this to generate a graph
  2759. source, _ = run_c(c_src, link=["cgraph", "gvc"])
  2760. # this should have produced a valid graph
  2761. dot("svg", source=source)
  2762. def test_2397_1():
  2763. """
  2764. a variant of test_2397 that confirms the same works via the command line
  2765. https://gitlab.com/graphviz/graphviz/-/issues/2397
  2766. """
  2767. source = 'digraph { a[label="foo\\\\\\"bar"]; }'
  2768. # run this through dot
  2769. output = dot("dot", source=source)
  2770. # the output should be valid dot
  2771. dot("svg", source=output)
  2772. @pytest.mark.skipif(shutil.which("shellcheck") is None, reason="shellcheck unavailable")
  2773. def test_2404():
  2774. """
  2775. shell syntax used by gvmap should be correct
  2776. https://gitlab.com/graphviz/graphviz/-/issues/2404
  2777. """
  2778. gvmap_sh = Path(__file__).parent / "../cmd/gvmap/gvmap.sh"
  2779. subprocess.check_call(["shellcheck", "-S", "error", gvmap_sh])
  2780. def test_2406():
  2781. """
  2782. arrow types like `invdot` and `onormalonormal` should be displayed correctly
  2783. https://gitlab.com/graphviz/graphviz/-/issues/2406
  2784. """
  2785. # locate our associated test case in this directory
  2786. input = Path(__file__).parent / "2406.dot"
  2787. assert input.exists(), "unexpectedly missing test case"
  2788. # run it through Graphviz
  2789. output = dot("svg", input)
  2790. # the rounded hollows should be present
  2791. assert re.search(r"\bellipse\b", output), "missing element of invdot arrow"
  2792. @pytest.mark.parametrize("source", ("2413_1.dot", "2413_2.dot"))
  2793. def test_2413(source: str):
  2794. """
  2795. graphs that induce an edge length > 65535 should be supported
  2796. https://gitlab.com/graphviz/graphviz/-/issues/2413
  2797. """
  2798. # locate our associated test case in this directory
  2799. input = Path(__file__).parent / source
  2800. assert input.exists(), "unexpectedly missing test case"
  2801. # run it through Graphviz
  2802. proc = subprocess.run(
  2803. ["dot", "-Tsvg", "-o", os.devnull, input],
  2804. stderr=subprocess.PIPE,
  2805. check=True,
  2806. universal_newlines=True,
  2807. )
  2808. # work around macOS warnings
  2809. stderr = remove_xtype_warnings(proc.stderr).strip()
  2810. # work around ASan informational printing
  2811. stderr = remove_asan_summary(stderr)
  2812. # no warnings should have been generated
  2813. assert stderr == "", "long edges resulted in a warning"
  2814. def test_2429():
  2815. """
  2816. the vt target should be usable
  2817. https://gitlab.com/graphviz/graphviz/-/issues/2429
  2818. """
  2819. # a basic graph
  2820. source = "digraph { a -> b; }"
  2821. # run it through Graphviz
  2822. dot("vt", source=source)
  2823. @pytest.mark.skipif(which("nop") is None, reason="nop not available")
  2824. @pytest.mark.xfail(
  2825. strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/2436"
  2826. )
  2827. def test_2436():
  2828. """
  2829. nop should preserve empty labels
  2830. https://gitlab.com/graphviz/graphviz/-/issues/2436
  2831. """
  2832. # locate our associated test case in this directory
  2833. input = Path(__file__).parent / "2436.dot"
  2834. assert input.exists(), "unexpectedly missing test case"
  2835. # run it through nop
  2836. nop = which("nop")
  2837. output = subprocess.check_output([nop, input], universal_newlines=True)
  2838. # the empty label should be present
  2839. assert re.search(r'\blabel\s*=\s*""', output), "empty label was not preserved"
  2840. @pytest.mark.xfail(
  2841. strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/2434"
  2842. )
  2843. def test_2434():
  2844. """
  2845. the order in which `agmemread` and `gvContext` calls are made should have
  2846. no impact
  2847. https://gitlab.com/graphviz/graphviz/-/issues/2434
  2848. """
  2849. # find co-located test source
  2850. c_src = (Path(__file__).parent / "2434.c").resolve()
  2851. assert c_src.exists(), "missing test case"
  2852. # generate an SVG by calling `gvContext` first
  2853. before, _ = run_c(c_src, ["before"], link=["cgraph", "gvc"])
  2854. # generate an SVG by calling `gvContext` second
  2855. after, _ = run_c(c_src, ["after"], link=["cgraph", "gvc"])
  2856. # resulting images should be identical
  2857. assert before == after, "agmemread/gvContext ordering affected image output"
  2858. def test_2437():
  2859. """
  2860. both an arrowhead and an arrowtail shall be created when using dir=both,
  2861. compass ports, an edge default attribute and rank=same
  2862. https://gitlab.com/graphviz/graphviz/-/issues/2437
  2863. """
  2864. # locate our associated test case in this directory
  2865. input = Path(__file__).parent / "2437.dot"
  2866. assert input.exists(), "unexpectedly missing test case"
  2867. # translate this to SVG
  2868. svg = dot("svg", input)
  2869. # load this as XML
  2870. root = ET.fromstring(svg)
  2871. # The output is expected to contain tree polygons. The graph "background"
  2872. # polygon, the arrowhead polygon and the arrowtail polygon.
  2873. polygons = root.findall(".//{http://www.w3.org/2000/svg}polygon")
  2874. assert len(polygons) == 3, "wrong number of polygons in output"
  2875. @pytest.mark.xfail(
  2876. strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/2416"
  2877. )
  2878. def test_2416():
  2879. """
  2880. `splines=curved` should not affect arrow directions
  2881. https://gitlab.com/graphviz/graphviz/-/issues/2416
  2882. """
  2883. # an input graph that provokes the problem
  2884. input = "digraph G { splines=curved; b -> a; a -> b; }"
  2885. # run it through Graphviz
  2886. output = dot("json", source=input)
  2887. data = json.loads(output)
  2888. edges = data["edges"]
  2889. assert len(edges) == 2, "unexpected number of output edges"
  2890. # extract the height each edge’s arrow starts at
  2891. y_1 = edges[0]["_hdraw_"][3]["points"][0][1]
  2892. y_2 = edges[1]["_hdraw_"][3]["points"][0][1]
  2893. # assuming the graph is vertical, these should not be too close
  2894. assert abs(y_1 - y_2) > 1, "edge arrows appear to be drawn next to the same node"
  2895. @pytest.mark.skipif(which("gvpr") is None, reason="GVPR not available")
  2896. def test_2454():
  2897. """
  2898. gvpr should support sscanf
  2899. https://gitlab.com/graphviz/graphviz/-/issues/2454
  2900. """
  2901. # an input graph that provokes the problem
  2902. input = "graph x{a -- {b c}}"
  2903. # run it through Graphviz
  2904. output = dot("dot", source=input)
  2905. # run it through gvpr
  2906. program = Path(__file__).parent / "2454.gvpr"
  2907. with subprocess.Popen(
  2908. ["gvpr", "-cf", program], stdin=subprocess.PIPE, universal_newlines=True
  2909. ) as p:
  2910. p.communicate(output)
  2911. assert p.returncode == 0, "gvpr failed"
  2912. @pytest.mark.skipif(which("twopi") is None, reason="twopi not available")
  2913. @pytest.mark.xfail(
  2914. strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/2457"
  2915. )
  2916. def test_2457():
  2917. """
  2918. node definition order should not affect twopi’s layout
  2919. https://gitlab.com/graphviz/graphviz/-/issues/2457
  2920. """
  2921. # locate our associated test cases in this directory
  2922. case1 = Path(__file__).parent / "2457_1.dot"
  2923. assert case1.exists(), "unexpectedly missing test case"
  2924. case2 = Path(__file__).parent / "2457_2.dot"
  2925. assert case2.exists(), "unexpectedly missing test case"
  2926. # tweak the environment to force deterministic PDF generation
  2927. env = os.environ.copy()
  2928. env["SOURCE_DATE_EPOCH"] = "0"
  2929. # generate PDFs
  2930. pdf1 = subprocess.check_output(["twopi", "-Tpdf", case1], env=env)
  2931. pdf2 = subprocess.check_output(["twopi", "-Tpdf", case2], env=env)
  2932. assert pdf1 == pdf2, "node definition order affected PDF generation"
  2933. @pytest.mark.xfail(
  2934. strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/2458"
  2935. )
  2936. def test_2458():
  2937. """
  2938. `pack=true` should not result in edge labels disappearing
  2939. https://gitlab.com/graphviz/graphviz/-/issues/2458
  2940. """
  2941. # locate our associated test case in this directory
  2942. input = Path(__file__).parent / "2458.dot"
  2943. assert input.exists(), "unexpectedly missing test case"
  2944. # run it through Graphviz
  2945. output = dot("svg", input)
  2946. # the edge label should be present
  2947. assert re.search(r"\bconnected\b", output), "missing edge label"
  2948. def test_2460():
  2949. """
  2950. labels involving back slashes should come out correctly in JSON
  2951. https://gitlab.com/graphviz/graphviz/-/issues/2460
  2952. """
  2953. # locate our associated test case in this directory
  2954. input = Path(__file__).parent / "2460.dot"
  2955. assert input.exists(), "unexpectedly missing test case"
  2956. # run it through Graphviz
  2957. output = dot("json", input)
  2958. data = json.loads(output)
  2959. assert (
  2960. data["objects"][0]["_ldraw_"][2]["text"]
  2961. == r"double back slash in label \\. End should be the last word - End"
  2962. ), "back slashes in labels handled incorrectly"
  2963. @pytest.mark.xfail(
  2964. strict=platform.system() != "Windows",
  2965. reason="https://gitlab.com/graphviz/graphviz/-/issues/2470",
  2966. )
  2967. def test_2470():
  2968. """
  2969. another “trouble in init_rank variant”
  2970. https://gitlab.com/graphviz/graphviz/-/issues/2470
  2971. """
  2972. # locate our associated test case in this directory
  2973. input = Path(__file__).parent / "2470.dot"
  2974. assert input.exists(), "unexpectedly missing test case"
  2975. # run it through Graphviz
  2976. dot("ps", input)
  2977. @pytest.mark.xfail(
  2978. reason="https://gitlab.com/graphviz/graphviz/-/issues/2471",
  2979. strict=True,
  2980. )
  2981. def test_2471():
  2982. """
  2983. another “trouble in init_rank variant”
  2984. https://gitlab.com/graphviz/graphviz/-/issues/2471
  2985. """
  2986. # locate our associated test case in this directory
  2987. input = Path(__file__).parent / "2471.dot"
  2988. assert input.exists(), "unexpectedly missing test case"
  2989. # run it through Graphviz
  2990. dot("png", input)
  2991. @pytest.mark.xfail(
  2992. is_rocky_8(),
  2993. reason="Cairo is <v1.16 or malfunctions",
  2994. strict=True,
  2995. )
  2996. def test_2473_1():
  2997. """
  2998. `SOURCE_DATE_EPOCH` should be usable to suppress timestamps
  2999. https://gitlab.com/graphviz/graphviz/-/issues/2473
  3000. """
  3001. # a trivial graph
  3002. graph = "graph { a -- b }".encode("utf-8")
  3003. # set an epoch
  3004. env = os.environ.copy()
  3005. env["SOURCE_DATE_EPOCH"] = "60"
  3006. # generate a PDF
  3007. first_run = subprocess.check_output(["dot", "-Tpdf"], input=graph, env=env)
  3008. # wait long enough for the current time to change
  3009. time.sleep(2)
  3010. # generate another PDF
  3011. second_run = subprocess.check_output(["dot", "-Tpdf"], input=graph, env=env)
  3012. assert (
  3013. first_run == second_run
  3014. ), "PDF output is dependent on current time even when $SOURCE_DATE_EPOCH is set"
  3015. def test_2473_2():
  3016. """
  3017. When handling `SOURCE_DATE_EPOCH`, from
  3018. https://reproducible-builds.org/specs/source-date-epoch/:
  3019. If the value is malformed, the build process SHOULD exit with a non-zero
  3020. error code.
  3021. https://gitlab.com/graphviz/graphviz/-/issues/2473
  3022. """
  3023. # set up an invalid epoch
  3024. env = os.environ.copy()
  3025. env["SOURCE_DATE_EPOCH"] = "foo"
  3026. # confirm Graphviz rejects this
  3027. with pytest.raises(subprocess.CalledProcessError):
  3028. subprocess.run(
  3029. ["dot", "-Tpdf", "-o", os.devnull],
  3030. input="graph { a -- b }",
  3031. env=env,
  3032. check=True,
  3033. encoding="utf-8",
  3034. universal_newlines=True,
  3035. )
  3036. def test_2476():
  3037. """
  3038. tweaking `mclimit` should not lead to a “trouble in init_rank” failure
  3039. https://gitlab.com/graphviz/graphviz/-/issues/2476
  3040. """
  3041. # locate our associated test case in this directory
  3042. input = Path(__file__).parent / "2476.dot"
  3043. assert input.exists(), "unexpectedly missing test case"
  3044. # run it through Graphviz
  3045. subprocess.check_call(["dot", "-Tsvg", "-Gmclimit=0.5", "-o", os.devnull, input])
  3046. def test_2490():
  3047. """
  3048. the `crow` arrow shall be correctly placed and orientated when ports are used
  3049. https://gitlab.com/graphviz/graphviz/-/issues/2490
  3050. """
  3051. # locate our associated test case in this directory
  3052. input = Path(__file__).parent / "2490.dot"
  3053. assert input.exists(), "unexpectedly missing test case"
  3054. # translate this to SVG
  3055. svg = dot("svg", input)
  3056. # load this as XML
  3057. root = ET.fromstring(svg)
  3058. # The output is expected to contain three polygons, of which the two last
  3059. # are the `crow` arrow shapes of the edge head and tail. Except for the
  3060. # crow's "toes", the corners of these are expected to have the same x
  3061. # position as the nodes' centers. The "toes" are expected to have x
  3062. # positions half the width more or less than the nodes' centers.
  3063. ellipses = root.findall(".//{http://www.w3.org/2000/svg}ellipse")
  3064. assert len(ellipses) == 2, "wrong number of ellipses in output"
  3065. cx = float(ellipses[0].get("cx"))
  3066. assert float(ellipses[1].get("cx")) == cx
  3067. polygons = root.findall(".//{http://www.w3.org/2000/svg}polygon")
  3068. assert len(polygons) == 3, "wrong number of polygons in output"
  3069. for polygon_index, polygon in enumerate(polygons):
  3070. points_attr = polygon.get("points")
  3071. point_pair_strs = points_attr.split(" ")
  3072. points = [point_pair_str.split(",") for point_pair_str in point_pair_strs]
  3073. if polygon_index == 0:
  3074. assert len(points) == 5
  3075. # ignore the graph polygon
  3076. continue
  3077. assert len(points) == 9
  3078. for point_index, point in enumerate(points):
  3079. x = float(point[0])
  3080. crow_width = 9
  3081. expected_crow_tip_and_shaft_x = cx
  3082. expected_crow_toe_left_x = cx - crow_width / 2
  3083. expected_crow_toe_right_x = cx + crow_width / 2
  3084. expected_first_crow_toe_x = (
  3085. expected_crow_toe_left_x
  3086. if polygon_index == 1
  3087. else expected_crow_toe_right_x
  3088. )
  3089. expected_second_crow_toe_x = (
  3090. expected_crow_toe_right_x
  3091. if polygon_index == 1
  3092. else expected_crow_toe_left_x
  3093. )
  3094. if point_index in [0, 2, 3, 4, 5, 6, 8]:
  3095. assert x == expected_crow_tip_and_shaft_x
  3096. elif point_index == 1:
  3097. assert x == expected_first_crow_toe_x
  3098. elif point_index == 7:
  3099. assert x == expected_second_crow_toe_x
  3100. @pytest.mark.skipif(which("gv2gml") is None, reason="gv2gml not available")
  3101. def test_2493():
  3102. """
  3103. `gv2gml` should support the yWorks.com variant of GML
  3104. https://gitlab.com/graphviz/graphviz/-/issues/2493
  3105. """
  3106. # a trivial graph with a colored label
  3107. src = 'graph { a -- b[label="foo", fontcolor="red"]; }'
  3108. # pass this through `gv2gml`
  3109. gml = subprocess.check_output(["gv2gml", "-y"], input=src, universal_newlines=True)
  3110. assert (
  3111. re.search(r"\bfontcolor\b", gml) is None
  3112. ), "gv2gml emitted 'fontcolor' when in yWorks.com mode"
  3113. assert (
  3114. re.search(r"\bcolor\b", gml) is not None
  3115. ), "gv2gml did not emit LabelGraphics 'color' attribute"
  3116. def test_2497():
  3117. """
  3118. graph rendering should be deterministic
  3119. https://gitlab.com/graphviz/graphviz/-/issues/2497
  3120. """
  3121. # get our baseline reference
  3122. input = Path(__file__).parent / "2497.dot"
  3123. assert input.exists(), "unexpectedly missing test case"
  3124. ref = dot("svg", input)
  3125. # now repeat this, expecting it not to change
  3126. for _ in range(20):
  3127. out = dot("svg", input)
  3128. assert ref == out, "repeated rendering changed output"
  3129. def test_2502():
  3130. """
  3131. unicode labels should be usable
  3132. https://gitlab.com/graphviz/graphviz/-/issues/2502
  3133. """
  3134. # locate our associated test case in this directory
  3135. input = Path(__file__).parent / "2502.dot"
  3136. assert input.exists(), "unexpectedly missing test case"
  3137. # run it through Graphviz
  3138. dot("dot", input)
  3139. @pytest.mark.xfail(
  3140. strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/2516"
  3141. )
  3142. def test_2516():
  3143. """
  3144. errors in HTML labels should result in a message with correct line number
  3145. https://gitlab.com/graphviz/graphviz/-/issues/2516
  3146. """
  3147. # locate our associated test case in this directory
  3148. input = Path(__file__).parent / "2516.dot"
  3149. assert input.exists(), "unexpectedly missing test case"
  3150. # run it through Graphviz
  3151. proc = subprocess.run(
  3152. ["dot", "-Tsvg", "-o", os.devnull, input],
  3153. stderr=subprocess.PIPE,
  3154. universal_newlines=True,
  3155. )
  3156. assert proc.returncode != 0, "malformed HTML label was accepted"
  3157. assert (
  3158. re.search(r"\bline 1\b", proc.stderr) is None
  3159. ), "incorrect line number in error message"
  3160. assert (
  3161. re.search(r"\bline 2\b", proc.stderr) is not None
  3162. ), "correct line number missing from error message"
  3163. @pytest.mark.parametrize(
  3164. "testcase",
  3165. (
  3166. "705.dot",
  3167. pytest.param(
  3168. "2521.dot",
  3169. marks=pytest.mark.xfail(
  3170. strict=False,
  3171. reason="https://gitlab.com/graphviz/graphviz/-/issues/2521",
  3172. ),
  3173. ),
  3174. "2521_1.dot",
  3175. ),
  3176. )
  3177. def test_2521(testcase: str):
  3178. """
  3179. `newrank=false` should reset to the default behavior
  3180. https://gitlab.com/graphviz/graphviz/-/issues/2521
  3181. """
  3182. # locate our associated test case in this directory
  3183. input = Path(__file__).parent / testcase
  3184. assert input.exists(), "unexpectedly missing test case"
  3185. # process this with and without `newrank=true`
  3186. off = subprocess.check_output(["dot", "-Tpng", input])
  3187. on = subprocess.check_output(["dot", "-Gnewrank=true", "-Tpng", input])
  3188. assert off != on, "-Gnewrank=true had no effect"
  3189. # we should be able to reset `newrank` with an explicit setting
  3190. force_off = subprocess.check_output(["dot", "-Gnewrank=false", "-Tpng", input])
  3191. assert force_off == off, "-Gnewrank=false did not reset the default"
  3192. @pytest.mark.xfail(
  3193. is_macos(), strict=True, reason="https://gitlab.com/graphviz/graphviz/-/issues/2538"
  3194. )
  3195. def test_2538():
  3196. """
  3197. `chanSearch` assertion on `cp` should not fail
  3198. https://gitlab.com/graphviz/graphviz/-/issues/2538
  3199. """
  3200. # locate our associated test case in this directory
  3201. input = Path(__file__).parent / "2538.dot"
  3202. assert input.exists(), "unexpectedly missing test case"
  3203. # run it through Graphviz
  3204. dot("dot", input)
  3205. @pytest.mark.skipif(which("sfdp") is None, reason="sfdp not available")
  3206. def test_2556():
  3207. """
  3208. sfdp should not fail a GTS assertion
  3209. https://gitlab.com/graphviz/graphviz/-/issues/2556
  3210. """
  3211. # locate our associated test case in this directory
  3212. input = Path(__file__).parent / "2556.dot"
  3213. assert input.exists(), "unexpectedly missing test case"
  3214. # run this through sfdp
  3215. sfdp = which("sfdp")
  3216. p = subprocess.run(
  3217. [sfdp, "-Tpng", "-o", os.devnull, input],
  3218. stderr=subprocess.PIPE,
  3219. universal_newlines=True,
  3220. )
  3221. # if sfdp was built without libgts, it will not handle anything non-trivial
  3222. no_gts_error = "remove_overlap: Graphviz not built with triangulation library"
  3223. if no_gts_error in p.stderr:
  3224. assert p.returncode != 0, "sfdp returned success after an error message"
  3225. return
  3226. p.check_returncode()
  3227. def test_2559():
  3228. """
  3229. `concentrate=true` should actually concentrate edges
  3230. https://gitlab.com/graphviz/graphviz/-/issues/2559
  3231. """
  3232. # locate our associated test case in this directory
  3233. input = Path(__file__).parent / "2559.dot"
  3234. assert input.exists(), "unexpectedly missing test case"
  3235. # convert this to JSON
  3236. layout = dot("json", input)
  3237. parsed = json.loads(layout)
  3238. # the last edge, d→b, should be drawn as a curve rather than a straight edge
  3239. assert not parsed["edges"][-1]["pos"].startswith(
  3240. "e"
  3241. ), "concentrated edge drawn as a regular straight edge"
  3242. @pytest.mark.skipif(which("fdp") is None, reason="fdp not available")
  3243. def test_2563():
  3244. """
  3245. `overlap` parameters should generate different results
  3246. https://gitlab.com/graphviz/graphviz/-/issues/2563
  3247. """
  3248. # locate our associated test case in this directory
  3249. input = Path(__file__).parent / "2563.dot"
  3250. assert input.exists(), "unexpectedly missing test case"
  3251. # try various `overlap=…` values
  3252. results: Set[str] = set()
  3253. for overlap in ("scale", "scalexy"):
  3254. # run this through fdp
  3255. fdp = which("fdp")
  3256. p = subprocess.run(
  3257. [fdp, f"-Goverlap={overlap}", input],
  3258. stdout=subprocess.PIPE,
  3259. stderr=subprocess.PIPE,
  3260. universal_newlines=True,
  3261. )
  3262. # if fdp was built without libgts, it will not handle anything non-trivial
  3263. no_gts_error = "remove_overlap: Graphviz not built with triangulation library"
  3264. if no_gts_error in p.stderr:
  3265. assert p.returncode != 0, "fdp returned success after an error message"
  3266. return
  3267. p.check_returncode()
  3268. # remove the overlap parameter itself, that would otherwise cause each
  3269. # output to be unique
  3270. output = re.sub(r"\boverlap\s*=\s*scale(xy)?\b", "", p.stdout)
  3271. assert (
  3272. output not in results
  3273. ), "altering `overlap` attribute did not affect output"
  3274. results.add(output)
  3275. def test_2564():
  3276. """
  3277. `overlap="scale"` should not result in all nodes overlapping
  3278. https://gitlab.com/graphviz/graphviz/-/issues/2564
  3279. """
  3280. # locate our associated test case in this directory
  3281. input = Path(__file__).parent / "2564.dot"
  3282. assert input.exists(), "unexpectedly missing test case"
  3283. # convert this to JSON
  3284. layout = subprocess.check_output(
  3285. ["dot", "-Kneato", "-Tjson", input], universal_newlines=True
  3286. )
  3287. parsed = json.loads(layout)
  3288. # nodes should not be on top of one another
  3289. starts = []
  3290. for node in parsed["objects"]:
  3291. start = re.match(r"(?P<start>\d+(\.\d+)?,\d+(\.\d+)?)\b", node["pos"]).group(
  3292. "start"
  3293. )
  3294. assert start not in starts, "nodes overlap"
  3295. starts += [start]
  3296. @pytest.mark.skipif(shutil.which("tclsh") is None, reason="tclsh not available")
  3297. @pytest.mark.skipif(
  3298. platform.system() == "Windows",
  3299. reason="pexpect.spawn is not available on Windows "
  3300. "(https://pexpect.readthedocs.io/en/stable/overview.html#pexpect-on-windows)",
  3301. )
  3302. def test_2568():
  3303. """
  3304. tags used in TCL output should be usable for later lookup
  3305. https://gitlab.com/graphviz/graphviz/-/issues/2568
  3306. """
  3307. # locate the TCL input for this test
  3308. prelude = Path(__file__).parent / "2568.tcl"
  3309. assert prelude.exists(), "unexpectedly missing test collateral"
  3310. # if this appears to be an ASan-enabled CI job, teach `tclsh` to load ASan’s
  3311. # supporting library because it is otherwise unaware that Tcldot depends on this
  3312. # being loaded first
  3313. env = os.environ.copy()
  3314. dot_exe = which("dot")
  3315. if is_asan_instrumented(dot_exe):
  3316. cc = os.environ.get("CC", "gcc")
  3317. libasan = subprocess.check_output(
  3318. [cc, "-print-file-name=libasan.so"], universal_newlines=True
  3319. ).strip()
  3320. print(f"setting LD_PRELOAD={libasan}")
  3321. env["LD_PRELOAD"] = libasan
  3322. # startup TCL and load our graph setup code
  3323. proc = pexpect.spawn("tclsh", timeout=1, env=env)
  3324. proc.expect("% ")
  3325. proc.sendline(f'source "{shlex.quote(str(prelude))}"')
  3326. # look for tags to query
  3327. while True:
  3328. index = proc.expect(
  3329. [
  3330. "invalid command name",
  3331. re.compile(rb"-tags {\d(?P<tag>(edge|node)0x[\da-fA-F]+)}"),
  3332. pexpect.TIMEOUT,
  3333. ]
  3334. )
  3335. # stdout and stderr are multiplexed onto the same stream by `pexpect`, so if one
  3336. # of the commands we previously entered was not recognized, we will see an error
  3337. # at the end of the output stream
  3338. assert index != 0, "at least one tag was not recognized"
  3339. # if we got no output within 1s, assume we are done and try to neatly exit
  3340. if index == 2:
  3341. proc.sendeof()
  3342. proc.wait()
  3343. break
  3344. tag = proc.match.group("tag").decode("utf-8")
  3345. # …try to look up its corresponding entities
  3346. if tag.startswith("edge"):
  3347. cmd = "listnodes"
  3348. else:
  3349. cmd = "listedges"
  3350. proc.sendline(f"{tag} {cmd}")
  3351. @pytest.mark.skipif(which("sfdp") is None, reason="sfdp not available")
  3352. def test_2572():
  3353. """
  3354. sfdp should be able to find non-overlapping layouts
  3355. https://gitlab.com/graphviz/graphviz/-/issues/2572
  3356. """
  3357. # locate our associated test case in this directory
  3358. input = Path(__file__).parent / "2572.dot"
  3359. assert input.exists(), "unexpectedly missing test case"
  3360. # run this through SFDP and convert this to JSON
  3361. sfdp = which("sfdp")
  3362. layout = subprocess.check_output(
  3363. [sfdp, "-Kneato", "-Tjson", input], universal_newlines=True
  3364. )
  3365. parsed = json.loads(layout)
  3366. @dataclasses.dataclass
  3367. class Box:
  3368. """
  3369. a geometric rectangle, defined by two of its corners
  3370. """
  3371. llx: float # lower left X coordinate
  3372. lly: float # lower left Y coordinate
  3373. urx: float # upper right X coordinate
  3374. ury: float # upper right Y coordinate
  3375. def overlaps(self, other: "Box") -> bool:
  3376. """
  3377. do we intersect the given box?
  3378. """
  3379. if self.llx > other.urx:
  3380. return False
  3381. if self.lly > other.ury:
  3382. return False
  3383. if self.urx < other.llx:
  3384. return False
  3385. if self.ury < other.lly:
  3386. return False
  3387. return True
  3388. nodes: List[Box] = []
  3389. for obj in parsed["objects"]:
  3390. # extract the ellipse drawn for this node
  3391. ellipses = [e for e in obj["_draw_"] if e["op"] == "e"]
  3392. assert len(ellipses) == 1, "could not find ellipse for node"
  3393. center_x, center_y, width, height = ellipses[0]["rect"]
  3394. assert center_x >= width, "ellipse extends into negative X space"
  3395. assert center_y >= height, "ellipse extends into negative Y space"
  3396. node = Box(
  3397. center_x - width, center_y - height, center_x + width, center_y + height
  3398. )
  3399. assert not any(n.overlaps(node) for n in nodes), "nodes overlap"
  3400. nodes.append(node)
  3401. @pytest.mark.skipif(which("gvpr") is None, reason="GVPR not available")
  3402. def test_2577():
  3403. """
  3404. accessing an uninitialized string should not corrupt GVPR’s state
  3405. https://gitlab.com/graphviz/graphviz/-/issues/2577
  3406. """
  3407. # find our collocated test case
  3408. program = Path(__file__).parent / "2577.gvpr"
  3409. assert program.exists(), "unexpectedly missing test case"
  3410. # run it through GVPR
  3411. output = gvpr(program)
  3412. # it should have printed an empty string for the uninitialized attribute
  3413. assert (
  3414. "Before...\n<>\nAfter." in output
  3415. ), "incorrect handling of uninitialized attribute in GVPR"
  3416. @pytest.mark.skipif(which("gvpr") is None, reason="GVPR not available")
  3417. def test_2577_1():
  3418. """
  3419. a variant of `test_2577` that does not involve attribute access
  3420. https://gitlab.com/graphviz/graphviz/-/issues/2577
  3421. """
  3422. # run GVPR on a simple program
  3423. gvprbin = which("gvpr")
  3424. output = subprocess.check_output(
  3425. [gvprbin, 'BEGIN { printf("hello%s world\\n", ""); }'],
  3426. stdin=subprocess.DEVNULL,
  3427. universal_newlines=True,
  3428. )
  3429. # it should have printed the expected text
  3430. assert output == "hello world\n", "gvpr cannot handle empty strings to printf"
  3431. @pytest.mark.parametrize(
  3432. "testcase",
  3433. (
  3434. "2585",
  3435. "2585_1",
  3436. "2585_2",
  3437. "2585_3",
  3438. "2585_4",
  3439. "2585_5",
  3440. "2585_6",
  3441. "2585_7",
  3442. ),
  3443. )
  3444. @pytest.mark.skipif(which("gvpr") is None, reason="GVPR not available")
  3445. def test_2585(testcase: str):
  3446. """
  3447. GVPR should reject various invalid uses of `void` types
  3448. https://gitlab.com/graphviz/graphviz/-/issues/2585
  3449. """
  3450. # locate our associated test case in this directory
  3451. input = Path(__file__).parent / f"{testcase}.gvpr"
  3452. assert input.exists(), "unexpectedly missing test case"
  3453. # run the program
  3454. gvprbin = which("gvpr")
  3455. ret = subprocess.call(
  3456. [gvprbin, "-o", os.devnull, "-f", input], stdin=subprocess.DEVNULL
  3457. )
  3458. assert ret == 1, "GVPR did not reject invalid use of `void`"
  3459. @pytest.mark.skipif(which("gml2gv") is None, reason="gml2gv not available")
  3460. def test_2586():
  3461. """
  3462. labels should be preserved in GML→GV translation
  3463. https://gitlab.com/graphviz/graphviz/-/issues/2586
  3464. """
  3465. # locate our associated test case in this directory
  3466. input = Path(__file__).parent / "2586.gml"
  3467. assert input.exists(), "unexpectedly missing test case"
  3468. # translate it
  3469. gml2gv = which("gml2gv")
  3470. gv = subprocess.check_output([gml2gv, input], universal_newlines=True)
  3471. assert (
  3472. re.search(r'\blabel\s*=\s*"?0"?\b', gv) is not None
  3473. ), "labels not preserved in GML→GV translation"
  3474. @pytest.mark.skipif(which("gvgen") is None, reason="gvgen not available")
  3475. def test_2588():
  3476. """
  3477. `gvgen` should not crash when producing random graphs
  3478. https://gitlab.com/graphviz/graphviz/-/issues/2588
  3479. """
  3480. gvgen = which("gvgen")
  3481. # this execution depends on random numbers, so we need to run many times to
  3482. # have a chance of provoking the bug
  3483. for _ in range(200):
  3484. subprocess.check_call([gvgen, "-R", "20"], stdout=subprocess.DEVNULL)
  3485. @pytest.mark.skipif(which("edgepaint") is None, reason="edgepaint not available")
  3486. @pytest.mark.skipif(which("gvgen") is None, reason="gvgen not available")
  3487. def test_2591():
  3488. """
  3489. edgepaint color schemes should do something
  3490. https://gitlab.com/graphviz/graphviz/-/issues/2591
  3491. """
  3492. # make an input graph
  3493. gvgen = which("gvgen")
  3494. graph = subprocess.check_output([gvgen, "-k", "5"], universal_newlines=True)
  3495. # run it through neato
  3496. laidout = subprocess.check_output(
  3497. ["dot", "-Kneato", "-Goverlap=false"], input=graph, universal_newlines=True
  3498. )
  3499. # try two different edgepaint invocations
  3500. edgepaint = which("edgepaint")
  3501. gray = subprocess.check_output(
  3502. [edgepaint, "--angle=89.999", "--color_scheme=gray"],
  3503. input=laidout,
  3504. universal_newlines=True,
  3505. )
  3506. rgb = subprocess.check_output(
  3507. [edgepaint, "--angle=89.999", "--color_scheme=#00ff00,#0000ff"],
  3508. input=laidout,
  3509. universal_newlines=True,
  3510. )
  3511. # process these into an image
  3512. gray_svg = subprocess.check_output(
  3513. ["dot", "-Kneato", "-n2", "-Tsvg"], input=gray, universal_newlines=True
  3514. )
  3515. rgb_svg = subprocess.check_output(
  3516. ["dot", "-Kneato", "-n2", "-Tsvg"], input=rgb, universal_newlines=True
  3517. )
  3518. assert gray_svg != rgb_svg, "edgepaint --color_scheme had no effect"
  3519. @pytest.mark.skipif(shutil.which("tclsh") is None, reason="tclsh not available")
  3520. @pytest.mark.skipif(
  3521. platform.system() == "Windows",
  3522. reason="pexpect.spawn is not available on Windows "
  3523. "(https://pexpect.readthedocs.io/en/stable/overview.html#pexpect-on-windows)",
  3524. )
  3525. @pytest.mark.xfail(
  3526. is_cmake() and is_macos(),
  3527. reason="FIXME: 'vgpane' command is unrecognized for unknown reasons",
  3528. strict=True,
  3529. )
  3530. @pytest.mark.xfail(
  3531. is_autotools() and is_macos(),
  3532. reason="Autotools on macOS does not detect TCL",
  3533. strict=True,
  3534. )
  3535. @pytest.mark.xfail(
  3536. is_cmake() and is_ubuntu_2004(),
  3537. reason="TCL packages are not built on Ubuntu 20.04 with CMake < 3.18",
  3538. strict=True,
  3539. )
  3540. def test_2596():
  3541. """
  3542. running Tclpathplan `triangulate` with a malformed callback script should not read
  3543. out-of-bounds
  3544. https://gitlab.com/graphviz/graphviz/-/issues/2596
  3545. """
  3546. # if this appears to be an ASan-enabled CI job, teach `tclsh` to load ASan’s
  3547. # supporting library because it is otherwise unaware that Tcldot depends on this
  3548. # being loaded first
  3549. env = os.environ.copy()
  3550. dot_exe = which("dot")
  3551. if is_asan_instrumented(dot_exe):
  3552. cc = os.environ.get("CC", "gcc")
  3553. libasan = subprocess.check_output(
  3554. [cc, "-print-file-name=libasan.so"], universal_newlines=True
  3555. ).strip()
  3556. print(f"setting LD_PRELOAD={libasan}")
  3557. env["LD_PRELOAD"] = libasan
  3558. # startup TCL and load the pathplan module
  3559. proc = pexpect.spawn("tclsh", timeout=1, env=env)
  3560. proc.expect("% ")
  3561. proc.sendline("package require Tclpathplan")
  3562. proc.expect("% ")
  3563. # Create a pane. We assume the first created pane will be index 0, though
  3564. # this is not technically required.
  3565. proc.sendline("vgpane")
  3566. proc.expect("vgpane0")
  3567. proc.expect("% ")
  3568. # bind the triangulation callback to something ending in a trailing '%'
  3569. proc.sendline("vgpane0 bind triangle %")
  3570. proc.expect("% ")
  3571. # add a triangular polygon
  3572. proc.sendline("vgpane0 insert 1 1 2 2 1 2")
  3573. proc.expect("1")
  3574. proc.expect("% ")
  3575. # attempt triangulation on this polygon
  3576. proc.sendline("vgpane0 triangulate 1")
  3577. proc.expect("% ")
  3578. # delete the pane to clean up
  3579. proc.sendline("vgpane0 delete")
  3580. proc.expect("% ")
  3581. # tell TCL to exit
  3582. proc.sendeof()
  3583. proc.wait()
  3584. @pytest.mark.skipif(which("gvgen") is None, reason="gvgen not available")
  3585. @pytest.mark.skipif(which("mingle") is None, reason="mingle not available")
  3586. @pytest.mark.xfail(
  3587. reason="https://gitlab.com/graphviz/graphviz/-/issues/2599", strict=True
  3588. )
  3589. def test_2599():
  3590. """
  3591. mingle should not segfault when processing simple graphs
  3592. https://gitlab.com/graphviz/graphviz/-/issues/2599
  3593. """
  3594. # generate a graph
  3595. gvgen = which("gvgen")
  3596. graph = subprocess.check_output([gvgen, "-d", "-k", "5"], universal_newlines=True)
  3597. # process it into canonical form
  3598. processed = subprocess.check_output(["dot"], universal_newlines=True, input=graph)
  3599. # pass it through mingle
  3600. mingle = which("mingle")
  3601. proc = subprocess.run(
  3602. [mingle, "-v", "999"], universal_newlines=True, input=processed
  3603. )
  3604. # Address Sanitizer catches segfaults and turns them into non-zero exits, so ignore
  3605. # testing in this scenario
  3606. if is_asan_instrumented(mingle):
  3607. pytest.skip("crashes of mingle are harder to detect under ASan")
  3608. assert proc.returncode in (0, 1), "mingle crashed"
  3609. @pytest.mark.skipif(which("acyclic") is None, reason="acyclic not available")
  3610. def test_2600():
  3611. """
  3612. acyclic should produce output
  3613. https://gitlab.com/graphviz/graphviz/-/issues/2600
  3614. """
  3615. # run acyclic on a simple cyclic graph
  3616. acyclic = which("acyclic")
  3617. ret = subprocess.run(
  3618. [acyclic],
  3619. input="digraph { A -> B -> C -> D -> E; E -> A }",
  3620. stdout=subprocess.PIPE,
  3621. check=False,
  3622. universal_newlines=True,
  3623. )
  3624. assert ret.returncode == 1, "acyclic did not detect a cyclic graph"
  3625. assert ret.stdout.strip() != "", "acyclic produced no output"
  3626. @pytest.mark.skipif(which("dot_builtins") is None, reason="dot_builtins not available")
  3627. def test_2604():
  3628. """
  3629. dot_builtins should not repeat formats in guidance
  3630. https://gitlab.com/graphviz/graphviz/-/issues/2604
  3631. """
  3632. # a simple graph
  3633. input = "digraph { a -> b; }"
  3634. # run dot_builtins with an incorrect format
  3635. dot_builtins = which("dot_builtins")
  3636. proc = subprocess.run(
  3637. [dot_builtins, "-o", os.devnull, "-Tpng:"],
  3638. stderr=subprocess.PIPE,
  3639. input=input,
  3640. universal_newlines=True,
  3641. check=False,
  3642. )
  3643. assert proc.returncode != 0, "dot_builtins accepted malformed format 'png:'"
  3644. assert (
  3645. len(re.findall(r"\bpng:cairo:cairo\b", proc.stderr)) <= 1
  3646. ), "duplicate formats listed in guidance"
  3647. @pytest.mark.skipif(shutil.which("convert") is None, reason="ImageMagick not available")
  3648. @pytest.mark.skipif(which("neato") is None, reason="neato not available")
  3649. @pytest.mark.skipif(
  3650. platform.system() == "Windows", reason="`convert` on Windows is not ImageMagick"
  3651. )
  3652. def test_2609(tmp_path: Path):
  3653. """
  3654. GIFs should not be blank
  3655. https://gitlab.com/graphviz/graphviz/-/issues/2609
  3656. """
  3657. # locate our associated test case in this directory
  3658. input = Path(__file__).parent / "2609.dot"
  3659. assert input.exists(), "unexpectedly missing test case"
  3660. # run this through Neato and convert to GIF
  3661. neato = which("neato")
  3662. gif = tmp_path / "2609.gif"
  3663. subprocess.check_call([neato, "-Tgif", input, "-o", gif])
  3664. # translate this into the simplest format we can parse to validate
  3665. ppm = tmp_path / "2609.ppm"
  3666. subprocess.check_call(["convert", gif, ppm])
  3667. with open(ppm, "rb") as f:
  3668. # we should have the PPM header
  3669. assert f.read(3) == b"P6\n"
  3670. # skip the dimensions line
  3671. while True:
  3672. if f.read(1) in (b"", b"\n"):
  3673. break
  3674. # skip maximum intensity line
  3675. while True:
  3676. if f.read(1) in (b"", b"\n"):
  3677. break
  3678. # read the first pixel as a reference color
  3679. reference = f.read(3)
  3680. # the remaining pixels should not all be identical if we have an actual image
  3681. while True:
  3682. pixel = f.read(3)
  3683. if len(pixel) < 3:
  3684. break
  3685. if pixel != reference:
  3686. # found a different pixel
  3687. return
  3688. pytest.fail("generated GIF was a solid color")
  3689. def test_2613():
  3690. """
  3691. Graphviz should not fail an assertion when processing this graph
  3692. https://gitlab.com/graphviz/graphviz/-/issues/2613
  3693. """
  3694. # locate our associated test case in this directory
  3695. input = Path(__file__).parent / "2613.dot"
  3696. assert input.exists(), "unexpectedly missing test case"
  3697. # generate it as a PDF
  3698. dot("pdf", input)
  3699. def test_2614():
  3700. """
  3701. quotes in strings should be correctly escaped
  3702. https://gitlab.com/graphviz/graphviz/-/issues/2614
  3703. """
  3704. # locate our associated test case in this directory
  3705. input = Path(__file__).parent / "2614.dot"
  3706. assert input.exists(), "unexpectedly missing test case"
  3707. # generate the canonical form of this
  3708. canonical = dot("canon", input)
  3709. # it should be re-parseable
  3710. dot("svg", source=canonical)
  3711. # quotes should have been escaped
  3712. assert canonical.count('\\"') == 2, "quotes in string were not properly escaped"
  3713. def test_2619():
  3714. """
  3715. loading a JPEG with initial EXIF stream should be possible
  3716. https://gitlab.com/graphviz/graphviz/-/issues/2619
  3717. """
  3718. # we need to run in our own directory so relative path references work
  3719. cwd = Path(__file__).parent
  3720. # our test case should be translatable to PDF
  3721. subprocess.check_call(["dot", "-Tpdf", "-o", os.devnull, "2619.dot"], cwd=cwd)
  3722. def test_2620():
  3723. """
  3724. arrows in this graph should not be truncated
  3725. https://gitlab.com/graphviz/graphviz/-/issues/2620
  3726. """
  3727. # locate our associated test case in this directory
  3728. input = Path(__file__).parent / "2620.dot"
  3729. assert input.exists(), "unexpectedly missing test case"
  3730. # render to SVG
  3731. svg = dot("svg", input)
  3732. # parse the SVG
  3733. root = ET.fromstring(svg)
  3734. # most of the differences between the “good” and “bad” rendering are small
  3735. # (~1pt diff), so discriminate using one that has been observed to be much larger
  3736. edge = root.findall(
  3737. ".//{http://www.w3.org/2000/svg}g[@id='edge101']/{http://www.w3.org/2000/svg}path"
  3738. )
  3739. assert len(edge) == 1, "could not find expected edge"
  3740. # parse the expected drawing instructions out of this
  3741. m = re.match("M(?P<move>.*)C(?P<curve>.*)", edge[0].attrib["d"])
  3742. assert m is not None, "drawing command in unexpected format"
  3743. # the curve is expected to be composed of two Béziers
  3744. points = m.group("curve").split()
  3745. assert len(points) == 6, "unexpected number of Bézier curve components"
  3746. bezier2 = [pt.split(",") for pt in points[3:]]
  3747. assert all(len(pt) == 2 for pt in bezier2), "unexpected Bézier composition"
  3748. # compare the end point of the last command against what we expect with a large
  3749. # margin of error
  3750. expected = -7286.11
  3751. assert abs(float(bezier2[2][1]) - expected) < 1000, "incorrect edge construction"
  3752. @pytest.mark.parametrize("package", ("Tcldot", "Tclpathplan"))
  3753. @pytest.mark.skipif(shutil.which("tclsh") is None, reason="tclsh not available")
  3754. @pytest.mark.xfail(
  3755. is_autotools() and is_macos(),
  3756. reason="Autotools on macOS does not detect TCL",
  3757. strict=True,
  3758. )
  3759. @pytest.mark.xfail(
  3760. is_cmake() and is_ubuntu_2004(),
  3761. reason="TCL packages are not built on Ubuntu 20.04 with CMake < 3.18",
  3762. strict=True,
  3763. )
  3764. def test_import_tcl_package(package: str):
  3765. """
  3766. The given TCL package should be loadable
  3767. """
  3768. # if this appears to be an ASan-enabled CI job, teach `tclsh` to load ASan’s
  3769. # supporting library because it is otherwise unaware that Tcldot depends on this
  3770. # being loaded first
  3771. env = os.environ.copy()
  3772. dot_exe = which("dot")
  3773. if is_asan_instrumented(dot_exe):
  3774. cc = os.environ.get("CC", "gcc")
  3775. libasan = subprocess.check_output(
  3776. [cc, "-print-file-name=libasan.so"], universal_newlines=True
  3777. ).strip()
  3778. print(f"setting LD_PRELOAD={libasan}")
  3779. env["LD_PRELOAD"] = libasan
  3780. # ask TCL to import the given package
  3781. response = subprocess.check_output(
  3782. ["tclsh"],
  3783. stderr=subprocess.STDOUT,
  3784. input=f"package require {package};",
  3785. universal_newlines=True,
  3786. env=env,
  3787. )
  3788. assert "can't find package" not in response, f"{package} cannot be loaded by TCL"
  3789. @pytest.mark.skipif(shutil.which("tclsh") is None, reason="tclsh not available")
  3790. @pytest.mark.skipif(
  3791. platform.system() == "Windows",
  3792. reason="pexpect.spawn is not available on Windows "
  3793. "(https://pexpect.readthedocs.io/en/stable/overview.html#pexpect-on-windows)",
  3794. )
  3795. @pytest.mark.xfail(
  3796. is_cmake() and is_macos(),
  3797. reason="FIXME: 'vgpane' command is unrecognized for unknown reasons",
  3798. strict=True,
  3799. )
  3800. @pytest.mark.xfail(
  3801. is_autotools() and is_macos(),
  3802. reason="Autotools on macOS does not detect TCL",
  3803. strict=True,
  3804. )
  3805. @pytest.mark.xfail(
  3806. is_cmake() and is_ubuntu_2004(),
  3807. reason="TCL packages are not built on Ubuntu 20.04 with CMake < 3.18",
  3808. strict=True,
  3809. )
  3810. def test_triangulation_overflow():
  3811. """
  3812. running Tclpathplan `triangulate` with a malformed polygon should be rejected
  3813. """
  3814. # if this appears to be an ASan-enabled CI job, teach `tclsh` to load ASan’s
  3815. # supporting library because it is otherwise unaware that Tcldot depends on this
  3816. # being loaded first
  3817. env = os.environ.copy()
  3818. dot_exe = which("dot")
  3819. if is_asan_instrumented(dot_exe):
  3820. cc = os.environ.get("CC", "gcc")
  3821. libasan = subprocess.check_output(
  3822. [cc, "-print-file-name=libasan.so"], universal_newlines=True
  3823. ).strip()
  3824. print(f"setting LD_PRELOAD={libasan}")
  3825. env["LD_PRELOAD"] = libasan
  3826. # startup TCL and load the pathplan module
  3827. proc = pexpect.spawn("tclsh", timeout=1, env=env)
  3828. proc.expect("% ")
  3829. proc.sendline("package require Tclpathplan")
  3830. proc.expect("% ")
  3831. # Create a pane. We assume the first created pane will be index 0, though
  3832. # this is not technically required.
  3833. proc.sendline("vgpane")
  3834. proc.expect("vgpane0")
  3835. proc.expect("% ")
  3836. # add a “polygon” with only a single point
  3837. proc.sendline("vgpane0 insert 4 5")
  3838. proc.expect("1")
  3839. proc.expect("% ")
  3840. # attempt triangulation on this polygon
  3841. proc.sendline("vgpane0 triangulate 1")
  3842. proc.expect("cannot be triangulated")
  3843. proc.expect("% ")
  3844. # delete the pane to clean up
  3845. proc.sendline("vgpane0 delete")
  3846. proc.expect("% ")
  3847. # tell TCL to exit
  3848. proc.sendeof()
  3849. proc.wait()
  3850. @pytest.mark.skipif(shutil.which("tclsh") is None, reason="tclsh not available")
  3851. @pytest.mark.skipif(
  3852. platform.system() == "Windows",
  3853. reason="pexpect.spawn is not available on Windows "
  3854. "(https://pexpect.readthedocs.io/en/stable/overview.html#pexpect-on-windows)",
  3855. )
  3856. @pytest.mark.xfail(
  3857. is_cmake() and is_macos(),
  3858. reason="FIXME: 'vgpane' command is unrecognized for unknown reasons",
  3859. strict=True,
  3860. )
  3861. @pytest.mark.xfail(
  3862. is_autotools() and is_macos(),
  3863. reason="Autotools on macOS does not detect TCL",
  3864. strict=True,
  3865. )
  3866. @pytest.mark.xfail(
  3867. is_cmake() and is_ubuntu_2004(),
  3868. reason="TCL packages are not built on Ubuntu 20.04 with CMake < 3.18",
  3869. strict=True,
  3870. )
  3871. def test_vgpane_bad_triangulation():
  3872. """
  3873. running Tclpathplan `triangulate` with incorrect arguments should be rejected
  3874. """
  3875. # if this appears to be an ASan-enabled CI job, teach `tclsh` to load ASan’s
  3876. # supporting library because it is otherwise unaware that Tcldot depends on this
  3877. # being loaded first
  3878. env = os.environ.copy()
  3879. dot_exe = which("dot")
  3880. if is_asan_instrumented(dot_exe):
  3881. cc = os.environ.get("CC", "gcc")
  3882. libasan = subprocess.check_output(
  3883. [cc, "-print-file-name=libasan.so"], universal_newlines=True
  3884. ).strip()
  3885. print(f"setting LD_PRELOAD={libasan}")
  3886. env["LD_PRELOAD"] = libasan
  3887. # startup TCL and load the pathplan module
  3888. proc = pexpect.spawn("tclsh", timeout=1, env=env)
  3889. proc.expect("% ")
  3890. proc.sendline("package require Tclpathplan")
  3891. proc.expect("% ")
  3892. # Create a pane. We assume the first created pane will be index 0, though
  3893. # this is not technically required.
  3894. proc.sendline("vgpane")
  3895. proc.expect("vgpane0")
  3896. proc.expect("% ")
  3897. # bind the triangulation callback to something ending in a trailing '%'
  3898. proc.sendline("vgpane0 bind triangle %")
  3899. proc.expect("% ")
  3900. # run triangulation with no polygon ID, which should be rejected
  3901. proc.sendline("vgpane0 triangulate")
  3902. proc.expect("wrong # args")
  3903. # delete the pane to clean up
  3904. proc.sendline("vgpane0 delete")
  3905. proc.expect("% ")
  3906. # tell TCL to exit
  3907. proc.sendeof()
  3908. proc.wait()
  3909. @pytest.mark.skipif(shutil.which("tclsh") is None, reason="tclsh not available")
  3910. @pytest.mark.skipif(
  3911. platform.system() == "Windows",
  3912. reason="pexpect.spawn is not available on Windows "
  3913. "(https://pexpect.readthedocs.io/en/stable/overview.html#pexpect-on-windows)",
  3914. )
  3915. @pytest.mark.xfail(
  3916. is_cmake() and is_macos(),
  3917. reason="FIXME: 'vgpane' command is unrecognized for unknown reasons",
  3918. strict=True,
  3919. )
  3920. @pytest.mark.xfail(
  3921. is_autotools() and is_macos(),
  3922. reason="Autotools on macOS does not detect TCL",
  3923. strict=True,
  3924. )
  3925. @pytest.mark.xfail(
  3926. is_cmake() and is_ubuntu_2004(),
  3927. reason="TCL packages are not built on Ubuntu 20.04 with CMake < 3.18",
  3928. strict=True,
  3929. )
  3930. def test_vgpane_delete():
  3931. """
  3932. it should be possible to delete an existing `vgpane`
  3933. """
  3934. # if this appears to be an ASan-enabled CI job, teach `tclsh` to load ASan’s
  3935. # supporting library because it is otherwise unaware that Tcldot depends on this
  3936. # being loaded first
  3937. env = os.environ.copy()
  3938. dot_exe = which("dot")
  3939. if is_asan_instrumented(dot_exe):
  3940. cc = os.environ.get("CC", "gcc")
  3941. libasan = subprocess.check_output(
  3942. [cc, "-print-file-name=libasan.so"], universal_newlines=True
  3943. ).strip()
  3944. print(f"setting LD_PRELOAD={libasan}")
  3945. env["LD_PRELOAD"] = libasan
  3946. # startup TCL and load the pathplan module
  3947. proc = pexpect.spawn("tclsh", timeout=1, env=env)
  3948. proc.expect("% ")
  3949. proc.sendline("package require Tclpathplan")
  3950. proc.expect("% ")
  3951. # Create a pane. We assume the first created pane will be index 0, though
  3952. # this is not technically required.
  3953. proc.sendline("vgpane")
  3954. proc.expect("vgpane0")
  3955. proc.expect("% ")
  3956. # delete the pane to clean up
  3957. proc.sendline("vgpane0 delete")
  3958. # `pexpect.expect` returns an index of which given expectation was matched. We
  3959. # expect this to return no output (not the invalid handle message) and therefore
  3960. # timeout.
  3961. is_valid = proc.expect(['Invalid handle: "vgpane0"', pexpect.TIMEOUT]) == 1
  3962. assert is_valid, "created vgpane was considered an invalid handle"
  3963. # tell TCL to exit
  3964. proc.sendeof()
  3965. proc.wait()
  3966. def test_changelog_dates():
  3967. """
  3968. Check the dates of releases in the changelog are correctly formatted
  3969. """
  3970. changelog = Path(__file__).parent / "../CHANGELOG.md"
  3971. with open(changelog, "rt", encoding="utf-8") as f:
  3972. for lineno, line in enumerate(f, 1):
  3973. m = re.match(r"## \[\d+\.\d+\.\d+\] [-–] (?P<date>.*)$", line)
  3974. if m is None:
  3975. continue
  3976. d = re.match(r"\d{4}-\d{2}-\d{2}", m.group("date"))
  3977. assert (
  3978. d is not None
  3979. ), f"CHANGELOG.md:{lineno}: date in incorrect format: {line}"
  3980. @pytest.mark.skipif(which("gvpack") is None, reason="gvpack not available")
  3981. def test_duplicate_hard_coded_metrics_warnings():
  3982. """
  3983. Check “no hard-coded metrics” warnings are not repeated
  3984. """
  3985. # use the #2239 test case that happens to provoke this
  3986. input = Path(__file__).parent / "2239.dot"
  3987. assert input.exists(), "unexpectedly missing test case"
  3988. # run it through gvpack
  3989. gvpack = which("gvpack")
  3990. p = subprocess.run(
  3991. [gvpack, "-u", "-o", os.devnull, input],
  3992. stderr=subprocess.PIPE,
  3993. universal_newlines=True,
  3994. )
  3995. assert (
  3996. p.stderr.count("no hard-coded metrics for 'sans'") <= 1
  3997. ), "multiple identical “no hard-coded metrics” warnings printed"
  3998. @pytest.mark.parametrize("branch", (0, 1, 2, 3))
  3999. @pytest.mark.skipif(which("gvpr") is None, reason="gvpr not available")
  4000. def test_gvpr_switches(branch: int):
  4001. """
  4002. confirm the behavior of GVPR switch statements
  4003. """
  4004. # an input GVPR program with multiple blocks and switches
  4005. program = textwrap.dedent(
  4006. f"""\
  4007. BEGIN {{
  4008. switch ({branch}) {{
  4009. case 0:
  4010. printf("begin 0\\n");
  4011. break;
  4012. case 1:
  4013. printf("begin 1\\n");
  4014. break;
  4015. case 2:
  4016. printf("begin 2\\n");
  4017. break;
  4018. default:
  4019. printf("begin 3\\n");
  4020. break;
  4021. }}
  4022. }}
  4023. END {{
  4024. switch ({branch}) {{
  4025. case 0:
  4026. printf("end 0\\n");
  4027. break;
  4028. case 1:
  4029. printf("end 1\\n");
  4030. break;
  4031. case 2:
  4032. printf("end 2\\n");
  4033. break;
  4034. default:
  4035. printf("end 3\\n");
  4036. break;
  4037. }}
  4038. }}
  4039. """
  4040. )
  4041. # run this through GVPR with no input graph
  4042. gvpr_bin = which("gvpr")
  4043. result = subprocess.check_output(
  4044. [gvpr_bin, program], stdin=subprocess.DEVNULL, universal_newlines=True
  4045. )
  4046. # confirm we got the expected output
  4047. assert result == f"begin {branch}\nend {branch}\n", "incorrect GVPR switch behavior"
  4048. @pytest.mark.parametrize(
  4049. "statement,expected",
  4050. (
  4051. ('printf("%d", 5)', "5"),
  4052. ('printf("%d", 0)', "0"),
  4053. ('printf("%.0d", 0)', ""),
  4054. ('printf("%.0d", 1)', "1"),
  4055. ('printf("%.d", 2)', "2"),
  4056. ('printf("%d", -1)', "-1"),
  4057. ('printf("%.3d", 5)', "005"),
  4058. ('printf("%.3d", -5)', "-005"),
  4059. ('printf("%5.3d", 5)', " 005"),
  4060. ('printf("%-5.3d", -5)', "-005 "),
  4061. ('printf("%-d", 5)', "5"),
  4062. ('printf("%-+d", 5)', "+5"),
  4063. ('printf("%+-d", 5)', "+5"),
  4064. ('printf("%+d", -5)', "-5"),
  4065. ('printf("% d", 5)', " 5"),
  4066. ('printf("% .0d", 0)', " "),
  4067. ('printf("%03d", 5)', "005"),
  4068. ('printf("%03d", -5)', "-05"),
  4069. ('printf("% +d", 5)', "+5"),
  4070. ('printf("%-03d", -5)', "-5 "),
  4071. ('printf("%o", 5)', "5"),
  4072. ('printf("%o", 8)', "10"),
  4073. ('printf("%o", 0)', "0"),
  4074. ('printf("%.0o", 0)', ""),
  4075. ('printf("%.0o", 1)', "1"),
  4076. ('printf("%.3o", 5)', "005"),
  4077. ('printf("%.3o", 8)', "010"),
  4078. ('printf("%5.3o", 5)', " 005"),
  4079. ('printf("%u", 5)', "5"),
  4080. ('printf("%u", 0)', "0"),
  4081. ('printf("%.0u", 0)', ""),
  4082. ('printf("%.0u", 1)', "1"),
  4083. ('printf("%.3u", 5)', "005"),
  4084. ('printf("%5.3u", 5)', " 005"),
  4085. ('printf("%u", 5)', "5"),
  4086. ('printf("%u", 0)', "0"),
  4087. ('printf("%.0u", 0)', ""),
  4088. ('printf("%.0u", 1)', "1"),
  4089. ('printf("%.3u", 5)', "005"),
  4090. ('printf("%5.3u", 5)', " 005"),
  4091. ('printf("%-x", 5)', "5"),
  4092. ('printf("%03x", 5)', "005"),
  4093. ('printf("%-x", 5)', "5"),
  4094. ('printf("%03x", 5)', "005"),
  4095. ('printf("%-X", 5)', "5"),
  4096. ('printf("%03X", 5)', "005"),
  4097. ('printf("%.2s", "abc")', "ab"),
  4098. ('printf("%.6s", "abc")', "abc"),
  4099. ('printf("%5s", "abc")', " abc"),
  4100. ('printf("%-5s", "abc")', "abc "),
  4101. ('printf("%5.2s", "abc")', " ab"),
  4102. ('printf("%%")', "%"),
  4103. ),
  4104. )
  4105. @pytest.mark.skipif(which("gvpr") is None, reason="gvpr not available")
  4106. def test_gvpr_printf(statement: str, expected: str):
  4107. """
  4108. check various behaviors of `printf` in a GVPR program
  4109. """
  4110. # a program that performs the given `printf`
  4111. program = f"BEGIN {{ {statement}; }}"
  4112. # run this through GVPR with no input graph
  4113. gvpr_bin = which("gvpr")
  4114. result = subprocess.check_output(
  4115. [gvpr_bin, program], stdin=subprocess.DEVNULL, universal_newlines=True
  4116. )
  4117. # confirm we got the expected output
  4118. assert result == expected, "incorrect GVPR printf behavior"
  4119. usage_info = """\
  4120. Usage: dot [-Vv?] [-(GNE)name=val] [-(KTlso)<val>] <dot files>
  4121. (additional options for neato) [-x] [-n<v>]
  4122. (additional options for fdp) [-L(gO)] [-L(nUCT)<val>]
  4123. (additional options for config) [-cv]
  4124. -V - Print version and exit
  4125. -v - Enable verbose mode
  4126. -Gname=val - Set graph attribute 'name' to 'val'
  4127. -Nname=val - Set node attribute 'name' to 'val'
  4128. -Ename=val - Set edge attribute 'name' to 'val'
  4129. -Tv - Set output format to 'v'
  4130. -Kv - Set layout engine to 'v' (overrides default based on command name)
  4131. -lv - Use external library 'v'
  4132. -ofile - Write output to 'file'
  4133. -O - Automatically generate an output filename based on the input filename with a .'format' appended. (Causes all -ofile options to be ignored.)
  4134. -P - Internally generate a graph of the current plugins.
  4135. -q[l] - Set level of message suppression (=1)
  4136. -s[v] - Scale input by 'v' (=72)
  4137. -y - Invert y coordinate in output
  4138. -n[v] - No layout mode 'v' (=1)
  4139. -x - Reduce graph
  4140. -Lg - Don't use grid
  4141. -LO - Use old attractive force
  4142. -Ln<i> - Set number of iterations to i
  4143. -LU<i> - Set unscaled factor to i
  4144. -LC<v> - Set overlap expansion factor to v
  4145. -LT[*]<v> - Set temperature (temperature factor) to v
  4146. -c - Configure plugins (Writes $prefix/lib/graphviz/config
  4147. with available plugin information. Needs write privilege.)
  4148. -? - Print usage and exit
  4149. """
  4150. def test_dot_questionmarkV():
  4151. """
  4152. test the output from two short options combined
  4153. """
  4154. out = subprocess.check_output(
  4155. ["dot", "-?V"],
  4156. universal_newlines=True,
  4157. )
  4158. assert out == usage_info, "unexpected usage info"
  4159. def test_dot_randomV():
  4160. """
  4161. test the output from a malformed command
  4162. """
  4163. expected = f"Error: dot: option -r unrecognized\n\n{usage_info}"
  4164. proc = subprocess.run(
  4165. ["dot", "-randomV"],
  4166. stderr=subprocess.PIPE,
  4167. universal_newlines=True,
  4168. check=False,
  4169. )
  4170. assert proc.returncode != 0, "malformed options were accepted"
  4171. assert proc.stderr == expected, "unexpected usage info"
  4172. def test_dot_V():
  4173. """
  4174. test the output from `dot -V`
  4175. """
  4176. proc = subprocess.run(
  4177. ["dot", "-V"], stderr=subprocess.PIPE, universal_newlines=True, check=True
  4178. )
  4179. c_src = (Path(__file__).parent / "get-package-version.c").resolve()
  4180. assert c_src.exists(), "missing test case"
  4181. package_version, _ = run_c(c_src)
  4182. assert proc.stderr.startswith(
  4183. f"dot - graphviz version {package_version.strip()} ("
  4184. ), "unexpected -V info"
  4185. def test_dot_Vquestionmark():
  4186. """
  4187. test the output from two short options combined
  4188. """
  4189. proc = subprocess.run(
  4190. ["dot", "-V?"], stderr=subprocess.PIPE, universal_newlines=True, check=True
  4191. )
  4192. c_src = (Path(__file__).parent / "get-package-version.c").resolve()
  4193. assert c_src.exists(), "missing test case"
  4194. package_version, _ = run_c(c_src)
  4195. assert proc.stderr.startswith(
  4196. f"dot - graphviz version {package_version.strip()} ("
  4197. ), "unexpected -V info"
  4198. def test_dot_Vrandom():
  4199. """
  4200. test the output from a short option mixed with long
  4201. """
  4202. proc = subprocess.run(
  4203. ["dot", "-Vrandom"], stderr=subprocess.PIPE, universal_newlines=True, check=True
  4204. )
  4205. c_src = (Path(__file__).parent / "get-package-version.c").resolve()
  4206. assert c_src.exists(), "missing test case"
  4207. package_version, _ = run_c(c_src)
  4208. assert proc.stderr.startswith(
  4209. f"dot - graphviz version {package_version.strip()} ("
  4210. ), "unexpected -V info"
  4211. def test_pic_font_size():
  4212. """
  4213. font size in PIC output format should not be clamped down to 1
  4214. related to https://gitlab.com/graphviz/graphviz/-/issues/2487
  4215. """
  4216. # run a basic graph through PIC generation
  4217. src = "graph { a -- b; }"
  4218. pic = dot("pic", source=src)
  4219. # confirm we got a non-1 font size
  4220. m = re.search(r"^\.ps (\d+)", pic, flags=re.MULTILINE)
  4221. assert int(m.group(1)) > 1, "font size clamped down to 1"
  4222. @pytest.mark.skipif(which("mm2gv") is None, reason="mm2gv not available")
  4223. def test_mm_banner_overflow(tmp_path: Path):
  4224. """mm2gv should be robust against files with a corrupted banner"""
  4225. # construct a file with a corrupted banner > MM_MAX_TOKEN_LENGTH and < MM_MAX_LINE_LENGTH
  4226. mm = tmp_path / "matrix.mm"
  4227. mm.write_text(f"%{'a' * 10000}", encoding="utf-8")
  4228. # run this through mm2gv
  4229. ret = subprocess.call(["mm2gv", "-o", os.devnull, mm])
  4230. assert ret in (0, 1), "mm2gv crashed when processing malformed input"
  4231. assert ret == 1, "mm2gv did not reject malformed input"
  4232. def test_control_characters_in_error():
  4233. """
  4234. malformed input should not result in misleading control data making it to the
  4235. output terminal unfiltered
  4236. """
  4237. # Run something through Graphviz that will trigger an error where the error message
  4238. # will contain a color control sequence. This could be used to disrupt the user’s
  4239. # terminal in confusing ways.
  4240. src = 'graph { a[image="\033[31mfoo"]; }'
  4241. ret = subprocess.run(
  4242. ["dot", "-Tsvg", "-o", os.devnull],
  4243. input=src,
  4244. stderr=subprocess.PIPE,
  4245. universal_newlines=True,
  4246. )
  4247. assert "\033" not in ret.stderr, "control character appears in error message"
  4248. # Now try something more malicious. Use the backspace character to display a different
  4249. # filename in the error message to what was referenced.
  4250. src = 'graph { a[image="foo.svg\010\010\010png"]; }'
  4251. ret = subprocess.run(
  4252. ["dot", "-Tsvg", "-o", os.devnull],
  4253. input=src,
  4254. stderr=subprocess.PIPE,
  4255. universal_newlines=True,
  4256. )
  4257. assert "\010" not in ret.stderr, "control character appears in error message"
  4258. def test_fig_max_colors():
  4259. """
  4260. using a large number of colors should not crash the FIG renderer
  4261. """
  4262. # contruct a graph that uses well over 256 colors
  4263. buf = io.StringIO()
  4264. buf.write("graph {\n")
  4265. for red in range(256):
  4266. for green in range(10):
  4267. buf.write(f' n_{red}_{green}[color="#{red:02x}{green:02x}00"];\n')
  4268. buf.write("}\n")
  4269. # render this using the FIG renderer
  4270. dot("fig", source=buf.getvalue())
  4271. @pytest.mark.skipif(which("gvpr") is None, reason="gvpr not available")
  4272. def test_gvpr_s2f():
  4273. """
  4274. casting a string to floating point in GVPR should work
  4275. """
  4276. # a GVPR program that casts a string to floating point and prints the result
  4277. program = 'BEGIN { float x = (float)"1.5"; printf("%0.1f\\n", x); }'
  4278. # run this through GVPR with no input graph
  4279. gvpr_bin = which("gvpr")
  4280. result = subprocess.check_output(
  4281. [gvpr_bin, program], stdin=subprocess.DEVNULL, universal_newlines=True
  4282. )
  4283. # confirm we got the expected output
  4284. assert result == "1.5\n", "incorrect GVPR float cast behavior"
  4285. def test_changelog():
  4286. """
  4287. sanity checks on ../CHANGELOG.md
  4288. """
  4289. changelog = Path(__file__).parent / "../CHANGELOG.md"
  4290. assert changelog.exists(), "CHANGELOG.md missing"
  4291. with open(changelog, "rt", encoding="utf-8") as f:
  4292. for lineno, line in enumerate(f, 1):
  4293. ignore_h2 = False
  4294. # an exception for an old heading
  4295. if line == "## [2.42.3] and earlier\n":
  4296. ignore_h2 = True
  4297. # an exception for unreleased versions
  4298. if line.startswith("## ") and "Unreleased" in line:
  4299. ignore_h2 = True
  4300. if (m := re.match("##(?P<remainder>[^#].*)$", line)) and not ignore_h2:
  4301. expected_format = r" \[\d+\.\d+\.\d+\] [\-–] \d{4}-\d{2}-\d{2}$"
  4302. assert re.match(expected_format, m.group("remainder")), (
  4303. f"CHANGELOG.md:{lineno}: second-level heading did not match "
  4304. f'regex r"{expected_format}": {line}'
  4305. )
  4306. if m := re.match("###(?P<remainder>.*)$", line):
  4307. assert m.group("remainder") in (
  4308. " Added",
  4309. " Changed",
  4310. " Fixed",
  4311. " Removed",
  4312. ), f"CHANGELOG.md:{lineno}: unexpected third-level heading: {line}"
  4313. if m := re.match(
  4314. r"\[(?P<version>\d+\.\d+\.\d+)\]:(?P<remainder>.*)$", line
  4315. ):
  4316. prefix = " https://gitlab.com/graphviz/graphviz/compare/"
  4317. assert m.group("remainder").startswith(
  4318. prefix
  4319. ), f"CHANGELOG.md:{lineno}: unexpected finalized history link: {line}"
  4320. remainder = m.group("remainder")[len(prefix) :]
  4321. assert m.group("remainder").endswith(
  4322. f'...{m.group("version")}'
  4323. ), f"CHANGELOG.md:{lineno}: history link is for wrong version: {line}"
  4324. version_range = re.match(
  4325. r"(?P<start_major>\d+)\.(?P<start_minor>\d+)\.(?P<start_patch>\d+)"
  4326. r"\.\.\."
  4327. r"(?P<end_major>\d+)\.(?P<end_minor>\d+)\.(?P<end_patch>\d+)$",
  4328. remainder,
  4329. )
  4330. assert (
  4331. version_range
  4332. ), f"CHANGELOG.md:{lineno}: unexpected finalized history link: {line}"
  4333. start = tuple(
  4334. int(version_range.group(v))
  4335. for v in ("start_major", "start_minor", "start_patch")
  4336. )
  4337. end = tuple(
  4338. int(version_range.group(v))
  4339. for v in ("end_major", "end_minor", "end_patch")
  4340. )
  4341. assert (
  4342. start < end
  4343. ), f"CHANGELOG.md:{lineno}: invalid version range: {line}"
  4344. def test_agxbuf_print_nul():
  4345. """
  4346. `agxbprint` should not account for nor append a NUL byte
  4347. """
  4348. # find co-located test source
  4349. c_src = (Path(__file__).parent / "agxbuf-print-nul.c").resolve()
  4350. assert c_src.exists(), "missing test case"
  4351. lib = Path(__file__).parents[1] / "lib"
  4352. if platform.system() == "Windows" and not is_mingw():
  4353. cflags = [f"/I{lib}"]
  4354. else:
  4355. # GNU99 needed for `strndup`
  4356. cflags = ["-std=gnu99", f"-I{lib}"]
  4357. run_c(c_src, cflags=cflags)
  4358. def test_agxbuf_use_implicit_nul():
  4359. """
  4360. `agxbuf` should be able to use its entire memory as an inline string
  4361. """
  4362. # find co-located test source
  4363. c_src = (Path(__file__).parent / "agxbuf-use-implicit-nul.c").resolve()
  4364. assert c_src.exists(), "missing test case"
  4365. lib = Path(__file__).parents[1] / "lib"
  4366. if platform.system() == "Windows" and not is_mingw():
  4367. cflags = [f"/I{lib}"]
  4368. else:
  4369. # GNU99 needed for `strndup`
  4370. cflags = ["-std=gnu99", f"-I{lib}"]
  4371. run_c(c_src, cflags=cflags)
  4372. @pytest.mark.skipif(which("edgepaint") is None, reason="edgepaint not available")
  4373. def test_edgepaint_error_message():
  4374. """
  4375. when failing to open its output, edgepaint should not dereference a null
  4376. pointer
  4377. """
  4378. # try to open a non-existent file
  4379. edgepaint = which("edgepaint")
  4380. proc = subprocess.run(
  4381. [edgepaint, "-o", "/a/nonexistent/path"],
  4382. stdout=subprocess.DEVNULL,
  4383. stderr=subprocess.PIPE,
  4384. universal_newlines=True,
  4385. )
  4386. # edgepaint should name itself in the error message, not “(null)”
  4387. assert re.search(
  4388. r"\bedgepaint\b", proc.stderr
  4389. ), "edgepaint does not know its own name"