mathandy / svgpathtools

A collection of tools for manipulating and analyzing SVG Path objects and Bezier curves.
MIT License
557 stars 142 forks source link

`TestGeneration.test_path_parsing` and `TestPath.test_hash` tests fail on aarch64 with version 1.4.4 #163

Open ggardet opened 2 years ago

ggardet commented 2 years ago

TestGeneration.test_path_parsing and TestPath.test_hash tests fail on aarch64 with version 1.4.4:

[   35s] =================================== FAILURES ===================================
[   35s] _______________________ TestGeneration.test_path_parsing _______________________
[   35s] 
[   35s] self = <test_generation.TestGeneration testMethod=test_path_parsing>
[   35s] 
[   35s]     def test_path_parsing(self):
[   35s]         """Examples from the SVG spec"""
[   35s]         paths = [
[   35s]             'M 100,100 L 300,100 L 200,300 Z',
[   35s]             'M 0,0 L 50,20 M 100,100 L 300,100 L 200,300 Z',
[   35s]             'M 100,100 L 200,200',
[   35s]             'M 100,200 L 200,100 L -100,-200',
[   35s]             'M 100,200 C 100,100 250,100 250,200 S 400,300 400,200',
[   35s]             'M 100,200 C 100,100 400,100 400,200',
[   35s]             'M 100,500 C 25,400 475,400 400,500',
[   35s]             'M 100,800 C 175,700 325,700 400,800',
[   35s]             'M 600,200 C 675,100 975,100 900,200',
[   35s]             'M 600,500 C 600,350 900,650 900,500',
[   35s]             'M 600,800 C 625,700 725,700 750,800 S 875,900 900,800',
[   35s]             'M 200,300 Q 400,50 600,300 T 1000,300',
[   35s]             'M -3.4E+38,3.4E+38 L -3.4E-38,3.4E-38',
[   35s]             'M 0,0 L 50,20 M 50,20 L 200,100 Z',
[   35s]             'M 600,350 L 650,325 A 25,25 -30 0,1 700,300 L 750,275',
[   35s]         ]
[   35s]         float_paths = [
[   35s]             'M 100.0,100.0 L 300.0,100.0 L 200.0,300.0 L 100.0,100.0',
[   35s]             'M 0.0,0.0 L 50.0,20.0 M 100.0,100.0 L 300.0,100.0 L 200.0,300.0 L 100.0,100.0',
[   35s]             'M 100.0,100.0 L 200.0,200.0',
[   35s]             'M 100.0,200.0 L 200.0,100.0 L -100.0,-200.0',
[   35s]             'M 100.0,200.0 C 100.0,100.0 250.0,100.0 250.0,200.0 C 250.0,300.0 400.0,300.0 400.0,200.0',
[   35s]             'M 100.0,200.0 C 100.0,100.0 400.0,100.0 400.0,200.0',
[   35s]             'M 100.0,500.0 C 25.0,400.0 475.0,400.0 400.0,500.0',
[   35s]             'M 100.0,800.0 C 175.0,700.0 325.0,700.0 400.0,800.0',
[   35s]             'M 600.0,200.0 C 675.0,100.0 975.0,100.0 900.0,200.0',
[   35s]             'M 600.0,500.0 C 600.0,350.0 900.0,650.0 900.0,500.0',
[   35s]             'M 600.0,800.0 C 625.0,700.0 725.0,700.0 750.0,800.0 C 775.0,900.0 875.0,900.0 900.0,800.0',
[   35s]             'M 200.0,300.0 Q 400.0,50.0 600.0,300.0 Q 800.0,550.0 1000.0,300.0',
[   35s]             'M -3.4e+38,3.4e+38 L -3.4e-38,3.4e-38',
[   35s]             'M 0.0,0.0 L 50.0,20.0 L 200.0,100.0 L 50.0,20.0',
[   35s]             ('M 600.0,350.0 L 650.0,325.0 A 27.9508497187,27.9508497187 -30.0 0,1 700.0,300.0 L 750.0,275.0',  # Python 2
[   35s]              'M 600.0,350.0 L 650.0,325.0 A 27.95084971874737,27.95084971874737 -30.0 0,1 700.0,300.0 L 750.0,275.0')  # Python 3
[   35s]         ]
[   35s]     
[   35s]         for path, flpath in zip(paths[::-1], float_paths[::-1]):
[   35s]             # Note:  Python 3 and Python 2 differ in the number of digits
[   35s]             # truncated when returning a string representation of a float
[   35s]             parsed_path = parse_path(path)
[   35s]             res = parsed_path.d()
[   35s]             if isinstance(flpath, tuple):
[   35s]                 option3 = res == flpath[1]  # Python 3
[   35s]                 flpath = flpath[0]
[   35s]             else:
[   35s]                 option3 = False
[   35s]             option1 = res == path
[   35s]             option2 = res == flpath
[   35s]     
[   35s]             msg = ('\npath =\n {}\nflpath =\n {}\nparse_path(path).d() =\n {}'
[   35s]                    ''.format(path, flpath, res))
[   35s] >           self.assertTrue(option1 or option2 or option3, msg=msg)
[   35s] E           AssertionError: False is not true : 
[   35s] E           path =
[   35s] E            M 600,350 L 650,325 A 25,25 -30 0,1 700,300 L 750,275
[   35s] E           flpath =
[   35s] E            M 600.0,350.0 L 650.0,325.0 A 27.9508497187,27.9508497187 -30.0 0,1 700.0,300.0 L 750.0,275.0
[   35s] E           parse_path(path).d() =
[   35s] E            M 600.0,350.0 L 650.0,325.0 A 27.950849718747367,27.950849718747367 -30.0 0,1 700.0,300.0 L 750.0,275.0
[   35s] 
[   35s] test/test_generation.py:63: AssertionError
[   35s] ______________________________ TestPath.test_hash ______________________________
[   35s] 
[   35s] self = <test_path.TestPath testMethod=test_hash>
[   35s] 
[   35s]     def test_hash(self):
[   35s]         line1 = Line(600.5 + 350.5j, 650.5 + 325.5j)
[   35s]         arc1 = Arc(650 + 325j, 25 + 25j, -30, 0, 1, 700 + 300j)
[   35s]         arc2 = Arc(650 + 325j, 30 + 25j, -30, 0, 0, 700 + 300j)
[   35s]         cub1 = CubicBezier(650 + 325j, 25 + 25j, -30, 700 + 300j)
[   35s]         cub2 = CubicBezier(700 + 300j, 800 + 400j, 750 + 200j, 600 + 100j)
[   35s]         quad3 = QuadraticBezier(600 + 100j, 600, 600 + 300j)
[   35s]         linez = Line(600 + 300j, 600 + 350j)
[   35s]     
[   35s]         bezpath = Path(line1, cub1, cub2, quad3)
[   35s]         bezpathz = Path(line1, cub1, cub2, quad3, linez)
[   35s]         path = Path(line1, arc1, cub2, quad3)
[   35s]         pathz = Path(line1, arc1, cub2, quad3, linez)
[   35s]         lpath = Path(linez)
[   35s]         qpath = Path(quad3)
[   35s]         cpath = Path(cub1)
[   35s]         apath = Path(arc1, arc2)
[   35s]     
[   35s]         test_curves = [bezpath, bezpathz, path, pathz, lpath, qpath, cpath,
[   35s]                        apath, line1, arc1, arc2, cub1, cub2, quad3, linez]
[   35s]     
[   35s]         # this is necessary due to changes to the builtin `hash` function
[   35s]         user_hash_seed = os.environ.get("PYTHONHASHSEED", "")
[   35s]         os.environ["PYTHONHASHSEED"] = "314"
[   35s]         if version_info >= (3, 8):
[   35s]             expected_hashes = [
[   35s]                 -6073024107272494569, -2519772625496438197, 8726412907710383506,
[   35s]                 2132930052750006195, 3112548573593977871, 991446120749438306,
[   35s]                 -5589397644574569777, -4438808571483114580, -3125333407400456536,
[   35s]                 -4418099728831808951, 702646573139378041, -6331016786776229094,
[   35s]                 5053050772929443013, 6102272282813527681, -5385294438006156225
[   35s]             ]
[   35s]         elif (3, 2) <= version_info < (3, 8):
[   35s]             expected_hashes = [
[   35s]                 -5662973462929734898, 5166874115671195563, 5223434942701471389,
[   35s]                 -7224979960884350294, -5178990533869800243, -4003140762934044601,
[   35s]                 8575549467429100514, -6692132994808317852, 1594848578230132678,
[   35s]                 -6374833902132909499, 4188352014604112779, -5090374009174854814,
[   35s]                 -7093907105533857815, 2036243740727202243, -8108488067585685407
[   35s]             ]
[   35s]         else:
[   35s]     
[   35s]             expected_hashes = [
[   35s]                 -5762846476463470127, -138736730317965290, -2005041722222729058,
[   35s]                 8448700906794235291, -5178990533869800243, -4003140762934044601,
[   35s]                 8575549467429100514, 5166859065265868968, 1373103287265872323,
[   35s]                 -1022491904150314631, 4188352014604112779, -5090374009174854814,
[   35s]                 -7093907105533857815, 2036243740727202243, -8108488067585685407
[   35s]             ]
[   35s]     
[   35s]         if version_info.major == 2 and os.name == 'nt':
[   35s]             # the expected hash values for 2.7 apparently differed on Windows
[   35s]             # if you work in Windows and want to fix this test, please do
[   35s]             return
[   35s]     
[   35s]         for c, h in zip(test_curves, expected_hashes):
[   35s] >           self.assertTrue(hash(c) == h, msg="hash {} was expected for curve = {}".format(h, c))
[   35s] E           AssertionError: False is not true : hash 8726412907710383506 was expected for curve = Path(Line(start=(600.5+350.5j), end=(650.5+325.5j)),
[   35s] E                Arc(start=(650+325j), radius=(27.950849718747367+27.950849718747367j), rotation=-30, large_arc=False, sweep=True, end=(700+300j)),
[   35s] E                CubicBezier(start=(700+300j), control1=(800+400j), control2=(750+200j), end=(600+100j)),
[   35s] E                QuadraticBezier(start=(600+100j), control=600, end=(600+300j)))
[   35s] 
[   35s] test/test_path.py:778: AssertionError
StefanBruens commented 2 years ago

The hash fail has very likely the same root cause as the parsing test earlier, the Arc radius autoscaling. (Failing hash test case is path = Path(line1, arc1, cub2, quad3)).

As can be seen, the actual values for the autoscaled radius are the same, just with different accuracy depending on architecture and python version.

For the d() method, the format string for each coorditate could be changed to {:16g}, which rounds the value so the representation becomes less architecture dependent:

>>> "{:.16g}".format(27.950849718747367)
'27.95084971874737'

This of course only changes the parser test case, the hash test would still fail.

Another option would be to change the test cases so the arc radiuses are no longer autoscaled.