|
| 1 | +#!/usr/bin/python3 |
| 2 | + |
| 3 | +import sys |
| 4 | +import time |
| 5 | +import threading |
| 6 | +import random |
| 7 | +import tkinter |
| 8 | + |
| 9 | +import unittest |
| 10 | +class TestThreads(unittest.TestCase): |
| 11 | + def test_threads(self): |
| 12 | + import subprocess |
| 13 | + p = subprocess.Popen([sys.executable, __file__, "run"], |
| 14 | + stdout=subprocess.PIPE, |
| 15 | + stderr=subprocess.PIPE) |
| 16 | + self.addCleanup(p.stdout.close) |
| 17 | + self.addCleanup(p.stderr.close) |
| 18 | + try: |
| 19 | + #Test code is designed to complete in a few seconds |
| 20 | + stdout, stderr = p.communicate(timeout=10) |
| 21 | + except subprocess.TimeoutExpired: |
| 22 | + p.kill() |
| 23 | + stdout, stderr = p.communicate() |
| 24 | + self.fail("Test code hang. Stderr: " + repr(stderr)) |
| 25 | + rc = p.returncode |
| 26 | + self.assertTrue(rc == 0, |
| 27 | + "Nonzero exit status: " + str(rc) + "; stderr:" + repr(stderr)) |
| 28 | + self.assertTrue(len(stderr) == 0, "stderr: " + repr(stderr)) |
| 29 | + |
| 30 | + |
| 31 | + |
| 32 | +running = True |
| 33 | + |
| 34 | +class Track(threading.Thread): |
| 35 | + """ |
| 36 | + Calculates coordinates for a ballistic track |
| 37 | + with random angle and velocity |
| 38 | + and fires the callback to draw each consecutive segment. |
| 39 | + """ |
| 40 | + def __init__(self, track_number, draw_callback): |
| 41 | + """ |
| 42 | + :param track_number: ordinal |
| 43 | + :param draw_callback: fn(track_number, x, y) |
| 44 | + that draws the extention of the specified track |
| 45 | + to the specified point. The callback must keep track |
| 46 | + of any previous coordinates itself. |
| 47 | + """ |
| 48 | + threading.Thread.__init__(self) |
| 49 | + #self.setDaemon(True) |
| 50 | + self.track_number = track_number |
| 51 | + self.draw_callback = draw_callback |
| 52 | + |
| 53 | + def run(self): |
| 54 | + #starting point for the track |
| 55 | + y = 0.0001 #height |
| 56 | + x = 999.0 #range |
| 57 | + #initial velocities |
| 58 | + yVel = 400. + random.random() * 200. |
| 59 | + xVel = -200. - random.random() * 150. |
| 60 | + # Stop drawing when the track hits the ground |
| 61 | + while y > 0 and running: |
| 62 | + #How long to sleep, in seconds, between track updates |
| 63 | + time.sleep(0.01) #realism: >1 Fun: 0.01 |
| 64 | + |
| 65 | + yVel -= 9.8 #gravity, more or less |
| 66 | + xVel *= .9998 #air resistance |
| 67 | + |
| 68 | + y += yVel |
| 69 | + x += xVel |
| 70 | + |
| 71 | + if running: |
| 72 | + self.draw_callback(self.track_number, x, y) |
| 73 | + |
| 74 | + |
| 75 | +class App: |
| 76 | + """ |
| 77 | + The main app logic |
| 78 | + """ |
| 79 | + def __init__(self, window): |
| 80 | + self.track_no = 0 #last index of track added |
| 81 | + self.track_coordinates = {} #coords of track tips |
| 82 | + self.threads = [] |
| 83 | + |
| 84 | + self.window = window |
| 85 | + self.frame = tkinter.Frame(window) |
| 86 | + self.frame.pack() |
| 87 | + self.graph = tkinter.Canvas(self.frame) |
| 88 | + self.graph.pack() |
| 89 | + |
| 90 | + self.t_cleanup = threading.Thread(target=self.tf_cleanup) |
| 91 | + self.window.after(0, self.add_tracks, 5) |
| 92 | + self.window.after(1000, self.t_cleanup.start) |
| 93 | + |
| 94 | + def add_tracks(self,depth): |
| 95 | + if depth>0: self.window.after(5, self.add_tracks, depth-1) |
| 96 | + tracks_to_add = 40 |
| 97 | + start_no = self.track_no |
| 98 | + self.track_no += tracks_to_add |
| 99 | + for self.track_no in range(start_no, self.track_no): |
| 100 | + #self.window.after(t, self.add_tracks) |
| 101 | + #self.track_no += 1 #Make a new track number |
| 102 | + #if (self.track_no > 40): |
| 103 | + t = Track(self.track_no, self.draw_track_segment) |
| 104 | + self.threads.append(t) |
| 105 | + t.start() |
| 106 | + |
| 107 | + def tf_cleanup(self): |
| 108 | + global running |
| 109 | + running = False |
| 110 | + for t in self.threads: t.join() |
| 111 | + self.window.after(0,self.window.destroy) |
| 112 | + |
| 113 | + def draw_track_segment(self, track_no, x, y): |
| 114 | + # x & y are virtual coordinates for the purpose of simulation. |
| 115 | + #To convert them to screen coordinates, |
| 116 | + # we scale them down so the graphs fit into the window, and move the origin. |
| 117 | + # Y is also inverted because in canvas, the UL corner is the origin. |
| 118 | + newsx, newsy = (250.+x/100., 150.-y/100.) |
| 119 | + |
| 120 | + try: |
| 121 | + (oldsx, oldsy) = self.track_coordinates[track_no] |
| 122 | + except KeyError: |
| 123 | + pass |
| 124 | + else: |
| 125 | + self.graph.create_line(oldsx, oldsy, |
| 126 | + newsx, newsy) |
| 127 | + self.track_coordinates[track_no] = (newsx, newsy) |
| 128 | + |
| 129 | + def go(self): |
| 130 | + self.window.mainloop() |
| 131 | + |
| 132 | +tests_gui = (TestThreads,) |
| 133 | + |
| 134 | +if __name__=='__main__': |
| 135 | + import sys,os |
| 136 | + if sys.argv[1:]==['run']: |
| 137 | + if os.name == 'nt': |
| 138 | + import ctypes |
| 139 | + #the bug causes crashes |
| 140 | + ctypes.windll.kernel32.SetErrorMode(3) |
| 141 | + App(tkinter.Tk()).go() |
| 142 | + else: |
| 143 | + from test.support import run_unittest |
| 144 | + run_unittest(*tests_gui) |
0 commit comments