fun

Add test file for COMPN assignment

Author
Maarten 'Vngngdn' Vangeneugden
Date
Oct. 19, 2017, 8:51 p.m.
Hash
5dea5c7653135e3f0055483029712b19244c172f
Parent
46f629cb608fa34fb7a82a17edab14a0a99f65f1
Modified file
compn-test.py

compn-test.py

359 additions and 0 deletions.

View changes Hide changes
+
1
Copyright 2016 Maarten Vangeneugden
+
2
+
3
Licensed under the Apache License, Version 2.0 (the "License");
+
4
you may not use this file except in compliance with the License.
+
5
You may obtain a copy of the License at
+
6
+
7
    http://www.apache.org/licenses/LICENSE-2.0
+
8
+
9
Unless required by applicable law or agreed to in writing, software
+
10
distributed under the License is distributed on an "AS IS" BASIS,
+
11
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+
12
See the License for the specific language governing permissions and
+
13
limitations under the License.
+
14
+
15
Contact me at: maarten.vangeneugden@student.uhasselt.be
+
16
"""
+
17
+
18
# That was the legal hurr durr fluff. Now for the actual program:
+
19
from __future__ import print_function
+
20
# READ THIS IF YOU USE THIS TEST FOR THE FIRST TIME.
+
21
#
+
22
# This test was designed to test the interactive server assignment. It will run a
+
23
# series of tests to check whether the program works as supposed to.
+
24
#
+
25
# This test is built to deal with edge cases, extreme input, and attempts to
+
26
# break the server. If your server passes this test, you can be assured it's
+
27
# (relatively speaking) robust enough, even though I cannot make 100%
+
28
# guarantees.
+
29
#
+
30
# To run this test, start the server, and then run this file.
+
31
#
+
32
# There are a couple of flags you can set:
+
33
# -1: Let the tests run for the first server. EXPERIMENTAL, may not work!
+
34
# -2: Default. Let the tests run for the 'Interactive information server' task.
+
35
# --verbose: Print all details.
+
36
#
+
37
# This test sends large bogus strings to the server (which may have lengths of
+
38
# up to 100). Be sure to accept enough bytes on your server in order to not
+
39
# cause the data to queue up too much. (socket.recv(2048) should suffice)
+
40
# You can also set the max string length in the functions that create them.
+
41
#
+
42
# By Maarten Vangeneugden - 1438256
+
43
+
44
import socket as SocketModule  # Altered namespacing for readability.
+
45
import random  # For server input randomization
+
46
import string  # For 'em wacky bogus strings bitch
+
47
import time  # Time control.
+
48
import itertools  # For generating command permutations.
+
49
import sys  # For command-line argument handling.
+
50
+
51
# Constants and program settings (Not necessarily constant, although you can
+
52
# treat them as such)
+
53
CLIENT_IP = "127.0.0.1"  # If not on localhost, change accordingly.
+
54
SERVER_ADDRESS = "localhost"
+
55
SERVER_PORT = 2000
+
56
SERVER = 2  # The server to test for. 1 is the first, 2 the interactive one.
+
57
# The verbose mode can be turned on by the user using the --verbose flag, and
+
58
# will print all details. Defaults to False, printing only the most necessary
+
59
# data.
+
60
VERBOSE_MODE = False
+
61
RECONNECTIONS_AMOUNT = 2  # The amount of times the client will connect.
+
62
# The amount of seconds to wait in order to trigger a timeout.
+
63
# Don't make this as low as possible, but take a reasonable time. 5 is a nice
+
64
# minimum.
+
65
TIMEOUT_WAIT = 8  # SET THIS MANUALLY if your server takes longer before timeout.
+
66
+
67
# Functions
+
68
def verbose_print(string):
+
69
    """ Prints the given string to the output, but only if the user has set the
+
70
    verbose mode. """
+
71
    if VERBOSE_MODE:
+
72
        print(string)
+
73
+
74
def flag_handling(arguments):
+
75
    """ Checks the given flags, and sets the program's settings accordingly. """
+
76
    # Settings that can be altered:
+
77
    global SERVER
+
78
    global VERBOSE_MODE
+
79
+
80
    for argument in arguments:
+
81
        if argument == "--verbose":  # Activate verbose mode
