import multiprocessing import os import random import re import signal import string from io import StringIO import subprocess import sys import time import xml.etree.ElementTree as ET from collections import Counter from typing import List, Union import numpy as np import pandas as pd import requests import shutil import zmq def init_tests(args): random.seed() distributor_exec = args["distributor"] worker_exec = args["worker"] no_cache = args["no_cache"] cache_dir = "test_files/" if args["cache_dir"] != "": cache_dir = args["cache_dir"] simple_delimiters = [", ", ". ", " "] all_delimiters = [x + " " for x in string.punctuation] filename_simple = "test_simple_text.txt" filename_complex = "test_complex_text.txt" base_port = 6001 base_port = check_ports_free(base_port) word_list_path = cache_dir + "word_list.txt" book_base_url = "https://www.gutenberg.org/cache/epub/" book_urls = ["84/pg84.txt", "145/pg145.txt", "1342/pg1342.txt", "1513/pg1513.txt", "2701/pg2701.txt"] # book_urls = ["84/pg84.txt", # "1342/pg1342.txt", # "16389/pg16389.txt", # "50150/pg50150.txt", # "67979/pg67979.txt", # "75201/pg75201.txt"] books_selected = [random.choice(book_urls)] book_urls.remove(books_selected[0]) books_selected.append(random.choice(book_urls)) if not os.path.exists(cache_dir): os.mkdir(cache_dir) if not os.path.isfile(word_list_path): download_word_list(word_list_path) with open(word_list_path, "r") as f: word_list = f.read().splitlines() book_list = [] for b in books_selected: book_path = cache_dir + b.split("/")[1] existing_book_size = 1 if os.path.isfile(book_path): existing_book_size = os.path.getsize(book_path) if not os.path.isfile(book_path) or existing_book_size == 0: book_url = book_base_url + b download_book(book_url, book_path) with open(book_path, "r") as f: book_list.append(f.read().encode("ascii", errors="ignore")) filename_book_1 = "book_1.txt" filename_book_2 = "book_2.txt" filename_interop = "interop_test.txt" filename_valgrind = "valgrind_test.txt" test_args = {"is_ubuntu20_eecs_system": is_ubuntu20_eecs(), "distributor": distributor_exec, "worker": worker_exec, "no_cache": no_cache, "cache_dir": cache_dir, "simple_delimiters": simple_delimiters, "all_delimiters": all_delimiters, "filename_simple": filename_simple, "filename_complex": filename_complex, "base_port": base_port, "word_list": word_list, "books": book_list, "filename_book_1": filename_book_1, "filename_book_2": filename_book_2, "filename_interop": filename_interop, "filename_valgrind": filename_valgrind, } generate_test_files(test_args) return test_args # check if current system is Ubuntu 20 EECS test system def is_ubuntu20_eecs(): return True # required_operating_system = "Operating System: Ubuntu 20.04.6 LTS" # required_domain_name = "eecsit.tu-berlin.de" # # hostnamectl = subprocess.Popen("hostnamectl", stdout=subprocess.PIPE) # os_name = subprocess.check_output(["grep", "-o", "Operating System:.*"], stdin=hostnamectl.stdout).decode("utf-8").strip() # hostnamectl.wait() # # domain_name = subprocess.check_output(["hostname", "-d"]).decode("utf-8").strip() # # return required_operating_system == os_name and required_domain_name == domain_name # checks if the base port and all following 8 ports are actually free for testing # if not, check the next 10 def check_ports_free(base_port): if not shutil.which("netstat") or sys.platform != "linux": print("netstat not found. " "To ensure all used ports during testing are free, please install net-tools with: sudo apt install net-tools", file=sys.stderr) return base_port proc = subprocess.Popen(["netstat -tuna | tail -n +2 | tr -s [:blank:] ','"], stdout=subprocess.PIPE, shell=True) netstat_output, netstat_err = proc.communicate() netstat_output = netstat_output.decode("ascii") df = pd.read_csv(StringIO(netstat_output)) df = df["Local"] df = df.apply(lambda x: x.split(":")[-1]) ports = np.arange(base_port, base_port + 8).tolist() port_list = [str(x) for x in ports] if sum((df == i).any() for i in port_list) > 0: base_port += 10 base_port = check_ports_free(base_port) return base_port def download_word_list(output_path): word_list_url = "https://raw.githubusercontent.com/dwyl/english-words/master/words_alpha.txt" word_list = None try: url_content = requests.get(word_list_url, timeout=5) word_list = url_content.text except requests.exceptions.Timeout: sys.exit("Failed to get word list. Can not run tests without it. Quitting...") if word_list is None: sys.exit("Failed to get word list. Can not run tests without it. Quitting...") with open(output_path, "w") as f: f.write(word_list) def download_book(url, output_path): # book_text = None # try: # url_content = requests.get(url, timeout=90, stream=True) # book_text = url_content.text # except requests.exceptions.Timeout: # sys.exit("Failed to get book text. Can not run tests without it. Quitting...") # if book_text is None: # sys.exit("Failed to get book text. Can not run tests without it. Quitting...") # # with open(output_path, "w") as f: # f.write(book_text) #### code based on https://stackoverflow.com/a/77873699 session = requests.Session() book = b"" expected_length = None i = 0 while len(book) != expected_length and i < 15: if len(book): headers = {"Range": f"bytes={len(book)}-"} expected_status = 206 else: headers = {} expected_status = 200 # print(f"{url}: got {len(book)} / {expected_length} bytes...") resp = session.get(url, stream=True, headers=headers) resp.raise_for_status() if resp.status_code != expected_status: raise ValueError(f"Unexpected status code: {resp.status_code}") if expected_length is None: # Only update this on the first request content_length = resp.headers.get("Content-Length") if not content_length: raise ValueError("Content-Length header not found") expected_length = int(content_length) try: for chunk in resp.iter_content(chunk_size=8192): book += chunk except requests.exceptions.ChunkedEncodingError: pass i += 1 if len(book) != expected_length: sys.exit("Failed to get book text. Can not run tests without it. Quitting...") #### code end with open(output_path, "w") as f: f.write(book.decode("utf-8",errors="ignore")) def generate_test_files(test_args): # generate simple file word_list = get_part_of_word_list(test_args["word_list"], 250) simple_delimiters = test_args["simple_delimiters"] max_msg_size = 1500 max_string_length = 50 * max_msg_size test_string = generate_text_from_word_list(word_list, simple_delimiters, max_string_length) f = open(test_args["filename_simple"], "w") f.write(test_string) f.close() # generate complex file word_list = get_part_of_word_list(test_args["word_list"], 2500) all_delimiters = test_args["all_delimiters"] max_string_length = 2.5e5 test_string = generate_text_from_word_list(word_list, all_delimiters, max_string_length) f = open(test_args["filename_complex"], "w") f.write(test_string) f.close() def kill_zmq_distributor_and_worker(kill_memcheck=False): try: distributor_procs = subprocess.check_output(["pidof", "zmq_distributor"]).decode("utf-8").split() distributor_procs = [int(x) for x in distributor_procs] except subprocess.CalledProcessError: distributor_procs = [] try: worker_procs = subprocess.check_output(["pidof", "zmq_worker"]).decode("utf-8").split() worker_procs = [int(x) for x in worker_procs] except subprocess.CalledProcessError: worker_procs = [] if kill_memcheck: # pgrep required, because pidof only returns exact matches try: memcheck_procs = subprocess.check_output(["pgrep", "memcheck"]).decode("utf-8").split() memcheck_procs = [int(x) for x in memcheck_procs] except subprocess.CalledProcessError: memcheck_procs = [] else: memcheck_procs = [] procs = distributor_procs + worker_procs + memcheck_procs for p in procs: try: os.kill(p, signal.SIGKILL) except ProcessLookupError: continue def get_part_of_word_list(word_list, n_words): word_list_part = random.sample(word_list, n_words) return word_list_part def random_capitalization(words): for i in range(len(words)): words[i] = ''.join(map(random.choice, zip(words[i].lower(), words[i].upper()))) def generate_text_from_word_list(w_list, delim, text_size): word_lengths = [len(x) for x in w_list] median_length = np.median(word_lengths) words_to_pick = int(text_size / median_length) words = random.choices(w_list, k=words_to_pick) random_capitalization(words) i = random.randint(25,100) while i < len(words): words.insert(i, "\n") i += random.randint(25,100) test_string = "".join(x + random.choice(delim) for x in words) return test_string def count_words(input_string): input_string = input_string.lower() words = re.findall("[a-z]+", input_string) words = Counter(words) words = sorted(words.items(), key=lambda a: (-a[1], a[0])) output_string = "word,frequency\n" for w in words: output_string += w[0] + "," + str(w[1]) + "\n" return output_string def create_test_debug_output(test_name, num_w, expected_output, dist_output): debug_base_path = "debug/" if not os.path.isdir(debug_base_path): os.mkdir(debug_base_path) out_name = debug_base_path + test_name + "_n-workers=" + str(num_w) + "_" f = open(out_name + "expected.txt", "w") f.write(expected_output) f.close() f = open(out_name + "distributor_output.txt", "w") f.write(dist_output) f.close() def run_worker(w_exec : Union[str, List[str]], port : str): proc_worker = None if isinstance(w_exec, str): proc_worker = subprocess.Popen([w_exec, port]) elif isinstance(w_exec, list): w_exec.append(port) proc_worker = subprocess.Popen(w_exec) proc_worker.wait() def join_workers(workers: list): for w in workers: if isinstance(w, subprocess.Popen): w.wait() elif isinstance(w, multiprocessing.Process): w.join() def start_threaded_workers(worker_exec : Union[str, List[List[str]]], port_list : list, use_multiprocessing = True): # use_multiprocessing = False if use_multiprocessing: worker_procs = [] if isinstance(worker_exec, str): for p in port_list: proc = multiprocessing.Process(target=run_worker, args=(worker_exec, p)) worker_procs.append(proc) proc.start() elif isinstance(worker_exec, list): for p, exe in zip(port_list, worker_exec): proc = multiprocessing.Process(target=run_worker, args=(exe, p)) worker_procs.append(proc) proc.start() return worker_procs else: proc_workers = None if isinstance(worker_exec, str): proc_workers = subprocess.Popen([worker_exec] + port_list) elif isinstance(worker_exec, list): proc_workers = subprocess.Popen(worker_exec[0] + port_list) return [proc_workers] def start_distributor(dist_args : List[str]): return subprocess.Popen(dist_args, stdout=subprocess.PIPE, encoding="ascii") def run_worker_load_distribution(port, return_dict): return_message = "test1worker1load1distribution1\0" context = zmq.Context.instance() socket = context.socket(zmq.REP) socket.bind("tcp://*:" + str(port)) received_requests = 0 while True: message = socket.recv() message = message.decode("ascii") msg_type = message[:3] if msg_type == "map" or msg_type == "red": received_requests += 1 socket.send(bytes(return_message, encoding="ascii")) elif msg_type == "rip": socket.send(bytes("rip\0", encoding="ascii")) break else: print("Unknown message type!") break socket.close() return_dict[port] = received_requests def check_valgrind_output_no_errors(filename): if not os.path.isfile(filename): return True tree = ET.parse(filename) errors = tree.findall("error") return len(errors) == 0 def cleanup_files(test_args, remove_valgrind_files=True): for k, v in test_args.items(): if not remove_valgrind_files and "valgrind" in k and ".xml" in v: continue if "filename" in k and os.path.isfile(v): os.remove(v) if test_args["no_cache"]: shutil.rmtree(test_args["cache_dir"])