1import pickle
2import unittest
3from test import support
4
5turtle = support.import_module('turtle')
6Vec2D = turtle.Vec2D
7
8test_config = """\
9width = 0.75
10height = 0.8
11canvwidth = 500
12canvheight = 200
13leftright = 100
14topbottom = 100
15mode = world
16colormode = 255
17delay = 100
18undobuffersize = 10000
19shape = circle
20pencolor  = red
21fillcolor  = blue
22resizemode  = auto
23visible  = None
24language = english
25exampleturtle = turtle
26examplescreen = screen
27title = Python Turtle Graphics
28using_IDLE = ''
29"""
30
31test_config_two = """\
32# Comments!
33# Testing comments!
34pencolor  = red
35fillcolor  = blue
36visible  = False
37language = english
38# Some more
39# comments
40using_IDLE = False
41"""
42
43invalid_test_config = """
44pencolor = red
45fillcolor: blue
46visible = False
47"""
48
49
50class TurtleConfigTest(unittest.TestCase):
51
52    def get_cfg_file(self, cfg_str):
53        self.addCleanup(support.unlink, support.TESTFN)
54        with open(support.TESTFN, 'w') as f:
55            f.write(cfg_str)
56        return support.TESTFN
57
58    def test_config_dict(self):
59
60        cfg_name = self.get_cfg_file(test_config)
61        parsed_cfg = turtle.config_dict(cfg_name)
62
63        expected = {
64            'width' : 0.75,
65            'height' : 0.8,
66            'canvwidth' : 500,
67            'canvheight': 200,
68            'leftright': 100,
69            'topbottom': 100,
70            'mode': 'world',
71            'colormode': 255,
72            'delay': 100,
73            'undobuffersize': 10000,
74            'shape': 'circle',
75            'pencolor' : 'red',
76            'fillcolor' : 'blue',
77            'resizemode' : 'auto',
78            'visible' : None,
79            'language': 'english',
80            'exampleturtle': 'turtle',
81            'examplescreen': 'screen',
82            'title': 'Python Turtle Graphics',
83            'using_IDLE': '',
84        }
85
86        self.assertEqual(parsed_cfg, expected)
87
88    def test_partial_config_dict_with_comments(self):
89
90        cfg_name = self.get_cfg_file(test_config_two)
91        parsed_cfg = turtle.config_dict(cfg_name)
92
93        expected = {
94            'pencolor': 'red',
95            'fillcolor': 'blue',
96            'visible': False,
97            'language': 'english',
98            'using_IDLE': False,
99        }
100
101        self.assertEqual(parsed_cfg, expected)
102
103    def test_config_dict_invalid(self):
104
105        cfg_name = self.get_cfg_file(invalid_test_config)
106
107        with support.captured_stdout() as stdout:
108            parsed_cfg = turtle.config_dict(cfg_name)
109
110        err_msg = stdout.getvalue()
111
112        self.assertIn('Bad line in config-file ', err_msg)
113        self.assertIn('fillcolor: blue', err_msg)
114
115        self.assertEqual(parsed_cfg, {
116            'pencolor': 'red',
117            'visible': False,
118        })
119
120
121class VectorComparisonMixin:
122
123    def assertVectorsAlmostEqual(self, vec1, vec2):
124        if len(vec1) != len(vec2):
125            self.fail("Tuples are not of equal size")
126        for idx, (i, j) in enumerate(zip(vec1, vec2)):
127            self.assertAlmostEqual(
128                i, j, msg='values at index {} do not match'.format(idx))
129
130class Multiplier:
131
132    def __mul__(self, other):
133        return f'M*{other}'
134
135    def __rmul__(self, other):
136        return f'{other}*M'
137
138
139class TestVec2D(VectorComparisonMixin, unittest.TestCase):
140
141    def test_constructor(self):
142        vec = Vec2D(0.5, 2)
143        self.assertEqual(vec[0], 0.5)
144        self.assertEqual(vec[1], 2)
145        self.assertIsInstance(vec, Vec2D)
146
147        self.assertRaises(TypeError, Vec2D)
148        self.assertRaises(TypeError, Vec2D, 0)
149        self.assertRaises(TypeError, Vec2D, (0, 1))
150        self.assertRaises(TypeError, Vec2D, vec)
151        self.assertRaises(TypeError, Vec2D, 0, 1, 2)
152
153    def test_repr(self):
154        vec = Vec2D(0.567, 1.234)
155        self.assertEqual(repr(vec), '(0.57,1.23)')
156
157    def test_equality(self):
158        vec1 = Vec2D(0, 1)
159        vec2 = Vec2D(0.0, 1)
160        vec3 = Vec2D(42, 1)
161        self.assertEqual(vec1, vec2)
162        self.assertEqual(vec1, tuple(vec1))
163        self.assertEqual(tuple(vec1), vec1)
164        self.assertNotEqual(vec1, vec3)
165        self.assertNotEqual(vec2, vec3)
166
167    def test_pickling(self):
168        vec = Vec2D(0.5, 2)
169        for proto in range(pickle.HIGHEST_PROTOCOL + 1):
170            with self.subTest(proto=proto):
171                pickled = pickle.dumps(vec, protocol=proto)
172                unpickled = pickle.loads(pickled)
173                self.assertEqual(unpickled, vec)
174                self.assertIsInstance(unpickled, Vec2D)
175
176    def _assert_arithmetic_cases(self, test_cases, lambda_operator):
177        for test_case in test_cases:
178            with self.subTest(case=test_case):
179
180                ((first, second), expected) = test_case
181
182                op1 = Vec2D(*first)
183                op2 = Vec2D(*second)
184
185                result = lambda_operator(op1, op2)
186
187                expected = Vec2D(*expected)
188
189                self.assertVectorsAlmostEqual(result, expected)
190
191    def test_vector_addition(self):
192
193        test_cases = [
194            (((0, 0), (1, 1)), (1.0, 1.0)),
195            (((-1, 0), (2, 2)), (1, 2)),
196            (((1.5, 0), (1, 1)), (2.5, 1)),
197        ]
198
199        self._assert_arithmetic_cases(test_cases, lambda x, y: x + y)
200
201    def test_vector_subtraction(self):
202
203        test_cases = [
204            (((0, 0), (1, 1)), (-1, -1)),
205            (((10.625, 0.125), (10, 0)), (0.625, 0.125)),
206        ]
207
208        self._assert_arithmetic_cases(test_cases, lambda x, y: x - y)
209
210    def test_vector_multiply(self):
211
212        vec1 = Vec2D(10, 10)
213        vec2 = Vec2D(0.5, 3)
214        answer = vec1 * vec2
215        expected = 35
216        self.assertAlmostEqual(answer, expected)
217
218        vec = Vec2D(0.5, 3)
219        expected = Vec2D(5, 30)
220        self.assertVectorsAlmostEqual(vec * 10, expected)
221        self.assertVectorsAlmostEqual(10 * vec, expected)
222        self.assertVectorsAlmostEqual(vec * 10.0, expected)
223        self.assertVectorsAlmostEqual(10.0 * vec, expected)
224
225        M = Multiplier()
226        self.assertEqual(vec * M, Vec2D(f"{vec[0]}*M", f"{vec[1]}*M"))
227        self.assertEqual(M * vec, f'M*{vec}')
228
229    def test_vector_negative(self):
230        vec = Vec2D(10, -10)
231        expected = (-10, 10)
232        self.assertVectorsAlmostEqual(-vec, expected)
233
234    def test_distance(self):
235        vec = Vec2D(6, 8)
236        expected = 10
237        self.assertEqual(abs(vec), expected)
238
239        vec = Vec2D(0, 0)
240        expected = 0
241        self.assertEqual(abs(vec), expected)
242
243        vec = Vec2D(2.5, 6)
244        expected = 6.5
245        self.assertEqual(abs(vec), expected)
246
247    def test_rotate(self):
248
249        cases = [
250            (((0, 0), 0), (0, 0)),
251            (((0, 1), 90), (-1, 0)),
252            (((0, 1), -90), (1, 0)),
253            (((1, 0), 180), (-1, 0)),
254            (((1, 0), 360), (1, 0)),
255        ]
256
257        for case in cases:
258            with self.subTest(case=case):
259                (vec, rot), expected = case
260                vec = Vec2D(*vec)
261                got = vec.rotate(rot)
262                self.assertVectorsAlmostEqual(got, expected)
263
264
265class TestTNavigator(VectorComparisonMixin, unittest.TestCase):
266
267    def setUp(self):
268        self.nav = turtle.TNavigator()
269
270    def test_goto(self):
271        self.nav.goto(100, -100)
272        self.assertAlmostEqual(self.nav.xcor(), 100)
273        self.assertAlmostEqual(self.nav.ycor(), -100)
274
275    def test_pos(self):
276        self.assertEqual(self.nav.pos(), self.nav._position)
277        self.nav.goto(100, -100)
278        self.assertEqual(self.nav.pos(), self.nav._position)
279
280    def test_left(self):
281        self.assertEqual(self.nav._orient, (1.0, 0))
282        self.nav.left(90)
283        self.assertVectorsAlmostEqual(self.nav._orient, (0.0, 1.0))
284
285    def test_right(self):
286        self.assertEqual(self.nav._orient, (1.0, 0))
287        self.nav.right(90)
288        self.assertVectorsAlmostEqual(self.nav._orient, (0, -1.0))
289
290    def test_reset(self):
291        self.nav.goto(100, -100)
292        self.assertAlmostEqual(self.nav.xcor(), 100)
293        self.assertAlmostEqual(self.nav.ycor(), -100)
294        self.nav.reset()
295        self.assertAlmostEqual(self.nav.xcor(), 0)
296        self.assertAlmostEqual(self.nav.ycor(), 0)
297
298    def test_forward(self):
299        self.nav.forward(150)
300        expected = Vec2D(150, 0)
301        self.assertVectorsAlmostEqual(self.nav.position(), expected)
302
303        self.nav.reset()
304        self.nav.left(90)
305        self.nav.forward(150)
306        expected = Vec2D(0, 150)
307        self.assertVectorsAlmostEqual(self.nav.position(), expected)
308
309        self.assertRaises(TypeError, self.nav.forward, 'skldjfldsk')
310
311    def test_backwards(self):
312        self.nav.back(200)
313        expected = Vec2D(-200, 0)
314        self.assertVectorsAlmostEqual(self.nav.position(), expected)
315
316        self.nav.reset()
317        self.nav.right(90)
318        self.nav.back(200)
319        expected = Vec2D(0, 200)
320        self.assertVectorsAlmostEqual(self.nav.position(), expected)
321
322    def test_distance(self):
323        self.nav.forward(100)
324        expected = 100
325        self.assertAlmostEqual(self.nav.distance(Vec2D(0,0)), expected)
326
327    def test_radians_and_degrees(self):
328        self.nav.left(90)
329        self.assertAlmostEqual(self.nav.heading(), 90)
330        self.nav.radians()
331        self.assertAlmostEqual(self.nav.heading(), 1.57079633)
332        self.nav.degrees()
333        self.assertAlmostEqual(self.nav.heading(), 90)
334
335    def test_towards(self):
336
337        coordinates = [
338            # coordinates, expected
339            ((100, 0), 0.0),
340            ((100, 100), 45.0),
341            ((0, 100), 90.0),
342            ((-100, 100), 135.0),
343            ((-100, 0), 180.0),
344            ((-100, -100), 225.0),
345            ((0, -100), 270.0),
346            ((100, -100), 315.0),
347        ]
348
349        for (x, y), expected in coordinates:
350            self.assertEqual(self.nav.towards(x, y), expected)
351            self.assertEqual(self.nav.towards((x, y)), expected)
352            self.assertEqual(self.nav.towards(Vec2D(x, y)), expected)
353
354    def test_heading(self):
355
356        self.nav.left(90)
357        self.assertAlmostEqual(self.nav.heading(), 90)
358        self.nav.left(45)
359        self.assertAlmostEqual(self.nav.heading(), 135)
360        self.nav.right(1.6)
361        self.assertAlmostEqual(self.nav.heading(), 133.4)
362        self.assertRaises(TypeError, self.nav.right, 'sdkfjdsf')
363        self.nav.reset()
364
365        rotations = [10, 20, 170, 300]
366        result = sum(rotations) % 360
367        for num in rotations:
368            self.nav.left(num)
369        self.assertEqual(self.nav.heading(), result)
370        self.nav.reset()
371
372        result = (360-sum(rotations)) % 360
373        for num in rotations:
374            self.nav.right(num)
375        self.assertEqual(self.nav.heading(), result)
376        self.nav.reset()
377
378        rotations = [10, 20, -170, 300, -210, 34.3, -50.2, -10, -29.98, 500]
379        sum_so_far = 0
380        for num in rotations:
381            if num < 0:
382                self.nav.right(abs(num))
383            else:
384                self.nav.left(num)
385            sum_so_far += num
386            self.assertAlmostEqual(self.nav.heading(), sum_so_far % 360)
387
388    def test_setheading(self):
389        self.nav.setheading(102.32)
390        self.assertAlmostEqual(self.nav.heading(), 102.32)
391        self.nav.setheading(-123.23)
392        self.assertAlmostEqual(self.nav.heading(), (-123.23) % 360)
393        self.nav.setheading(-1000.34)
394        self.assertAlmostEqual(self.nav.heading(), (-1000.34) % 360)
395        self.nav.setheading(300000)
396        self.assertAlmostEqual(self.nav.heading(), 300000%360)
397
398    def test_positions(self):
399        self.nav.forward(100)
400        self.nav.left(90)
401        self.nav.forward(-200)
402        self.assertVectorsAlmostEqual(self.nav.pos(), (100.0, -200.0))
403
404    def test_setx_and_sety(self):
405        self.nav.setx(-1023.2334)
406        self.nav.sety(193323.234)
407        self.assertVectorsAlmostEqual(self.nav.pos(), (-1023.2334, 193323.234))
408
409    def test_home(self):
410        self.nav.left(30)
411        self.nav.forward(-100000)
412        self.nav.home()
413        self.assertVectorsAlmostEqual(self.nav.pos(), (0,0))
414        self.assertAlmostEqual(self.nav.heading(), 0)
415
416    def test_distance_method(self):
417        self.assertAlmostEqual(self.nav.distance(30, 40), 50)
418        vec = Vec2D(0.22, .001)
419        self.assertAlmostEqual(self.nav.distance(vec), 0.22000227271553355)
420        another_turtle = turtle.TNavigator()
421        another_turtle.left(90)
422        another_turtle.forward(10000)
423        self.assertAlmostEqual(self.nav.distance(another_turtle), 10000)
424
425
426class TestTPen(unittest.TestCase):
427
428    def test_pendown_and_penup(self):
429
430        tpen = turtle.TPen()
431
432        self.assertTrue(tpen.isdown())
433        tpen.penup()
434        self.assertFalse(tpen.isdown())
435        tpen.pendown()
436        self.assertTrue(tpen.isdown())
437
438    def test_showturtle_hideturtle_and_isvisible(self):
439
440        tpen = turtle.TPen()
441
442        self.assertTrue(tpen.isvisible())
443        tpen.hideturtle()
444        self.assertFalse(tpen.isvisible())
445        tpen.showturtle()
446        self.assertTrue(tpen.isvisible())
447
448
449if __name__ == '__main__':
450    unittest.main()
451