1
|
|
|
#! /usr/bin/env python3 |
2
|
|
|
# -*- coding: utf-8 -*- |
3
|
|
|
|
4
|
|
|
""" |
5
|
|
|
Tests for the main launcher |
6
|
|
|
""" |
7
|
|
|
|
8
|
|
|
import argparse |
9
|
|
|
import contextlib |
10
|
|
|
import io |
11
|
|
|
import os |
12
|
|
|
import sys |
13
|
|
|
from unittest import TestCase |
14
|
|
|
|
15
|
|
|
from main import mergeOptionsWithConfigFile, parseOptions, determineProtocol, MSG_VERSION, createBot |
16
|
|
|
from irc_bot import IrcBot |
17
|
|
|
from discord_bot import DiscordBot |
18
|
|
|
|
19
|
|
|
|
20
|
|
|
class ConfigMergeTest(TestCase): |
21
|
|
|
"""Test merging a config file with a dict""" |
22
|
|
|
|
23
|
|
|
def assertMergedConfig(self, config, fileName, expected): |
24
|
|
|
"""Merge dict with file and assert the result matches expected""" |
25
|
|
|
configFile = os.path.join("testConfigs", f"{fileName}.json") |
26
|
|
|
actualConfig = mergeOptionsWithConfigFile(config, configFile) |
27
|
|
|
self.assertEqual(actualConfig, expected) |
28
|
|
|
|
29
|
|
|
|
30
|
|
|
def testEmpty(self): |
31
|
|
|
"""Empty into empty should equal empty""" |
32
|
|
|
self.assertMergedConfig({}, "empty", {}) |
33
|
|
|
|
34
|
|
|
def testAddSingleParameter(self): |
35
|
|
|
"""Add a single parameter to an empty config""" |
36
|
|
|
new = { |
37
|
|
|
"single": "test" |
38
|
|
|
} |
39
|
|
|
expected = { |
40
|
|
|
"single": "test" |
41
|
|
|
} |
42
|
|
|
self.assertMergedConfig(new, "empty", expected) |
43
|
|
|
|
44
|
|
|
def testAddSingleParameterOverwrites(self): |
45
|
|
|
"""Add a single parameter to a config that contains it already""" |
46
|
|
|
new = { |
47
|
|
|
"single": "test" |
48
|
|
|
} |
49
|
|
|
expected = { |
50
|
|
|
"single": "original" |
51
|
|
|
} |
52
|
|
|
self.assertMergedConfig(new, "single", expected) |
53
|
|
|
|
54
|
|
|
def testAddSingleParameterMerges(self): |
55
|
|
|
"""Add a single parameter to a config that contains a different one""" |
56
|
|
|
new = { |
57
|
|
|
"new": "test" |
58
|
|
|
} |
59
|
|
|
expected = { |
60
|
|
|
"new" : "test", |
61
|
|
|
"single" : "original" |
62
|
|
|
} |
63
|
|
|
self.assertMergedConfig(new, "single", expected) |
64
|
|
|
|
65
|
|
|
class ConfigParseTest(TestCase): |
66
|
|
|
"""Test parsing options into a config""" |
67
|
|
|
|
68
|
|
|
SAMPLE_CONFIG = { |
69
|
|
|
"server": "localhost", |
70
|
|
|
"port": 6667, |
71
|
|
|
"channel": "#dbwebb", |
72
|
|
|
"nick": "marvin", |
73
|
|
|
"realname": "Marvin The All Mighty dbwebb-bot", |
74
|
|
|
"ident": "password" |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
CHANGED_CONFIG = { |
78
|
|
|
"server": "remotehost", |
79
|
|
|
"port": 1234, |
80
|
|
|
"channel": "#db-o-webb", |
81
|
|
|
"nick": "imposter", |
82
|
|
|
"realname": "where is marvin?", |
83
|
|
|
"ident": "identify" |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
def testOverrideHardcodedParameters(self): |
87
|
|
|
"""Test that all the hard coded parameters can be overridden from commandline""" |
88
|
|
|
for parameter in ["server", "port", "channel", "nick", "realname", "ident"]: |
89
|
|
|
sys.argv = ["./main.py", f"--{parameter}", str(self.CHANGED_CONFIG.get(parameter))] |
90
|
|
|
actual = parseOptions(self.SAMPLE_CONFIG) |
91
|
|
|
self.assertEqual(actual.get(parameter), self.CHANGED_CONFIG.get(parameter)) |
92
|
|
|
|
93
|
|
|
def testOverrideMultipleParameters(self): |
94
|
|
|
"""Test that multiple parameters can be overridden from commandline""" |
95
|
|
|
sys.argv = ["./main.py", "--server", "dbwebb.se", "--port", "5432"] |
96
|
|
|
actual = parseOptions(self.SAMPLE_CONFIG) |
97
|
|
|
self.assertEqual(actual.get("server"), "dbwebb.se") |
98
|
|
|
self.assertEqual(actual.get("port"), 5432) |
99
|
|
|
|
100
|
|
|
def testOverrideWithFile(self): |
101
|
|
|
"""Test that parameters can be overridden with the --config option""" |
102
|
|
|
configFile = os.path.join("testConfigs", "server.json") |
103
|
|
|
sys.argv = ["./main.py", "--config", configFile] |
104
|
|
|
actual = parseOptions(self.SAMPLE_CONFIG) |
105
|
|
|
self.assertEqual(actual.get("server"), "irc.dbwebb.se") |
106
|
|
|
|
107
|
|
|
def testOverridePrecedenceConfigFirst(self): |
108
|
|
|
"""Test that proper precedence is considered. From most to least significant it should be: |
109
|
|
|
explicit parameter -> parameter in --config file -> default """ |
110
|
|
|
|
111
|
|
|
configFile = os.path.join("testConfigs", "server.json") |
112
|
|
|
sys.argv = ["./main.py", "--config", configFile, "--server", "important.com"] |
113
|
|
|
actual = parseOptions(self.SAMPLE_CONFIG) |
114
|
|
|
self.assertEqual(actual.get("server"), "important.com") |
115
|
|
|
|
116
|
|
|
def testOverridePrecedenceParameterFirst(self): |
117
|
|
|
"""Test that proper precedence is considered. From most to least significant it should be: |
118
|
|
|
explicit parameter -> parameter in --config file -> default """ |
119
|
|
|
|
120
|
|
|
configFile = os.path.join("testConfigs", "server.json") |
121
|
|
|
sys.argv = ["./main.py", "--server", "important.com", "--config", configFile] |
122
|
|
|
actual = parseOptions(self.SAMPLE_CONFIG) |
123
|
|
|
self.assertEqual(actual.get("server"), "important.com") |
124
|
|
|
|
125
|
|
|
def testBannedParameters(self): |
126
|
|
|
"""Don't allow config, help and version as parameters, as those options are special""" |
127
|
|
|
for bannedParameter in ["config", "help", "version"]: |
128
|
|
|
with self.assertRaises(argparse.ArgumentError): |
129
|
|
|
parseOptions({bannedParameter: "test"}) |
130
|
|
|
|
131
|
|
|
|
132
|
|
|
class FormattingTest(TestCase): |
133
|
|
|
"""Test the parameters that cause printouts""" |
134
|
|
|
|
135
|
|
|
USAGE = ("usage: main.py [-h] [-v] [--config CONFIG] [--server SERVER] [--port PORT] " |
136
|
|
|
"[--channel CHANNEL] [--nick NICK] [--realname REALNAME] [--ident IDENT]\n" |
137
|
|
|
" [{irc,discord}]\n") |
138
|
|
|
|
139
|
|
|
OPTIONS = ("positional arguments:\n {irc,discord}\n\n" |
140
|
|
|
"options:\n" |
141
|
|
|
" -h, --help show this help message and exit\n" |
142
|
|
|
" -v, --version\n" |
143
|
|
|
" --config CONFIG\n" |
144
|
|
|
" --server SERVER\n" |
145
|
|
|
" --port PORT\n" |
146
|
|
|
" --channel CHANNEL\n" |
147
|
|
|
" --nick NICK\n" |
148
|
|
|
" --realname REALNAME\n" |
149
|
|
|
" --ident IDENT") |
150
|
|
|
|
151
|
|
|
|
152
|
|
|
@classmethod |
153
|
|
|
def setUpClass(cls): |
154
|
|
|
"""Set the terminal width to 160 to prevent the tests from failing on small terminals""" |
155
|
|
|
os.environ["COLUMNS"] = "160" |
156
|
|
|
|
157
|
|
|
|
158
|
|
|
def assertPrintOption(self, options, returnCode, output): |
159
|
|
|
"""Assert that parseOptions returns a certain code and prints a certain output""" |
160
|
|
|
with self.assertRaises(SystemExit) as e: |
161
|
|
|
s = io.StringIO() |
162
|
|
|
with contextlib.redirect_stdout(s): |
163
|
|
|
sys.argv = ["./main.py"] + [options] |
164
|
|
|
parseOptions(ConfigParseTest.SAMPLE_CONFIG) |
165
|
|
|
self.assertEqual(e.exception.code, returnCode) |
166
|
|
|
self.assertEqual(s.getvalue(), output+"\n") # extra newline added by print() |
167
|
|
|
|
168
|
|
|
|
169
|
|
|
def testHelpPrintout(self): |
170
|
|
|
"""Test that a help is printed when providing the --help flag""" |
171
|
|
|
self.assertPrintOption("--help", 0, f"{self.USAGE}\n{self.OPTIONS}") |
172
|
|
|
|
173
|
|
|
def testHelpPrintoutShort(self): |
174
|
|
|
"""Test that a help is printed when providing the -h flag""" |
175
|
|
|
self.assertPrintOption("-h", 0, f"{self.USAGE}\n{self.OPTIONS}") |
176
|
|
|
|
177
|
|
|
def testVersionPrintout(self): |
178
|
|
|
"""Test that the version is printed when provided the --version flag""" |
179
|
|
|
self.assertPrintOption("--version", 0, MSG_VERSION) |
180
|
|
|
|
181
|
|
|
def testVersionPrintoutShort(self): |
182
|
|
|
"""Test that the version is printed when provided the -v flag""" |
183
|
|
|
self.assertPrintOption("-v", 0, MSG_VERSION) |
184
|
|
|
|
185
|
|
|
def testUnhandledOption(self): |
186
|
|
|
"""Test that unknown options gives an error""" |
187
|
|
|
with self.assertRaises(SystemExit) as e: |
188
|
|
|
s = io.StringIO() |
189
|
|
|
expectedError = f"{self.USAGE}main.py: error: unrecognized arguments: -g\n" |
190
|
|
|
with contextlib.redirect_stderr(s): |
191
|
|
|
sys.argv = ["./main.py", "-g"] |
192
|
|
|
parseOptions(ConfigParseTest.SAMPLE_CONFIG) |
193
|
|
|
self.assertEqual(e.exception.code, 2) |
194
|
|
|
self.assertEqual(s.getvalue(), expectedError) |
195
|
|
|
|
196
|
|
|
def testUnhandledArgument(self): |
197
|
|
|
"""Test that any argument gives an error""" |
198
|
|
|
with self.assertRaises(SystemExit) as e: |
199
|
|
|
s = io.StringIO() |
200
|
|
|
expectedError = (f"{self.USAGE}main.py: error: argument protocol: " |
201
|
|
|
"invalid choice: 'arg' (choose from 'irc', 'discord')\n") |
202
|
|
|
with contextlib.redirect_stderr(s): |
203
|
|
|
sys.argv = ["./main.py", "arg"] |
204
|
|
|
parseOptions(ConfigParseTest.SAMPLE_CONFIG) |
205
|
|
|
self.assertEqual(e.exception.code, 2) |
206
|
|
|
self.assertEqual(s.getvalue(), expectedError) |
207
|
|
|
|
208
|
|
|
class TestArgumentParsing(TestCase): |
209
|
|
|
"""Test parsing argument to determine whether to launch as irc or discord bot """ |
210
|
|
|
def testDetermineDiscordProtocol(self): |
211
|
|
|
"""Test that the it's possible to give argument to start the bot as a discord bot""" |
212
|
|
|
sys.argv = ["main.py", "discord"] |
213
|
|
|
protocol = determineProtocol() |
214
|
|
|
self.assertEqual(protocol, "discord") |
215
|
|
|
|
216
|
|
|
def testDetermineIRCProtocol(self): |
217
|
|
|
"""Test that the it's possible to give argument to start the bot as an irc bot""" |
218
|
|
|
sys.argv = ["main.py", "irc"] |
219
|
|
|
protocol = determineProtocol() |
220
|
|
|
self.assertEqual(protocol, "irc") |
221
|
|
|
|
222
|
|
|
def testDetermineIRCProtocolisDefault(self): |
223
|
|
|
"""Test that if no argument is given, irc is the default""" |
224
|
|
|
sys.argv = ["main.py"] |
225
|
|
|
protocol = determineProtocol() |
226
|
|
|
self.assertEqual(protocol, "irc") |
227
|
|
|
|
228
|
|
|
def testDetermineConfigThrowsOnInvalidProto(self): |
229
|
|
|
"""Test that determineProtocol throws error on unsupported protocols""" |
230
|
|
|
sys.argv = ["main.py", "gopher"] |
231
|
|
|
with self.assertRaises(SystemExit) as e: |
232
|
|
|
determineProtocol() |
233
|
|
|
self.assertEqual(e.exception.code, 2) |
234
|
|
|
|
235
|
|
|
class TestBotFactoryMethod(TestCase): |
236
|
|
|
"""Test that createBot returns expected instances of Bots""" |
237
|
|
|
def testCreateIRCBot(self): |
238
|
|
|
"""Test that an irc bot can be created""" |
239
|
|
|
bot = createBot("irc") |
240
|
|
|
self.assertIsInstance(bot, IrcBot) |
241
|
|
|
|
242
|
|
|
def testCreateDiscordBot(self): |
243
|
|
|
"""Test that a discord bot can be created""" |
244
|
|
|
bot = createBot("discord") |
245
|
|
|
self.assertIsInstance(bot, DiscordBot) |
246
|
|
|
|
247
|
|
|
def testCreateUnsupportedProtocolThrows(self): |
248
|
|
|
"""Test that trying to create a bot with an unsupported protocol will throw exception""" |
249
|
|
|
with self.assertRaises(ValueError) as e: |
250
|
|
|
createBot("gopher") |
251
|
|
|
self.assertEqual(str(e.exception), "Unsupported protocol: gopher") |
252
|
|
|
|