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