|
1 | | -import subprocess |
2 | | -import os |
3 | | -import sys |
4 | | -import time |
5 | | -import sqlite3 |
6 | | -import datetime |
7 | | -import urllib.parse |
8 | | - |
9 | | -import irc.client |
10 | | -import pytest |
11 | | -import tempora.timing |
12 | | - |
13 | | -import pmxbot.dictlib |
14 | | - |
15 | | - |
16 | | -class TestingClient: |
17 | | - """ |
18 | | - A simple client simulating a user other than the pmxbot |
19 | | - """ |
20 | | - |
21 | | - def __init__(self, server, port, nickname): |
22 | | - self.reactor = irc.client.Reactor() |
23 | | - self.c = self.reactor.server() |
24 | | - self.c.connect(server, port, nickname) |
25 | | - self.reactor.process_once(0.1) |
26 | | - self.channels = set() |
27 | | - |
28 | | - def join(self, channel): |
29 | | - self.c.join(channel) |
30 | | - self.channels.add(channel) |
31 | | - |
32 | | - def send_message(self, channel, message): |
33 | | - if channel not in self.channels: |
34 | | - self.join(channel) |
35 | | - self.c.privmsg(channel, message) |
36 | | - time.sleep(0.05) |
37 | | - |
38 | | - |
39 | | -class PmxbotHarness: |
40 | | - config = pmxbot.dictlib.ConfigDict( |
41 | | - server_port=6668, |
42 | | - bot_nickname='pmxbotTest', |
43 | | - log_channels=['#logged'], |
44 | | - other_channels=['#inane'], |
45 | | - database="sqlite:tests/functional/pmxbot.sqlite", |
46 | | - ) |
47 | | - |
48 | | - @classmethod |
49 | | - def setup_class(cls): |
50 | | - """Start an IRC server, launch the bot, and ask it to do stuff""" |
51 | | - path = os.path.dirname(os.path.abspath(__file__)) |
52 | | - cls.config_fn = os.path.join(path, 'testconf.yaml') |
53 | | - cls.config.to_yaml(cls.config_fn) |
54 | | - cls.dbfile = urllib.parse.urlparse(cls.config['database']).path |
55 | | - cls.db = sqlite3.connect(cls.dbfile) |
56 | | - env = os.environ.copy() |
57 | | - # copy the current sys.path to PYTHONPATH so subprocesses have access |
58 | | - # to libs pulled by tests_require |
59 | | - env['PYTHONPATH'] = os.pathsep.join(sys.path) |
60 | | - try: |
61 | | - cmd = [sys.executable, '-m', 'irc.server', '-p', '6668', '-l', 'debug'] |
62 | | - cls.server = subprocess.Popen(cmd, env=env) |
63 | | - except OSError: |
64 | | - pytest.skip("Unable to launch irc server.") |
65 | | - time.sleep(0.5) |
66 | | - # add './plugins' to the path so we get some pmxbot commands specific |
67 | | - # for testing. |
68 | | - plugins = os.path.join(path, 'plugins') |
69 | | - env['PYTHONPATH'] = os.pathsep.join([plugins, env['PYTHONPATH']]) |
70 | | - try: |
71 | | - # Launch pmxbot using Python directly (rather than through |
72 | | - # the console entry point, which can't be properly |
73 | | - # .terminate()d on Windows. |
74 | | - cmd = [sys.executable, '-m', 'pmxbot', cls.config_fn] |
75 | | - cls.bot = subprocess.Popen(cmd, env=env) |
76 | | - except OSError: |
77 | | - pytest.skip("Unable to launch pmxbot (pmxbot must be installed)") |
78 | | - cls.wait_for_tables() |
79 | | - cls.wait_for_output() |
80 | | - if cls.bot.poll() is not None: |
81 | | - pytest.skip("Bot did not start up properly") |
82 | | - cls.client = TestingClient('localhost', 6668, 'testingbot') |
83 | | - |
84 | | - @classmethod |
85 | | - def wait_for_output(cls): |
86 | | - """ |
87 | | - Wait for 'Running with config' in cls.bot.output |
88 | | - """ |
89 | | - if cls.bot.poll() is not None: |
90 | | - return |
91 | | - # stubbed |
92 | | - time.sleep(5) |
93 | | - |
94 | | - @classmethod |
95 | | - def wait_for_tables(cls, timeout=30): |
96 | | - watch = tempora.timing.Stopwatch() |
97 | | - while watch.split() < datetime.timedelta(seconds=timeout): |
98 | | - try: |
99 | | - cls.check_logs('#check') |
100 | | - return |
101 | | - except Exception: |
102 | | - # short-circuit if the bot has stopped |
103 | | - if cls.bot.poll() is not None: |
104 | | - return |
105 | | - time.sleep(0.2) |
106 | | - |
107 | | - @classmethod |
108 | | - def check_logs(cls, channel='', nick='', message=''): |
109 | | - if channel.startswith('#'): |
110 | | - channel = channel[1:] |
111 | | - time.sleep(0.2) |
112 | | - cursor = cls.db.cursor() |
113 | | - query = ( |
114 | | - "select * from logs where 1=1" |
115 | | - + " and channel = :channel" * bool(channel) |
116 | | - + " and nick = :nick" * bool(nick) |
117 | | - + " and message = :message" * bool(message) |
118 | | - ) |
119 | | - cursor.execute(query, locals()) |
120 | | - res = cursor.fetchall() |
121 | | - print(res) |
122 | | - return len(res) >= 1 |
123 | | - |
124 | | - @classmethod |
125 | | - def teardown_class(cls): |
126 | | - os.remove(cls.config_fn) |
127 | | - if hasattr(cls, 'bot') and not cls.bot.poll(): |
128 | | - cls.bot.terminate() |
129 | | - cls.bot.wait() |
130 | | - if hasattr(cls, 'server') and cls.server.poll() is None: |
131 | | - cls.server.terminate() |
132 | | - cls.server.wait() |
133 | | - if hasattr(cls, 'db'): |
134 | | - cls.db.rollback() |
135 | | - cls.db.close() |
136 | | - del cls.db |
137 | | - if hasattr(cls, 'client'): |
138 | | - del cls.client |
139 | | - # wait up to 10 seconds for the file to be removable |
140 | | - for x in range(100): |
141 | | - try: |
142 | | - if os.path.isfile(cls.dbfile): |
143 | | - os.remove(cls.dbfile) |
144 | | - break |
145 | | - except OSError: |
146 | | - time.sleep(0.1) |
147 | | - else: |
148 | | - raise RuntimeError('Could not remove log db', cls.dbfile) |
0 commit comments