+
82
            VERBOSE_MODE = True
+
83
            print("Verbose mode activated.")
+
84
        elif argument == "-1":
+
85
            SERVER = 1
+
86
            print("Set the server to test to 1.")
+
87
        elif argument == "-2":
+
88
            SERVER = 2
+
89
            print("Set the server to interactive (2).")
+
90
+
91
def run_after_erronous_input(socket):
+
92
    """ Sends a series of random bogus commands, and checks whether the server
+
93
    keeps running. """
+
94
    # Change these settings to your likings if you want to, but be aware that
+
95
    # extreme inputs (like strings of length 10000) may cause problems that
+
96
    # aren't really there.
+
97
    TEST_AMOUNT = 100  # The amount of bogus strings that will be generated.
+
98
    MAX_STRING_LENGTH = 100  # The maximum length of the bogus strings.
+
99
    no_error = True
+
100
+
101
    for i in range(TEST_AMOUNT):
+
102
        string_length = random.randint(0, MAX_STRING_LENGTH)
+
103
        bogus_command = \
+
104
        ''.join(random.SystemRandom().choice(string.printable) for _ in range(string_length))
+
105
        if bogus_command != "TIME" \
+
106
        and bogus_command != "IP" \
+
107
        and bogus_command != "EXIT":  # Just in case, but imagine the chance...
+
108
            try:
