Passed
Branch master (a5c0c0)
by Mikael
03:26 queued 19s
created

test_main   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 252
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 149
dl 0
loc 252
ccs 129
cts 129
cp 1
rs 9.44
c 0
b 0
f 0
wmc 37
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