Add test file for COMPN assignment
- Author
- Maarten 'Vngngdn' Vangeneugden
- Date
- Oct. 19, 2017, 6: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 |