+
109
                verbose_print("Testing if "+ bogus_command +" makes the server \
+
110
                        crash...")
+
111
                socket.send(bogus_command)
+
112
                if SERVER == 2:
+
113
                    socket.send("\n")
+
114
                socket.recv(2048)  # Receiving what's being sent to discard it.
+
115
            except:
+
116
                print("The server would not accept any more commands after \
+
117
                receiving the following command: " + bogus_command)
+
118
                no_error = False
+
119
                break  # Abort looping the for loop
+
120
    
+
121
    return no_error
+
122
+
123
def check_command_validity(socket, command, answer):
+
124
    """ Initiates a series of tests that will attempt to make the server respond
+
125
    invalid when a correct command sequence is sent, or respond valid when a
+
126
    false command is sent.
+
127
    """
+
128
    assert command_handled_correctly(socket, command, answer)
+
129
    assert command_misspelled_reject(socket, command, answer)
+
130
+
131
def command_handled_correctly(socket, command, answer):
+
132
    """ Checks whether the server handles the given command correctly. """
+
133
    if SERVER == 1:  # Only test the first command.
+
134
        socket.send(command)
+
135
        response = socket.recv(2048)
+
136
        if response == time.ctime():
+
137
            return True
+
138
        else: 
+
139
            print("When sending "+ command +", the expected answer was "+ \
+
140
                    answer +", but "+ response +" was returned by the server.")
+
141
            return False
+
142
+
143
    elif SERVER == 2:  # Interactive server
+
144
        command_sequences = generate_correct_commands(command)
+
145
        for sequence in command_sequences:
+
146
            verbose_print("Checking if "+ str(sequence) +" is accepted...")
+
147
            for part in sequence:
+
148
                socket.send(part)
+
149
            socket.send("\n")  # Executes the command.
+
150
            response = socket.recv(2048)
+
151
            if response != answer:
+
152
                """ NOTE: It can happen that the time is off by 1 second,
+
153
                because of the time it takes to run tests. If so, this condition
+
154
                will update the answer accordingly.
+
155
                """
+
156
                if response == time.ctime():
+
157
                    answer = time.ctime()
+
158
                else:
+
159
                    print("When sending "+ command +", the expected answer was "+ \
+
160
                            answer +", but "+ response +" was returned by the server.")
+
161
                    print("This sequence was sent to the server:")
+
162
                    print(sequence)
+
163
                    return False
+
164
            else:
+
165
                verbose_print(str(sequence) +" returned a valid response.")
+
166
+
167
    # This stage is only reached when the command is handled properly.
+
168
    return True
+
169
+
170
def command_misspelled_reject(socket, correct_command, answer):
+
171
    """ Sends a series of misspelled variations of an accepted command, in an
+
172
    attempt to trick the server in returning a response to said command, which
+
173
    in fact, should be rejected.
+
174
    """
+
175
    false_commands = generate_false_commands(correct_command)
+
176
    trailing_space_false_commands = [" " + correct_command, correct_command + " "]
+
177
    false_commands.extend(trailing_space_false_commands)
+
178
    while correct_command in false_commands:  # FIXME
+
179
        false_commands.remove(correct_command)
+
180
+
181
    for false_command in false_commands:
+
182
        verbose_print("Checking if "+ false_command +" is rejected...")
+
183
        socket.send(false_command)
+
184
        if SERVER == 2:
+
185
            socket.send("\n")
+
186
        """ Note that waiting for an answer still breaks when nothing is
+
187
        returned, because:
+
188
        "Incorrect commandos entered by the client should be ignored, and an
+
189
        error must be returned to the client." ~Assignment
+
190
        """
+
191
        response = socket.recv(2048)
+
192
        if answer == response and false_command != correct_command:
+
193
            print(false_command +" was accepted, which should be rejected!")
+
194
            return False  # The server responded to a false command.
+
195
        else:
+
196
            verbose_print(false_command +" successfully rejected!")
+
197
+
198
    return True  # Not a single false command was accepted.
+
199
+
200
def generate_correct_commands(correct_command):
+
201
    """ Generates all sequences of the given command that, if passed to the
+
202
    interactive server, should yield a correct response.
+
203
    For example: Sending "ABC" makes this function return
+
204
    [
+
205
        ["ABC"],
+
206
        ["A", "BC"],
+
207
        ["AB", "C"],
+
208
        ["A", "B", "C"]
+
209
    ]
+
210
    """
+
211
    correct_commands = [[correct_command]]  # Trivial list.
+
212
    for i in range(1, len(correct_command)):
+
213
        # So this is actually kind of like Lisp, but it does the job: Decide
+
214
        # first and rest of the command, and then continue the function
+
215
        # recursively on the rest, concatenating that result on the first, and
+
216
        # appending all that to the list of correct commands.
+
217
        first = [correct_command[:i]]  # From begin to i
+
218
        rest = correct_command[i:]  # From i to end
+
219
        for recursive_command in generate_correct_commands(rest):
+
220
            correct_commands.append(first + recursive_command)
+
221
+
222
    # And finally, return that to the caller:
+
223
    return correct_commands
+
224
    
+
225
def generate_false_commands(correct_command):
+
226
    """ Generates a list of false commands, similar to the given command.
+
227
    Basically, it creates a permutation of the casing, divided in lists, and so
+
228
    on.
+
229
    It may not be exhaustive, that would actually make running the test run far
+
230
    too long.
+
231
    """
+
232
    false_commands = []  # This will contain all false commands.
+
233
    if len(correct_command) > 1:
+
234
        # Recursively handle commands less 1 character:
+
235
        false_commands.extend(generate_false_commands( \
+
236
                correct_command[0:len(correct_command)-1]))
+
237
        false_commands.extend(generate_false_commands( \
+
238
                correct_command[1:]))
+
239
    # Handling all possible variations of the current length:
+
240
    temp_false_commands = []
+
241
    for element in itertools.permutations(correct_command, \
+
242
            len(correct_command)):
+
243
        temp_false_commands.append("".join(element))
+
244
+
245
    swapped_false_commands = []
+
246
    for false_command in temp_false_commands:
+
247
        for i in range(len(false_command)):
+
248
            new_false_command = false_command[0:i] + \
+
249
                false_command[i:].swapcase()
+
250
            swapped_false_commands.append(new_false_command)
+
251
    temp_false_commands.extend(swapped_false_commands)
+
252
+
253
    # Putting all false commands together:
+
254
    false_commands.extend(temp_false_commands)
+
255
    return false_commands  # And returning them all.
+
256
        
+
257
+
258
+
259
+
260
# --- Start of program ---
+
261
+
262
+
263
+
264
+
265
if len(sys.argv) >= 2:  # If the user has set flags, handle them:
+
266
    flag_handling(sys.argv)
+
267
+
268
"""
+
269
The test will run as follows:
+
270
    1. A predetermined amount of times, a socket will be created, that will try
+
271
    to connect to the server using the given data.
+
272
    2. Random strings will be sent, in an attempt to make the server shut down.
+
273
    3. The client will try different ways to receive the time of the server.
+
274
    4. The client will timeout, and look if the server reacts properly.
+
275
    5. The client will check for IP address data.
+
276
    6. The socket will be closed, and reopened by the loop.
+
277
    7. If the loop ends, a last connection will be started, to shut the server
+
278
       down.
+
279
    8. When that succeeds, all tests have passed.
+
280
"""
+
281
+
282
for i in range(RECONNECTIONS_AMOUNT):
+
283
    print("Client test (#" + str(i+1) +")")
+
284
    # Creating the socket for use:
+
285
    socket = SocketModule.socket(SocketModule.AF_INET, SocketModule.SOCK_STREAM)
+
286
    socket.connect((SERVER_ADDRESS, SERVER_PORT))
+
287
+
288
    # Testing bogus input:
+
289
    print("Testing if the server breaks on bogus input...")
+
290
    assert run_after_erronous_input(socket)
+
291
    print("Server seems to not crash on erronous input.")
+
292
+
293
    # Testing time output:
+
294
    # XXX: It's very likely that the speed of the time test overlaps with the
+
295
    # previous test, causing the server to block TIME. A sleep of 1 should handle
+
296
    # this.
+
297
    time.sleep(1)
+
298
    print("Testing if time gets printed correctly...")
+
299
    check_command_validity(socket, "TIME", time.ctime())
+
300
    print("Time seems to be handled fine.")
+
301
+
302
    # Testing if we can connect after a timeout:
+
303
    print("Testing if timing out works correctly...")
+
304
    # A simple test: Test if it doesn't time out when it shouldn't, and test if
+
305
    # it times out when it should.
+
306
    verbose_print("Test whether the server can be tricked in timing out after 2\
+
307
    seconds...")
+
308
    time.sleep(2)  # This should NOT yield a timeout.
+
309
    socket.send("I didn't time out!")
+
310
    if SERVER == 2:
+
311
        socket.send("\n")
+
312
    data = socket.recv(2048)
+
313
    assert data != "", "The client timed out, but that wasn't supposed to \
+
314
            happen after 2 seconds!"
+
315
+
316
    verbose_print("Server didn't time out after 2 seconds.")
+
317
    verbose_print("Now we will trigger a timeout.")
+
318
+
319
    time.sleep(TIMEOUT_WAIT)
+
320
    data = socket.recv(2048)
+
321
    if data == "":  # TCP sends "" when a disconnect takes place.
+
322
        print("Timed out as expected!")
+
323
        socket = SocketModule.socket(SocketModule.AF_INET, SocketModule.SOCK_STREAM)
+
324
        socket.connect((SERVER_ADDRESS, SERVER_PORT))
+
325
    else:
+
326
        assert False, "The program was still connected to the server. The \
+
327
        client should have timed out by this point, but didn't."    
+
328
+
329
+
330
    # Testing IP data:
+
331
    time.sleep(1)
+
332
    print("Testing if the client's address is sent correctly...")
+
333
    check_command_validity(socket, "IP", CLIENT_IP)
+
334
    print("Addresses seem to be handled fine.")
+
335
+
336
+
337
    socket.close()  # Closing the socket.
+
338
    print("Client test #"+ str(i+1) +" passed!")
+
339
+
340
# Final test: Tell the server to close down, and assert that we can't connect
+
341
# anymore.
+
342
print("Checking if the server doesn't exit on wrong EXIT commands...")
+
343
socket = SocketModule.socket(SocketModule.AF_INET, SocketModule.SOCK_STREAM)
+
344
socket.connect((SERVER_ADDRESS, SERVER_PORT))
+
345
assert command_misspelled_reject(socket, "EXIT", None)
+
346
# Now, we close the server down, and attempt to connect.
+
347
socket.send("EXIT\n")
+
348
socket.close()
+
349
+
350
try:
+
351
    socket.connect((SERVER_ADDRESS, SERVER_PORT))
+
352
    assert False, "The program was able to connect to the server, despite \
+
353
    having sent a shutdown command."
+
354
except SocketModule.error:
+
355
    print("Server shut down on command.")
+
356
+
357
# At this stage, all loops have been done, and all tests have passed.
+
358
print("All tests passed!")
+
359