From 8d2beade907e375627f47bd097d05fdfd6c5a356 Mon Sep 17 00:00:00 2001 From: Marco De Paoli Date: Sat, 4 Nov 2023 07:24:21 +0100 Subject: [PATCH 01/14] test cases --- .gitignore | 1 + Makefile | 4 ++++ tests.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 tests.py diff --git a/.gitignore b/.gitignore index 6184577..f56d3bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +__pycache__ .vscode smallchat.dSYM smallchat diff --git a/Makefile b/Makefile index a7a5c2a..79e8365 100644 --- a/Makefile +++ b/Makefile @@ -3,5 +3,9 @@ all: smallchat smallchat: smallchat.c $(CC) smallchat.c -o smallchat -O2 -Wall -W -std=c99 +test: smallchat + @echo please, make sure smallchat is running + python3 -m unittest tests.py + clean: rm -f smallchat diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..97ad954 --- /dev/null +++ b/tests.py @@ -0,0 +1,63 @@ +from queue import Queue, Empty +from subprocess import PIPE, Popen +from threading import Thread +from time import sleep +from unittest import TestCase + + +class Process: + def __init__(self, cmd): + self.p = Popen(cmd, stdin=PIPE, stdout=PIPE) + self.q_in = Queue() + self.t_in = Thread(target=self._populate) + self.t_in.daemon = True + self.t_in.start() + self.q_out = Queue() + self.t_out = Thread(target=self._consume) + self.t_out.daemon = True + self.t_out.start() + + def _consume(self): + for line in iter(self.p.stdout.readline, b''): + self.q_out.put(line) + self.p.stdout.close() + + def _populate(self): + while True: + line = self.q_in.get() + self.p.stdin.write(line) + self.p.stdin.flush() + + def close(self): + self.p.kill() + self.p.wait() + + def read(self): + return self.q_out.get() + + def write(self, line): + self.q_in.put(line) + + +class TestIntegration(TestCase): + CMD = ["nc", "localhost", "7711"] + WELCOME = b"Welcome to Simple Chat! Use /nick to set your nick.\n" + + def setUp(self): + self.c_first = Process(self.CMD) + self.c_second = Process(self.CMD) + + def tearDown(self): + self.c_first.close() + self.c_second.close() + + def test(self): + l = self.c_first.read() + self.assertEqual(l, self.WELCOME) + l = self.c_second.read() + self.assertEqual(l, self.WELCOME) + self.c_first.write(b"/nick test-me\n") + sleep(.1) # AH!! I thiunk this sleep has something to do with Antirez next lesson ;-) + self.c_first.write(b"Hi!\n") + l_second = self.c_second.read() + self.assertEqual(l_second, b"test-me> Hi!\n") From ea77d88becf758e1f5cdb561b8d16a6c45ce7ff9 Mon Sep 17 00:00:00 2001 From: Marco De Paoli Date: Sat, 4 Nov 2023 23:01:33 +0100 Subject: [PATCH 02/14] python minimal (and wrong) implementation --- Makefile | 1 - smallchat.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++ tests.py | 23 +++++++++++++++++----- 3 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 smallchat.py diff --git a/Makefile b/Makefile index 79e8365..ad907a0 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,6 @@ smallchat: smallchat.c $(CC) smallchat.c -o smallchat -O2 -Wall -W -std=c99 test: smallchat - @echo please, make sure smallchat is running python3 -m unittest tests.py clean: diff --git a/smallchat.py b/smallchat.py new file mode 100644 index 0000000..5218821 --- /dev/null +++ b/smallchat.py @@ -0,0 +1,55 @@ +import select +import socket +import threading + +ADDRESS = ("localhost", 7711) +WELCOME = b"Welcome to Simple Chat! Use /nick to set your nick.\n" +PREFIX = b"/nick " + + + +CONNS = [] + + +def serve(conn): + with conn: + print(f"Connected by {conn}") + conn.sendall(WELCOME) + while True: + data = conn.recv(1024) + if not data: + break + if data.startswith(PREFIX): + nick = data[len(PREFIX):-1] + else: + response = nick + b"> " + data + print("response", response) + for c in CONNS: + if c != conn: + c.sendall(response) + + +def main(): + clients = [] + with socket.socket() as sl: + sl.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sl.bind(ADDRESS) + sl.listen() + inputs = [sl] + outputs = [] + while True: + print(f"accepting...") + inputready, outputready, exceptready = select.select(inputs, outputs, []) + for s in inputready: + if s == sl: + conn, addr = sl.accept() + print(f"accepted {addr}") + CONNS.append(conn) + th = threading.Thread(target=serve, args=(conn, )) + th.start() + clients.append(th) + + +if __name__ == '__main__': + main() + diff --git a/tests.py b/tests.py index 97ad954..14c7aa9 100644 --- a/tests.py +++ b/tests.py @@ -1,5 +1,6 @@ from queue import Queue, Empty from subprocess import PIPE, Popen +from sys import executable from threading import Thread from time import sleep from unittest import TestCase @@ -39,17 +40,21 @@ def write(self, line): self.q_in.put(line) -class TestIntegration(TestCase): - CMD = ["nc", "localhost", "7711"] +class TestIntegration: + CLIENT = ["nc", "localhost", "7711"] WELCOME = b"Welcome to Simple Chat! Use /nick to set your nick.\n" def setUp(self): - self.c_first = Process(self.CMD) - self.c_second = Process(self.CMD) + self.server = Popen(self.SERVER) + sleep(.1) + self.c_first = Process(self.CLIENT) + self.c_second = Process(self.CLIENT) def tearDown(self): self.c_first.close() self.c_second.close() + self.server.kill() + self.server.wait() def test(self): l = self.c_first.read() @@ -57,7 +62,15 @@ def test(self): l = self.c_second.read() self.assertEqual(l, self.WELCOME) self.c_first.write(b"/nick test-me\n") - sleep(.1) # AH!! I thiunk this sleep has something to do with Antirez next lesson ;-) + sleep(.1) # AH!! I think this sleep has something to do with Antirez next lesson ;-) self.c_first.write(b"Hi!\n") l_second = self.c_second.read() self.assertEqual(l_second, b"test-me> Hi!\n") + + +class TestIntegrationPy(TestIntegration, TestCase): + SERVER = [executable, "smallchat.py"] + + +class TestIntegrationC(TestIntegration, TestCase): + SERVER = ["./smallchat"] From 8f325b80a655eeb620595282bcd1e91db00b83d7 Mon Sep 17 00:00:00 2001 From: Marco De Paoli Date: Sun, 5 Nov 2023 00:04:34 +0100 Subject: [PATCH 03/14] Python: added arbitrary long message --- Makefile | 2 +- smallchat.py | 27 +++++++++++----------- tests.py | 63 ++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 66 insertions(+), 26 deletions(-) diff --git a/Makefile b/Makefile index ad907a0..eec42e9 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ smallchat: smallchat.c $(CC) smallchat.c -o smallchat -O2 -Wall -W -std=c99 test: smallchat - python3 -m unittest tests.py + python3 -m unittest tests.py -v clean: rm -f smallchat diff --git a/smallchat.py b/smallchat.py index 5218821..ab49027 100644 --- a/smallchat.py +++ b/smallchat.py @@ -5,28 +5,32 @@ ADDRESS = ("localhost", 7711) WELCOME = b"Welcome to Simple Chat! Use /nick to set your nick.\n" PREFIX = b"/nick " - - - -CONNS = [] +CONNS = {} def serve(conn): with conn: - print(f"Connected by {conn}") + fd = conn.fileno() + # print(f"Connected by {conn} fd: {fd}") conn.sendall(WELCOME) + CONNS[fd] = conn + msg = b"" while True: data = conn.recv(1024) if not data: + CONNS.pop(fd) break - if data.startswith(PREFIX): - nick = data[len(PREFIX):-1] + msg += data + if data[-1:] != b"\n": + continue + if msg.startswith(PREFIX): + nick = msg[len(PREFIX):-1] else: - response = nick + b"> " + data - print("response", response) - for c in CONNS: + response = nick + b"> " + msg + for c in CONNS.values(): if c != conn: c.sendall(response) + msg = b"" def main(): @@ -38,13 +42,10 @@ def main(): inputs = [sl] outputs = [] while True: - print(f"accepting...") inputready, outputready, exceptready = select.select(inputs, outputs, []) for s in inputready: if s == sl: conn, addr = sl.accept() - print(f"accepted {addr}") - CONNS.append(conn) th = threading.Thread(target=serve, args=(conn, )) th.start() clients.append(th) diff --git a/tests.py b/tests.py index 14c7aa9..34200c3 100644 --- a/tests.py +++ b/tests.py @@ -3,7 +3,7 @@ from sys import executable from threading import Thread from time import sleep -from unittest import TestCase +from unittest import TestCase, skip class Process: @@ -47,30 +47,69 @@ class TestIntegration: def setUp(self): self.server = Popen(self.SERVER) sleep(.1) - self.c_first = Process(self.CLIENT) - self.c_second = Process(self.CLIENT) def tearDown(self): - self.c_first.close() - self.c_second.close() self.server.kill() self.server.wait() - def test(self): - l = self.c_first.read() + def test_minimal(self): + c_first = Process(self.CLIENT) + c_second = Process(self.CLIENT) + l = c_first.read() self.assertEqual(l, self.WELCOME) - l = self.c_second.read() + l = c_second.read() self.assertEqual(l, self.WELCOME) - self.c_first.write(b"/nick test-me\n") - sleep(.1) # AH!! I think this sleep has something to do with Antirez next lesson ;-) - self.c_first.write(b"Hi!\n") - l_second = self.c_second.read() + c_first.write(b"/nick test-me\n") + self.wait() + c_first.write(b"Hi!\n") + l_second = c_second.read() self.assertEqual(l_second, b"test-me> Hi!\n") + c_first.close() + c_second.close() + + def test_disconnected(self): + c_first = Process(self.CLIENT) + c_second = Process(self.CLIENT) + c_third = Process(self.CLIENT) + self.assertEqual(c_first.read(), self.WELCOME) + self.assertEqual(c_second.read(), self.WELCOME) + self.assertEqual(c_third.read(), self.WELCOME) + c_third.close() + c_first.write(b"/nick test-me\n") + self.wait() + c_first.write(b"Hi!\n") + l_second = c_second.read() + self.assertEqual(l_second, b"test-me> Hi!\n") + c_first.close() + c_second.close() + + def test_very_log_message(self): + c_first = Process(self.CLIENT) + c_second = Process(self.CLIENT) + l = c_first.read() + self.assertEqual(l, self.WELCOME) + l = c_second.read() + self.assertEqual(l, self.WELCOME) + c_first.write(b"/nick test-me\n") + self.wait() + msg = b"Hi, it's " + b"me" * 1000 + b".\n" + c_first.write(msg) + l_second = c_second.read() + self.assertEqual(l_second, b"test-me> " + msg) + c_first.close() + c_second.close() + class TestIntegrationPy(TestIntegration, TestCase): SERVER = [executable, "smallchat.py"] + def wait(self): + sleep(.1) # TODO: remove + class TestIntegrationC(TestIntegration, TestCase): SERVER = ["./smallchat"] + + def wait(self): + sleep(.1) From d5d6b5cc05fa040b54e5a4abad4f4c27cb290253 Mon Sep 17 00:00:00 2001 From: Marco De Paoli Date: Mon, 6 Nov 2023 09:42:44 +0100 Subject: [PATCH 04/14] Python: parse till newline --- smallchat.py | 91 +++++++++++++++++++++++++++++++++++----------------- tests.py | 50 +++++++++++++++++++++++------ 2 files changed, 101 insertions(+), 40 deletions(-) diff --git a/smallchat.py b/smallchat.py index ab49027..8bafb59 100644 --- a/smallchat.py +++ b/smallchat.py @@ -1,43 +1,73 @@ import select import socket +import sys import threading -ADDRESS = ("localhost", 7711) +# TODO: +# ehm... so many race conditions at the moment + + WELCOME = b"Welcome to Simple Chat! Use /nick to set your nick.\n" PREFIX = b"/nick " -CONNS = {} -def serve(conn): - with conn: +class Pool: + def __init__(self): + self.conns = {} + + def add(self, conn): fd = conn.fileno() - # print(f"Connected by {conn} fd: {fd}") - conn.sendall(WELCOME) - CONNS[fd] = conn - msg = b"" - while True: - data = conn.recv(1024) - if not data: - CONNS.pop(fd) - break - msg += data - if data[-1:] != b"\n": - continue - if msg.startswith(PREFIX): - nick = msg[len(PREFIX):-1] - else: - response = nick + b"> " + msg - for c in CONNS.values(): - if c != conn: - c.sendall(response) - msg = b"" - - -def main(): + self.conns[fd] = conn + + def delete(self, conn): + fd = conn.fileno() + self.conns.pop(fd) + + def publish(self, sender, msg): + response = sender.nick + b"> " + msg + for conn in self.conns.values(): + if conn != sender: + conn.sendall(response) + + +class Client: + def __init__(self, pool, conn): + self.pool = pool + self.conn = conn + self.nick = "" + + def _received(self, msg): + if msg.startswith(PREFIX): + self.nick = msg[len(PREFIX):-1] + else: + self.pool.publish(self, msg) + + def serve(self): + with self.conn: + self.conn.sendall(WELCOME) + self.pool.add(self.conn) + fd = self.conn.fileno() + self.pool.conns[fd] = self.conn + msg = bytearray() + while True: + data = self.conn.recv(1024) + if not data: + self.pool.delete(self.conn) + break + for car in data: + msg.append(car) + if car == ord("\n"): + self._received(msg) + msg.clear() + + +def main(host, port): + address = (host, int(port)) + pool = Pool() clients = [] with socket.socket() as sl: sl.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sl.bind(ADDRESS) + sl.bind(address) sl.listen() inputs = [sl] outputs = [] @@ -46,11 +76,12 @@ def main(): for s in inputready: if s == sl: conn, addr = sl.accept() - th = threading.Thread(target=serve, args=(conn, )) + client = Client(pool, conn) + th = threading.Thread(target=client.serve) th.start() clients.append(th) if __name__ == '__main__': - main() + main(*sys.argv[1:]) diff --git a/tests.py b/tests.py index 34200c3..f11c530 100644 --- a/tests.py +++ b/tests.py @@ -5,6 +5,11 @@ from time import sleep from unittest import TestCase, skip +from smallchat import WELCOME + +HOST = "localhost" +PORT = "7711" + class Process: def __init__(self, cmd): @@ -41,8 +46,7 @@ def write(self, line): class TestIntegration: - CLIENT = ["nc", "localhost", "7711"] - WELCOME = b"Welcome to Simple Chat! Use /nick to set your nick.\n" + CLIENT = ["nc", HOST, PORT] def setUp(self): self.server = Popen(self.SERVER) @@ -56,9 +60,9 @@ def test_minimal(self): c_first = Process(self.CLIENT) c_second = Process(self.CLIENT) l = c_first.read() - self.assertEqual(l, self.WELCOME) + self.assertEqual(l, WELCOME) l = c_second.read() - self.assertEqual(l, self.WELCOME) + self.assertEqual(l, WELCOME) c_first.write(b"/nick test-me\n") self.wait() c_first.write(b"Hi!\n") @@ -71,9 +75,9 @@ def test_disconnected(self): c_first = Process(self.CLIENT) c_second = Process(self.CLIENT) c_third = Process(self.CLIENT) - self.assertEqual(c_first.read(), self.WELCOME) - self.assertEqual(c_second.read(), self.WELCOME) - self.assertEqual(c_third.read(), self.WELCOME) + self.assertEqual(c_first.read(), WELCOME) + self.assertEqual(c_second.read(), WELCOME) + self.assertEqual(c_third.read(), WELCOME) c_third.close() c_first.write(b"/nick test-me\n") self.wait() @@ -87,9 +91,9 @@ def test_very_log_message(self): c_first = Process(self.CLIENT) c_second = Process(self.CLIENT) l = c_first.read() - self.assertEqual(l, self.WELCOME) + self.assertEqual(l, WELCOME) l = c_second.read() - self.assertEqual(l, self.WELCOME) + self.assertEqual(l, WELCOME) c_first.write(b"/nick test-me\n") self.wait() msg = b"Hi, it's " + b"me" * 1000 + b".\n" @@ -99,10 +103,28 @@ def test_very_log_message(self): c_first.close() c_second.close() + def test_many_consecutive_messages(self): + c_first = Process(self.CLIENT) + c_second = Process(self.CLIENT) + self.assertEqual(c_first.read(), WELCOME) + self.assertEqual(c_second.read(), WELCOME) + c_first.write(b"/nick test-me\n") + self.wait() + msg = b"Hi, it's " + b"me" * 1000 + b".\n" + COUNT = 100 + for idx in range(COUNT): + c_first.write(msg) + for idx in range(COUNT): + l_second = c_second.read() + self.assertEqual(l_second, b"test-me> " + msg) + c_first.close() + c_second.close() + + class TestIntegrationPy(TestIntegration, TestCase): - SERVER = [executable, "smallchat.py"] + SERVER = [executable, "smallchat.py", HOST, PORT] def wait(self): sleep(.1) # TODO: remove @@ -113,3 +135,11 @@ class TestIntegrationC(TestIntegration, TestCase): def wait(self): sleep(.1) + + @skip("TODO") + def test_very_log_message(self): + super().test_very_log_message() + + @skip("TODO") + def test_many_consecutive_messages(self): + super().test_many_consecutive_messages() From c62592a2635828b3a957db184f68015469986494 Mon Sep 17 00:00:00 2001 From: Marco De Paoli Date: Mon, 6 Nov 2023 21:32:49 +0100 Subject: [PATCH 05/14] little refactoring, and race test-case skeleton --- smallchat.py | 29 ++++++++++++++--------------- tests.py | 15 +++++++++++++-- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/smallchat.py b/smallchat.py index 8bafb59..f1d5d5d 100644 --- a/smallchat.py +++ b/smallchat.py @@ -15,16 +15,16 @@ class Pool: def __init__(self): self.conns = {} - def add(self, conn): - fd = conn.fileno() - self.conns[fd] = conn + def add(self, client): + print(f"Connected client fd={client.fd}, nick={client.nick}") + self.conns[client.fd] = client.conn - def delete(self, conn): - fd = conn.fileno() - self.conns.pop(fd) + def delete(self, client): + print(f"Disconnected client fd={client.fd}, nick={client.nick}") + self.conns.pop(client.fd) def publish(self, sender, msg): - response = sender.nick + b"> " + msg + response = sender.nick.encode() + b"> " + msg for conn in self.conns.values(): if conn != sender: conn.sendall(response) @@ -34,37 +34,36 @@ class Client: def __init__(self, pool, conn): self.pool = pool self.conn = conn - self.nick = "" + self.fd = conn.fileno() + self.nick = f"user:{self.fd}" def _received(self, msg): if msg.startswith(PREFIX): - self.nick = msg[len(PREFIX):-1] + self.nick = msg[len(PREFIX):-1].decode() else: self.pool.publish(self, msg) def serve(self): with self.conn: + self.pool.add(self) self.conn.sendall(WELCOME) - self.pool.add(self.conn) - fd = self.conn.fileno() - self.pool.conns[fd] = self.conn msg = bytearray() while True: data = self.conn.recv(1024) if not data: - self.pool.delete(self.conn) break for car in data: msg.append(car) if car == ord("\n"): self._received(msg) msg.clear() + self.pool.delete(self) def main(host, port): address = (host, int(port)) pool = Pool() - clients = [] + threads = [] with socket.socket() as sl: sl.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sl.bind(address) @@ -79,7 +78,7 @@ def main(host, port): client = Client(pool, conn) th = threading.Thread(target=client.serve) th.start() - clients.append(th) + threads.append(th) if __name__ == '__main__': diff --git a/tests.py b/tests.py index f11c530..0e41170 100644 --- a/tests.py +++ b/tests.py @@ -120,14 +120,25 @@ def test_many_consecutive_messages(self): c_first.close() c_second.close() - + def test_many_clients(self): + c_first = Process(self.CLIENT) + self.assertEqual(c_first.read(), WELCOME) + c_others = [Process(self.CLIENT) for idx in range(100)] + for c_other in c_others: + self.assertEqual(c_other.read(), WELCOME) + c_first.write(b"/nick test-me\n") + c_first.close() + for c_other in c_others: + c_other.close() + self.assertTrue(False) + class TestIntegrationPy(TestIntegration, TestCase): SERVER = [executable, "smallchat.py", HOST, PORT] def wait(self): - sleep(.1) # TODO: remove + pass class TestIntegrationC(TestIntegration, TestCase): From c47da44549ca4c488121e09edd4a9a60501ea496 Mon Sep 17 00:00:00 2001 From: Marco De Paoli Date: Mon, 6 Nov 2023 22:05:51 +0100 Subject: [PATCH 06/14] fix: message was sent to myself --- smallchat.py | 12 ++++++------ tests.py | 28 +++++++++++++++++++--------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/smallchat.py b/smallchat.py index f1d5d5d..ffa7b26 100644 --- a/smallchat.py +++ b/smallchat.py @@ -13,21 +13,21 @@ class Pool: def __init__(self): - self.conns = {} + self.clients = {} def add(self, client): print(f"Connected client fd={client.fd}, nick={client.nick}") - self.conns[client.fd] = client.conn + self.clients[client.fd] = client def delete(self, client): print(f"Disconnected client fd={client.fd}, nick={client.nick}") - self.conns.pop(client.fd) + self.clients.pop(client.fd) def publish(self, sender, msg): response = sender.nick.encode() + b"> " + msg - for conn in self.conns.values(): - if conn != sender: - conn.sendall(response) + for client in self.clients.values(): + if client != sender: + client.conn.sendall(response) class Client: diff --git a/tests.py b/tests.py index 0e41170..f661696 100644 --- a/tests.py +++ b/tests.py @@ -121,16 +121,22 @@ def test_many_consecutive_messages(self): c_second.close() def test_many_clients(self): - c_first = Process(self.CLIENT) - self.assertEqual(c_first.read(), WELCOME) - c_others = [Process(self.CLIENT) for idx in range(100)] - for c_other in c_others: - self.assertEqual(c_other.read(), WELCOME) - c_first.write(b"/nick test-me\n") - c_first.close() + clients = [Process(self.CLIENT) for idx in range(5)] + for client in clients: + self.assertEqual(client.read(), WELCOME) + c_first, c_second, *c_others = clients + c_first.write(b"/nick test-me-1\n") + c_first.write(b"Hi, I'm the first!\n") + c_second.write(b"/nick test-me-2\n") + c_second.write(b"Hi, it's me, I'm the second!\n") + self.assertEqual(c_first.read(), b"test-me-2> Hi, it's me, I'm the second!\n") + self.assertEqual(c_second.read(), b"test-me-1> Hi, I'm the first!\n") for c_other in c_others: - c_other.close() - self.assertTrue(False) + msgs = set([c_other.read(), c_other.read()]) + self.assertIn(b"test-me-1> Hi, I'm the first!\n", msgs) + self.assertIn(b"test-me-2> Hi, it's me, I'm the second!\n", msgs) + for client in clients: + client.close() @@ -154,3 +160,7 @@ def test_very_log_message(self): @skip("TODO") def test_many_consecutive_messages(self): super().test_many_consecutive_messages() + + @skip("TODO") + def test_many_clients(self): + super().test_many_clients() From f2ff6ed3ff1ceec088393ab79a1d05ad926bbacd Mon Sep 17 00:00:00 2001 From: Marco De Paoli Date: Wed, 8 Nov 2023 09:59:55 +0100 Subject: [PATCH 07/14] removed threading --- smallchat.py | 88 ++++++++++++++++++++++++++++++++-------------------- tests.py | 14 +++++---- 2 files changed, 63 insertions(+), 39 deletions(-) diff --git a/smallchat.py b/smallchat.py index ffa7b26..4c6fdda 100644 --- a/smallchat.py +++ b/smallchat.py @@ -1,84 +1,106 @@ +""" +TODO: + avoid sendall (it could block) +""" + import select import socket import sys -import threading - -# TODO: -# ehm... so many race conditions at the moment - WELCOME = b"Welcome to Simple Chat! Use /nick to set your nick.\n" PREFIX = b"/nick " -class Pool: - def __init__(self): +class Clients: + def __init__(self, inputs, outputs): + self.inputs = inputs + self.outputs = outputs self.clients = {} def add(self, client): print(f"Connected client fd={client.fd}, nick={client.nick}") self.clients[client.fd] = client + self.inputs.append(client.conn) + self.outputs.append(client.conn) def delete(self, client): print(f"Disconnected client fd={client.fd}, nick={client.nick}") self.clients.pop(client.fd) + self.inputs.remove(client.conn) + self.outputs.remove(client.conn) + + def get(self, conn): + return self.clients[conn.fileno()] def publish(self, sender, msg): response = sender.nick.encode() + b"> " + msg for client in self.clients.values(): if client != sender: - client.conn.sendall(response) + client.send(response) + + +class Protocol: + def __init__(self, notify): + self.notify = notify + self.buff = bytearray() + + def put(self, data): + for car in data: + self.buff.append(car) + if car == ord("\n"): + self.notify(self.buff) + self.buff.clear() class Client: - def __init__(self, pool, conn): - self.pool = pool + def __init__(self, conn, publish, notify_close): self.conn = conn + self.publish = publish + self.notify_close = notify_close self.fd = conn.fileno() self.nick = f"user:{self.fd}" + self.protocol = Protocol(self._received) + self.send(WELCOME) def _received(self, msg): if msg.startswith(PREFIX): self.nick = msg[len(PREFIX):-1].decode() else: - self.pool.publish(self, msg) - - def serve(self): - with self.conn: - self.pool.add(self) - self.conn.sendall(WELCOME) - msg = bytearray() - while True: - data = self.conn.recv(1024) - if not data: - break - for car in data: - msg.append(car) - if car == ord("\n"): - self._received(msg) - msg.clear() - self.pool.delete(self) + self.publish(self, msg) + + def receive(self): + data = self.conn.recv(1024) + if not data: + self.notify_close(self) + self.conn.close() + return False + self.protocol.put(data) + return True + + def send(self, response): + self.conn.sendall(response) def main(host, port): address = (host, int(port)) - pool = Pool() - threads = [] with socket.socket() as sl: sl.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sl.bind(address) sl.listen() inputs = [sl] outputs = [] + clients = Clients(inputs, outputs) while True: inputready, outputready, exceptready = select.select(inputs, outputs, []) for s in inputready: if s == sl: conn, addr = sl.accept() - client = Client(pool, conn) - th = threading.Thread(target=client.serve) - th.start() - threads.append(th) + client = Client(conn, clients.publish, clients.delete) + clients.add(client) + else: + client = clients.get(s) + assert client.conn == s + client.receive() if __name__ == '__main__': diff --git a/tests.py b/tests.py index f661696..c527c34 100644 --- a/tests.py +++ b/tests.py @@ -46,7 +46,10 @@ def write(self, line): class TestIntegration: + BIG_MESSAGE_BODY = b"Hi, it's " + b"me" * 10000 + b".\n" CLIENT = ["nc", HOST, PORT] + CONSECUTIVE_MESSAGES_COUNT = 1000 + CONTEMPORARY_CLIENTS_COUNT = 500 def setUp(self): self.server = Popen(self.SERVER) @@ -96,7 +99,7 @@ def test_very_log_message(self): self.assertEqual(l, WELCOME) c_first.write(b"/nick test-me\n") self.wait() - msg = b"Hi, it's " + b"me" * 1000 + b".\n" + msg = self.BIG_MESSAGE_BODY c_first.write(msg) l_second = c_second.read() self.assertEqual(l_second, b"test-me> " + msg) @@ -110,18 +113,17 @@ def test_many_consecutive_messages(self): self.assertEqual(c_second.read(), WELCOME) c_first.write(b"/nick test-me\n") self.wait() - msg = b"Hi, it's " + b"me" * 1000 + b".\n" - COUNT = 100 - for idx in range(COUNT): + msg = self.BIG_MESSAGE_BODY + for idx in range(self.CONSECUTIVE_MESSAGES_COUNT): c_first.write(msg) - for idx in range(COUNT): + for idx in range(self.CONSECUTIVE_MESSAGES_COUNT): l_second = c_second.read() self.assertEqual(l_second, b"test-me> " + msg) c_first.close() c_second.close() def test_many_clients(self): - clients = [Process(self.CLIENT) for idx in range(5)] + clients = [Process(self.CLIENT) for idx in range(self.CONTEMPORARY_CLIENTS_COUNT)] for client in clients: self.assertEqual(client.read(), WELCOME) c_first, c_second, *c_others = clients From 312d5ecfbe4961436ccfcff178d3e25a37d02626 Mon Sep 17 00:00:00 2001 From: Marco De Paoli Date: Wed, 8 Nov 2023 10:32:17 +0100 Subject: [PATCH 08/14] removed sendall --- smallchat.py | 30 ++++++++++++++++++------------ tests.py | 6 +++--- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/smallchat.py b/smallchat.py index 4c6fdda..eaddb9d 100644 --- a/smallchat.py +++ b/smallchat.py @@ -1,8 +1,3 @@ -""" -TODO: - avoid sendall (it could block) -""" - import select import socket import sys @@ -60,7 +55,7 @@ def __init__(self, conn, publish, notify_close): self.fd = conn.fileno() self.nick = f"user:{self.fd}" self.protocol = Protocol(self._received) - self.send(WELCOME) + self.out_buffer = bytearray() def _received(self, msg): if msg.startswith(PREFIX): @@ -68,17 +63,21 @@ def _received(self, msg): else: self.publish(self, msg) - def receive(self): + def raw_receive(self): data = self.conn.recv(1024) if not data: self.notify_close(self) self.conn.close() - return False - self.protocol.put(data) - return True + else: + self.protocol.put(data) + + def raw_send(self): + if self.out_buffer: + sent = self.conn.send(self.out_buffer) + self.out_buffer = self.out_buffer[sent:] def send(self, response): - self.conn.sendall(response) + self.out_buffer += response def main(host, port): @@ -97,10 +96,17 @@ def main(host, port): conn, addr = sl.accept() client = Client(conn, clients.publish, clients.delete) clients.add(client) + client.send(WELCOME) else: client = clients.get(s) assert client.conn == s - client.receive() + client.raw_receive() + for s in outputready: + if s.fileno() > 0: + client = clients.get(s) + assert client.conn == s + client.raw_send() + if __name__ == '__main__': diff --git a/tests.py b/tests.py index c527c34..764531f 100644 --- a/tests.py +++ b/tests.py @@ -46,10 +46,10 @@ def write(self, line): class TestIntegration: - BIG_MESSAGE_BODY = b"Hi, it's " + b"me" * 10000 + b".\n" + BIG_MESSAGE_BODY = b"Hi, it's " + b"me" * 1000 + b".\n" CLIENT = ["nc", HOST, PORT] - CONSECUTIVE_MESSAGES_COUNT = 1000 - CONTEMPORARY_CLIENTS_COUNT = 500 + CONSECUTIVE_MESSAGES_COUNT = 100 + CONTEMPORARY_CLIENTS_COUNT = 50 def setUp(self): self.server = Popen(self.SERVER) From 57fe6bf5af5665d7e7380d0a2ec7abdbdb38b179 Mon Sep 17 00:00:00 2001 From: Marco De Paoli Date: Wed, 8 Nov 2023 10:47:56 +0100 Subject: [PATCH 09/14] refactoring encode/decode message --- smallchat.py | 19 ++++++++++++------- tests.py | 3 +-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/smallchat.py b/smallchat.py index eaddb9d..de5bc9b 100644 --- a/smallchat.py +++ b/smallchat.py @@ -2,7 +2,7 @@ import socket import sys -WELCOME = b"Welcome to Simple Chat! Use /nick to set your nick.\n" +WELCOME = b"Welcome to Simple Chat! Use /nick to set your nick." PREFIX = b"/nick " @@ -39,12 +39,17 @@ def __init__(self, notify): self.notify = notify self.buff = bytearray() - def put(self, data): + @staticmethod + def encode(msg): + return msg + b"\n" + + def decode(self, data): for car in data: - self.buff.append(car) if car == ord("\n"): self.notify(self.buff) self.buff.clear() + else: + self.buff.append(car) class Client: @@ -59,7 +64,7 @@ def __init__(self, conn, publish, notify_close): def _received(self, msg): if msg.startswith(PREFIX): - self.nick = msg[len(PREFIX):-1].decode() + self.nick = msg[len(PREFIX):].decode() else: self.publish(self, msg) @@ -69,15 +74,15 @@ def raw_receive(self): self.notify_close(self) self.conn.close() else: - self.protocol.put(data) + self.protocol.decode(data) def raw_send(self): if self.out_buffer: sent = self.conn.send(self.out_buffer) self.out_buffer = self.out_buffer[sent:] - def send(self, response): - self.out_buffer += response + def send(self, msg): + self.out_buffer += Protocol.encode(msg) def main(host, port): diff --git a/tests.py b/tests.py index 764531f..e74532d 100644 --- a/tests.py +++ b/tests.py @@ -5,10 +5,9 @@ from time import sleep from unittest import TestCase, skip -from smallchat import WELCOME - HOST = "localhost" PORT = "7711" +WELCOME = b"Welcome to Simple Chat! Use /nick to set your nick.\n" class Process: From 08facc0441b9cddd94ce5153456fe1277c5dbe75 Mon Sep 17 00:00:00 2001 From: Marco De Paoli Date: Wed, 8 Nov 2023 11:03:08 +0100 Subject: [PATCH 10/14] refactoring OOP --- smallchat.py | 90 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/smallchat.py b/smallchat.py index de5bc9b..0feb129 100644 --- a/smallchat.py +++ b/smallchat.py @@ -6,20 +6,44 @@ PREFIX = b"/nick " -class Clients: +class Channel: + def __init__(self, conn, protocol_cls, notify_receive, notify_close): + self.conn = conn + self.notify_receive = notify_receive + self.notify_close = notify_close + self.fd = conn.fileno() + self.protocol = protocol_cls(self._received) + self.out_buffer = bytearray() + + def raw_receive(self): + data = self.conn.recv(1024) + if not data: + self.notify_close(self) + self.conn.close() + else: + self.protocol.decode(data) + + def raw_send(self): + if self.out_buffer: + sent = self.conn.send(self.out_buffer) + self.out_buffer = self.out_buffer[sent:] + + def send(self, msg): + self.out_buffer += self.protocol.encode(msg) + + +class Channels: def __init__(self, inputs, outputs): self.inputs = inputs self.outputs = outputs self.clients = {} def add(self, client): - print(f"Connected client fd={client.fd}, nick={client.nick}") self.clients[client.fd] = client self.inputs.append(client.conn) self.outputs.append(client.conn) def delete(self, client): - print(f"Disconnected client fd={client.fd}, nick={client.nick}") self.clients.pop(client.fd) self.inputs.remove(client.conn) self.outputs.remove(client.conn) @@ -27,6 +51,30 @@ def delete(self, client): def get(self, conn): return self.clients[conn.fileno()] + +class Client(Channel): + def __init__(self, conn, protocol_cls, publish, notify_close): + super().__init__(conn, protocol_cls, self._received, notify_close) + self.publish = publish + self.nick = f"user:{conn.fileno()}" + + def _received(self, msg): + if msg.startswith(PREFIX): + self.nick = msg[len(PREFIX):].decode() + else: + self.publish(self, msg) + + +class Clients(Channels): + def add(self, client): + super().add(client) + print(f"Connected client fd={client.fd}, nick={client.nick}") + client.send(WELCOME) + + def delete(self, client): + super().delete(client) + print(f"Disconnected client fd={client.fd}, nick={client.nick}") + def publish(self, sender, msg): response = sender.nick.encode() + b"> " + msg for client in self.clients.values(): @@ -52,39 +100,6 @@ def decode(self, data): self.buff.append(car) -class Client: - def __init__(self, conn, publish, notify_close): - self.conn = conn - self.publish = publish - self.notify_close = notify_close - self.fd = conn.fileno() - self.nick = f"user:{self.fd}" - self.protocol = Protocol(self._received) - self.out_buffer = bytearray() - - def _received(self, msg): - if msg.startswith(PREFIX): - self.nick = msg[len(PREFIX):].decode() - else: - self.publish(self, msg) - - def raw_receive(self): - data = self.conn.recv(1024) - if not data: - self.notify_close(self) - self.conn.close() - else: - self.protocol.decode(data) - - def raw_send(self): - if self.out_buffer: - sent = self.conn.send(self.out_buffer) - self.out_buffer = self.out_buffer[sent:] - - def send(self, msg): - self.out_buffer += Protocol.encode(msg) - - def main(host, port): address = (host, int(port)) with socket.socket() as sl: @@ -99,9 +114,8 @@ def main(host, port): for s in inputready: if s == sl: conn, addr = sl.accept() - client = Client(conn, clients.publish, clients.delete) + client = Client(conn, Protocol, clients.publish, clients.delete) clients.add(client) - client.send(WELCOME) else: client = clients.get(s) assert client.conn == s From 3a2478c2e01bdba89cc549371469e945ebd16fb0 Mon Sep 17 00:00:00 2001 From: Marco De Paoli Date: Wed, 8 Nov 2023 11:21:48 +0100 Subject: [PATCH 11/14] refactoring names --- smallchat.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/smallchat.py b/smallchat.py index 0feb129..c2d8412 100644 --- a/smallchat.py +++ b/smallchat.py @@ -6,7 +6,7 @@ PREFIX = b"/nick " -class Channel: +class Client: def __init__(self, conn, protocol_cls, notify_receive, notify_close): self.conn = conn self.notify_receive = notify_receive @@ -32,7 +32,7 @@ def send(self, msg): self.out_buffer += self.protocol.encode(msg) -class Channels: +class Clients: def __init__(self, inputs, outputs): self.inputs = inputs self.outputs = outputs @@ -52,7 +52,7 @@ def get(self, conn): return self.clients[conn.fileno()] -class Client(Channel): +class ChatClient(Client): def __init__(self, conn, protocol_cls, publish, notify_close): super().__init__(conn, protocol_cls, self._received, notify_close) self.publish = publish @@ -65,7 +65,7 @@ def _received(self, msg): self.publish(self, msg) -class Clients(Channels): +class ChatClients(Clients): def add(self, client): super().add(client) print(f"Connected client fd={client.fd}, nick={client.nick}") @@ -108,13 +108,13 @@ def main(host, port): sl.listen() inputs = [sl] outputs = [] - clients = Clients(inputs, outputs) + clients = ChatClients(inputs, outputs) while True: inputready, outputready, exceptready = select.select(inputs, outputs, []) for s in inputready: if s == sl: conn, addr = sl.accept() - client = Client(conn, Protocol, clients.publish, clients.delete) + client = ChatClient(conn, Protocol, clients.publish, clients.delete) clients.add(client) else: client = clients.get(s) From dbc02eda9fa0041bd00491498e50a250d99f5e9c Mon Sep 17 00:00:00 2001 From: Marco De Paoli Date: Wed, 8 Nov 2023 21:28:21 +0100 Subject: [PATCH 12/14] newline is not admitted inside message --- smallchat.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/smallchat.py b/smallchat.py index c2d8412..5010047 100644 --- a/smallchat.py +++ b/smallchat.py @@ -83,17 +83,20 @@ def publish(self, sender, msg): class Protocol: + END = b"\n" + def __init__(self, notify): self.notify = notify self.buff = bytearray() - @staticmethod - def encode(msg): - return msg + b"\n" + @classmethod + def encode(cls, msg): + assert not cls.END in msg + return msg + cls.END def decode(self, data): for car in data: - if car == ord("\n"): + if car == ord(self.END): self.notify(self.buff) self.buff.clear() else: @@ -121,10 +124,12 @@ def main(host, port): assert client.conn == s client.raw_receive() for s in outputready: - if s.fileno() > 0: - client = clients.get(s) - assert client.conn == s - client.raw_send() + if s.fileno() <= 0: + # sockets already closed during reception/recv + continue + client = clients.get(s) + assert client.conn == s + client.raw_send() From cbcd48886f685ec5f0334b313cfbed13bdff36bf Mon Sep 17 00:00:00 2001 From: Marco De Paoli Date: Thu, 9 Nov 2023 17:36:31 +0100 Subject: [PATCH 13/14] merge upstream --- .gitignore | 3 ++- README.md | 4 ++++ tests.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index f56d3bf..737d020 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ __pycache__ .vscode smallchat.dSYM -smallchat +smallchat-client +smallchat-server diff --git a/README.md b/README.md index b1556d1..b06c172 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Smallchat +Inspired by Salvatore Sanfilippo's series on creating a chat +Implemented in python + + TLDR: This is just a programming example for a few friends of mine. It somehow turned into a set of programming videos, continuing one project I started some time ago: Writing System Software videos series. 1. [First episode](https://www.youtube.com/watch?v=eT02gzeLmF0), how the basic server works. diff --git a/tests.py b/tests.py index e74532d..3ba632e 100644 --- a/tests.py +++ b/tests.py @@ -149,7 +149,7 @@ def wait(self): class TestIntegrationC(TestIntegration, TestCase): - SERVER = ["./smallchat"] + SERVER = ["./smallchat-server"] def wait(self): sleep(.1) From b3e8bc5845eecdb92525da42ac62421a37abf510 Mon Sep 17 00:00:00 2001 From: Marco De Paoli Date: Wed, 13 Dec 2023 22:27:12 +0100 Subject: [PATCH 14/14] Initial python client. --- Makefile | 5 ++- process.py | 116 +++++++++++++++++++++++++++++++++++++++++++++++ smallchat.py | 73 +++++++++++++++++++++++++----- tests.py | 124 ++++++++++++++++++++++----------------------------- 4 files changed, 234 insertions(+), 84 deletions(-) create mode 100644 process.py diff --git a/Makefile b/Makefile index 66395f5..be6a711 100644 --- a/Makefile +++ b/Makefile @@ -7,8 +7,9 @@ smallchat-server: smallchat-server.c chatlib.c smallchat-client: smallchat-client.c chatlib.c $(CC) smallchat-client.c chatlib.c -o smallchat-client $(CFLAGS) -test: smallchat +test: smallchat-server smallchat-client + python3 -m unittest process.py -v python3 -m unittest tests.py -v clean: - rm -f smallchat-server + rm -f smallchat-server smallchat-client diff --git a/process.py b/process.py new file mode 100644 index 0000000..253418e --- /dev/null +++ b/process.py @@ -0,0 +1,116 @@ +import subprocess +import sys +import unittest + + +class Process: + def __init__(self, args): + self.proc = subprocess.Popen( + args, stdout=subprocess.PIPE, stdin=subprocess.PIPE, bufsize=0) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def close(self): + self.stop() + self.proc.stdin.close() + self.proc.stdout.close() + + def read(self): + return self.proc.stdout.readline().strip() + + def stop(self): + self.terminate() + self.wait() + + def terminate(self): + self.proc.terminate() + + def wait(self): + self.proc.wait() + + def write(self, msg): + # print(f"Process.write msg: {msg}", file=sys.stderr) + self.proc.stdin.write(msg + b"\n") + self.proc.stdin.flush() + + +class TestProcess(unittest.TestCase): + def setUp(self): + self.p = Process([sys.executable, __file__]) + + def tearDown(self): + self.p.close() + + def test_stdout(self): + line = self.p.read() + self.assertEqual(line, b"started") + self.p.stop() + line = self.p.read() + self.assertFalse(line) + + def test_stdin(self): + line = self.p.read() + self.assertEqual(line, b"started") + self.p.write(b"test-request") + line = self.p.read() + self.assertEqual(line, b"test-request") + self.p.stop() + line = self.p.read() + self.assertFalse(line) + + def test_cycle(self): + line = self.p.read() + self.assertEqual(line, b"started") + self.p.write(b"test-request-1") + line = self.p.read() + self.assertEqual(line, b"test-request-1") + self.p.write(b"test-request-2") + line = self.p.read() + self.assertEqual(line, b"test-request-2") + self.p.write(b"/exit") + self.p.wait() + line = self.p.read() + self.assertFalse(line) + + + def test_long(self): + line = self.p.read() + self.assertEqual(line, b"started") + request = b"0123456789" * 10000 + self.p.write(request) + line = self.p.read() + self.assertEqual(line, request) + self.p.stop() + line = self.p.read() + self.assertFalse(line) + + +class TestProcessContext(unittest.TestCase): + def test_stdout(self): + with Process([sys.executable, __file__]) as p: + line = p.read() + self.assertEqual(line, b"started") + p.stop() + line = p.read() + self.assertFalse(line) + + +def test_main(): + sys.stdout.write("started\n") + sys.stdout.flush() + while True: + # print("read", file=sys.stderr) + request = sys.stdin.readline().strip() + # print("request", request, file=sys.stderr) + if request == '/exit': + break + sys.stdout.write(request + "\n") + sys.stdout.flush() + + +if __name__ == '__main__': + test_main(*sys.argv[1:]) diff --git a/smallchat.py b/smallchat.py index 5010047..225f7f6 100644 --- a/smallchat.py +++ b/smallchat.py @@ -12,7 +12,7 @@ def __init__(self, conn, protocol_cls, notify_receive, notify_close): self.notify_receive = notify_receive self.notify_close = notify_close self.fd = conn.fileno() - self.protocol = protocol_cls(self._received) + self.protocol = protocol_cls(self.notify_receive) self.out_buffer = bytearray() def raw_receive(self): @@ -29,6 +29,7 @@ def raw_send(self): self.out_buffer = self.out_buffer[sent:] def send(self, msg): + # print(f"send msg: {msg}", file=sys.stderr) self.out_buffer += self.protocol.encode(msg) @@ -59,6 +60,7 @@ def __init__(self, conn, protocol_cls, publish, notify_close): self.nick = f"user:{conn.fileno()}" def _received(self, msg): + # print(f"received msg: {msg}", file=sys.stderr) if msg.startswith(PREFIX): self.nick = msg[len(PREFIX):].decode() else: @@ -68,12 +70,12 @@ def _received(self, msg): class ChatClients(Clients): def add(self, client): super().add(client) - print(f"Connected client fd={client.fd}, nick={client.nick}") + # print(f"Connected client fd={client.fd}, nick={client.nick}") client.send(WELCOME) def delete(self, client): super().delete(client) - print(f"Disconnected client fd={client.fd}, nick={client.nick}") + # print(f"Disconnected client fd={client.fd}, nick={client.nick}") def publish(self, sender, msg): response = sender.nick.encode() + b"> " + msg @@ -85,8 +87,9 @@ def publish(self, sender, msg): class Protocol: END = b"\n" - def __init__(self, notify): + def __init__(self, notify, end=None): self.notify = notify + self.end = end or self.END self.buff = bytearray() @classmethod @@ -103,12 +106,57 @@ def decode(self, data): self.buff.append(car) -def main(host, port): - address = (host, int(port)) +class Stream: + def __init__(self, stdin, stdout): + self.stdin = stdin + self.stdout = stdout + self.closed = False + self.send = None + + def close(self): + self.closed = True + + def raw_receive(self): + msg = self.stdin.readline().rstrip() + self.send(msg.encode()) + + def receive(self, msg): + self.stdout.write(msg.decode() + "\n") + self.stdout.flush() + + def raw_send(self): + pass # noop + + +def _main_client(address): + stream = Stream(sys.stdin, sys.stdout) + + with socket.socket() as conn: + conn.connect(address) + client = Client(conn, Protocol, notify_receive=stream.receive, notify_close=stream.close) + stream.protocol = Protocol(client.send, "\n") + stream.send = client.send + inputs = [conn, sys.stdin] + outputs = [conn, sys.stdout] + clients = {conn: client, sys.stdin: stream, sys.stdout: stream} + while not stream.closed: + inputready, outputready, exceptready = select.select(inputs, outputs, []) + for s in inputready: + clients.get(s).raw_receive() + for s in outputready: + if s.fileno() <= 0: + # sockets already closed during reception/recv + continue + clients.get(s).raw_send() + + +def _main_server(address): with socket.socket() as sl: sl.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sl.bind(address) sl.listen() + sys.stdout.write(f"Server started address={address}\n") + sys.stdout.flush() inputs = [sl] outputs = [] clients = ChatClients(inputs, outputs) @@ -120,18 +168,19 @@ def main(host, port): client = ChatClient(conn, Protocol, clients.publish, clients.delete) clients.add(client) else: - client = clients.get(s) - assert client.conn == s - client.raw_receive() + clients.get(s).raw_receive() for s in outputready: if s.fileno() <= 0: # sockets already closed during reception/recv continue - client = clients.get(s) - assert client.conn == s - client.raw_send() + clients.get(s).raw_send() +def main(role, host, port): + fun = globals()["_main_" + role] + address = (host, int(port)) + fun(address) + if __name__ == '__main__': main(*sys.argv[1:]) diff --git a/tests.py b/tests.py index 3ba632e..44a1428 100644 --- a/tests.py +++ b/tests.py @@ -1,63 +1,36 @@ -from queue import Queue, Empty -from subprocess import PIPE, Popen from sys import executable -from threading import Thread from time import sleep from unittest import TestCase, skip +from process import Process +from smallchat import WELCOME + HOST = "localhost" PORT = "7711" -WELCOME = b"Welcome to Simple Chat! Use /nick to set your nick.\n" - - -class Process: - def __init__(self, cmd): - self.p = Popen(cmd, stdin=PIPE, stdout=PIPE) - self.q_in = Queue() - self.t_in = Thread(target=self._populate) - self.t_in.daemon = True - self.t_in.start() - self.q_out = Queue() - self.t_out = Thread(target=self._consume) - self.t_out.daemon = True - self.t_out.start() - def _consume(self): - for line in iter(self.p.stdout.readline, b''): - self.q_out.put(line) - self.p.stdout.close() - def _populate(self): - while True: - line = self.q_in.get() - self.p.stdin.write(line) - self.p.stdin.flush() +class TestServer: + def _wait_client_receive(self): + pass - def close(self): - self.p.kill() - self.p.wait() + def _wait_start_server(self): + line = self.server.read() + assert line.startswith(b"Server started") - def read(self): - return self.q_out.get() + def setUp(self): + self.server = Process(self.SERVER) + self._wait_start_server() - def write(self, line): - self.q_in.put(line) + def tearDown(self): + self.server.close() -class TestIntegration: - BIG_MESSAGE_BODY = b"Hi, it's " + b"me" * 1000 + b".\n" +class TestIntegration(TestServer): + BIG_MESSAGE_BODY = b"Hi, it's " + b"me" * 10000 + b"." CLIENT = ["nc", HOST, PORT] CONSECUTIVE_MESSAGES_COUNT = 100 CONTEMPORARY_CLIENTS_COUNT = 50 - def setUp(self): - self.server = Popen(self.SERVER) - sleep(.1) - - def tearDown(self): - self.server.kill() - self.server.wait() - def test_minimal(self): c_first = Process(self.CLIENT) c_second = Process(self.CLIENT) @@ -65,11 +38,11 @@ def test_minimal(self): self.assertEqual(l, WELCOME) l = c_second.read() self.assertEqual(l, WELCOME) - c_first.write(b"/nick test-me\n") - self.wait() - c_first.write(b"Hi!\n") + c_first.write(b"/nick test-me") + self._wait_client_receive() + c_first.write(b"Hi!") l_second = c_second.read() - self.assertEqual(l_second, b"test-me> Hi!\n") + self.assertEqual(l_second, b"test-me> Hi!") c_first.close() c_second.close() @@ -81,23 +54,24 @@ def test_disconnected(self): self.assertEqual(c_second.read(), WELCOME) self.assertEqual(c_third.read(), WELCOME) c_third.close() - c_first.write(b"/nick test-me\n") - self.wait() - c_first.write(b"Hi!\n") + c_first.write(b"/nick test-me") + c_first.write(b"/nick test-me") + self._wait_client_receive() + c_first.write(b"Hi!") l_second = c_second.read() - self.assertEqual(l_second, b"test-me> Hi!\n") + self.assertEqual(l_second, b"test-me> Hi!") c_first.close() c_second.close() - def test_very_log_message(self): + def test_very_long_message(self): c_first = Process(self.CLIENT) c_second = Process(self.CLIENT) l = c_first.read() self.assertEqual(l, WELCOME) l = c_second.read() self.assertEqual(l, WELCOME) - c_first.write(b"/nick test-me\n") - self.wait() + c_first.write(b"/nick test-me") + self._wait_client_receive() msg = self.BIG_MESSAGE_BODY c_first.write(msg) l_second = c_second.read() @@ -110,8 +84,8 @@ def test_many_consecutive_messages(self): c_second = Process(self.CLIENT) self.assertEqual(c_first.read(), WELCOME) self.assertEqual(c_second.read(), WELCOME) - c_first.write(b"/nick test-me\n") - self.wait() + c_first.write(b"/nick test-me") + self._wait_client_receive() msg = self.BIG_MESSAGE_BODY for idx in range(self.CONSECUTIVE_MESSAGES_COUNT): c_first.write(msg) @@ -126,37 +100,47 @@ def test_many_clients(self): for client in clients: self.assertEqual(client.read(), WELCOME) c_first, c_second, *c_others = clients - c_first.write(b"/nick test-me-1\n") - c_first.write(b"Hi, I'm the first!\n") - c_second.write(b"/nick test-me-2\n") - c_second.write(b"Hi, it's me, I'm the second!\n") - self.assertEqual(c_first.read(), b"test-me-2> Hi, it's me, I'm the second!\n") - self.assertEqual(c_second.read(), b"test-me-1> Hi, I'm the first!\n") + c_first.write(b"/nick test-me-1") + c_first.write(b"Hi, I'm the first!") + c_second.write(b"/nick test-me-2") + c_second.write(b"Hi, it's me, I'm the second!") + self.assertEqual(c_first.read(), b"test-me-2> Hi, it's me, I'm the second!") + self.assertEqual(c_second.read(), b"test-me-1> Hi, I'm the first!") for c_other in c_others: msgs = set([c_other.read(), c_other.read()]) - self.assertIn(b"test-me-1> Hi, I'm the first!\n", msgs) - self.assertIn(b"test-me-2> Hi, it's me, I'm the second!\n", msgs) + self.assertIn(b"test-me-1> Hi, I'm the first!", msgs) + self.assertIn(b"test-me-2> Hi, it's me, I'm the second!", msgs) for client in clients: client.close() class TestIntegrationPy(TestIntegration, TestCase): - SERVER = [executable, "smallchat.py", HOST, PORT] + SERVER = [executable, "smallchat.py", "server", HOST, PORT] - def wait(self): - pass + +class TestIntegrationClientPy(TestIntegration, TestCase): + CLIENT = [executable, "smallchat.py", "client", HOST, PORT] + CONTEMPORARY_CLIENTS_COUNT = 2 + SERVER = [executable, "smallchat.py", "server", HOST, PORT] + + @skip("TODO") + def test_many_clients(self): + super().test_many_clients() class TestIntegrationC(TestIntegration, TestCase): SERVER = ["./smallchat-server"] - def wait(self): + def _wait_client_receive(self): sleep(.1) + def _wait_start_server(self): + sleep(.01) + @skip("TODO") - def test_very_log_message(self): - super().test_very_log_message() + def test_very_long_message(self): + super().test_very_long_message() @skip("TODO") def test_many_consecutive_messages(self):