From 31ddae6f0c96d546f2fde7e3f1991210eedad6f1 Mon Sep 17 00:00:00 2001 From: Egor Tensin Date: Sat, 13 Jun 2026 22:49:36 +0200 Subject: test: move stress test scripts to stress/ --- test/CMakeLists.txt | 9 +- test/stress/CMakeLists.txt | 7 ++ test/stress/test.py | 251 +++++++++++++++++++++++++++++++++++++++++++++ test/stress/test.sh | 74 +++++++++++++ test/stress_test.py | 251 --------------------------------------------- test/stress_test.sh | 74 ------------- 6 files changed, 333 insertions(+), 333 deletions(-) create mode 100644 test/stress/CMakeLists.txt create mode 100755 test/stress/test.py create mode 100755 test/stress/test.sh delete mode 100755 test/stress_test.py delete mode 100755 test/stress_test.sh diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a956d03..3391bee 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,10 +1,3 @@ add_subdirectory(benchmarks) +add_subdirectory(stress) add_subdirectory(unit_tests) - -if(CMAKE_HOST_UNIX) - # Not going to bother with bash on Windows. - - # stress_test.py is a Python 3 script. - find_package(Python3 REQUIRED COMPONENTS Interpreter) - add_test(NAME stress_test COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/stress_test.sh" "$" "$") -endif() diff --git a/test/stress/CMakeLists.txt b/test/stress/CMakeLists.txt new file mode 100644 index 0000000..d73c65d --- /dev/null +++ b/test/stress/CMakeLists.txt @@ -0,0 +1,7 @@ +if(CMAKE_HOST_UNIX) + # Not going to bother with bash on Windows. + + # stress_test.py is a Python 3 script. + find_package(Python3 REQUIRED COMPONENTS Interpreter) + add_test(NAME stress_test COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/test.sh" "$" "$") +endif() diff --git a/test/stress/test.py b/test/stress/test.py new file mode 100755 index 0000000..2701db3 --- /dev/null +++ b/test/stress/test.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2019 Egor Tensin +# This file is part of the "math-server" project. +# For details, see https://github.com/egor-tensin/math-server. +# Distributed under the MIT License. + +'''This is a stupid script to feed the server with somewhat random arithmetic +expressions. +''' + +import argparse +from contextlib import contextmanager +import logging +import math +from multiprocessing import Pool +import random +import subprocess +from subprocess import PIPE +import sys +from timeit import default_timer + + +class Client: + DEFAULT_PATH = 'math-client' + DEFAULT_HOST = 'localhost' + DEFAULT_PORT = 18000 + + def __init__(self, path=None, host=None, port=None): + if path is None: + path = Client.DEFAULT_PATH + if host is None: + host = Client.DEFAULT_HOST + if port is None: + port = Client.DEFAULT_PORT + self._path = path + self._host = host + self._port = port + + def get_command_line(self): + return [self._path, '--host', self._host, '--port', str(self._port)] + + +@contextmanager +def timer(description): + start = default_timer() + try: + yield + finally: + duration = default_timer() - start + logging.info('%s: %.03f seconds', description, duration) + + +class Expr: + def __init__(self, impl): + self._impl = impl + + def __str__(self): + return self._impl + + def eval(self): + try: + return eval(self._impl) + except Exception: + logging.error("Couldn't evaluate expression: %s", self._impl) + raise + + +class ExprGen: + _OPERATORS = '+', '-', '*', '/' + _MIN_NUMOF_OPERATORS = 10 + _MAX_NUMOF_OPERATORS = 1000 + _MIN_NUMBER = int(-10e10) + _MAX_NUMBER = int(10e10) + + @staticmethod + def _random_operator(): + return ExprGen._OPERATORS[random.randrange(len(ExprGen._OPERATORS))] + + @staticmethod + def _random_number(): + return random.randint(ExprGen._MIN_NUMBER, ExprGen._MAX_NUMBER) + + @staticmethod + def gen_expression(): + numof_operators = random.randrange(ExprGen._MIN_NUMOF_OPERATORS, + ExprGen._MAX_NUMOF_OPERATORS + 1) + expr = '' + for _ in range(numof_operators): + expr += f"{ExprGen._random_number()} {ExprGen._random_operator()} " + expr += str(ExprGen._random_number()) + return Expr(expr) + + +class Input: + def __init__(self, expr_lst): + self._expr_lst = expr_lst + + @staticmethod + def generate(n): + if n < 1: + raise ValueError('input length must be positive') + return Input([ExprGen.gen_expression() for i in range(n)]) + + def stdin(self): + return '\n'.join(map(str, self._expr_lst)) + '\n' + + def expected_output(self): + return [expr.eval() for expr in self._expr_lst] + + +def _float_lists_equal(xs, ys): + if len(xs) != len(ys): + return False + for i in range(len(xs)): + if not math.isclose(xs[i], ys[i]): + return False + return True + + +class Output: + def __init__(self, stdout): + self._stdout = stdout + + def __eq__(self, other): + return self._stdout == other._stdout + + def __str__(self): + return self._stdout + + def parse(self): + values = [] + for line in self._stdout.split('\n')[:-1]: + try: + n = float(line) + except ValueError: + logging.error('Not a number: %s', line) + raise + values.append(n) + return values + + def equal_to_expected(self, expected): + this = self.parse() + if not _float_lists_equal(this, expected): + logging.error("Actual output doesn't match expected output") + logging.error('Expected output (length %d):\n%s', len(expected), expected) + logging.error('Actual output (length %d):\n%s', len(this), this) + return False + return True + + +def _run_client(client, stdin): + with _logging(): + cmd = client.get_command_line() + with timer('Client invocation'): + result = subprocess.run(cmd, stdout=PIPE, stderr=PIPE, input=stdin, + universal_newlines=True, check=True) + return Output(result.stdout) + + +def _run_clients(numof_clients, client, stdin): + with Pool(numof_clients) as pool: + results = pool.starmap(_run_client, [(client, stdin) for _ in range(numof_clients)]) + if len(results) != numof_clients: + raise RuntimeError(f'expected {numof_clients} results, got {len(results)}') + return results + + +def _run_stress_test(args): + client = Client(args.client, args.host, args.port) + input = Input.generate(args.numof_expressions) + stdin = input.stdin() + expected_output = input.expected_output() + actual_outputs = _run_clients(args.numof_processes, client, stdin) + + # Check that all outputs are equal to each other: + assert actual_outputs + actual_output, rest = actual_outputs[0], actual_outputs[1:] + for other in rest: + if actual_output == other: + continue + logging.error("Client outputs don't match, this should never happen") + logging.error('For example, this:\n%s', actual_output) + logging.error('... is not equal to this:\n%s', other) + return False + + # Check that the first output is equal to the expected output: + return actual_output.equal_to_expected(expected_output) + + +def _parse_positive_int(s): + try: + n = int(s) + except ValueError: + raise argparse.ArgumentTypeError(f'must be a positive integer: {s}') + if n < 1: + raise argparse.ArgumentTypeError(f'must be a positive integer: {s}') + return n + + +def _parse_args(argv=None): + if argv is None: + argv = sys.argv[1:] + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + + parser.add_argument('--host', '-H', metavar='HOST', + default=Client.DEFAULT_HOST, + help='server host') + parser.add_argument('--port', '-p', metavar='PORT', + type=int, default=Client.DEFAULT_PORT, + help='server port') + parser.add_argument('--processes', '-n', metavar='N', + dest='numof_processes', + type=_parse_positive_int, default=1, + help='number of processes') + parser.add_argument('--expressions', '-e', metavar='N', + dest='numof_expressions', + type=_parse_positive_int, default=1, + help='number of expressions') + parser.add_argument('--client', '-c', metavar='PATH', + default=Client.DEFAULT_PATH, + help='path to the client executable') + + return parser.parse_args(argv) + + +@contextmanager +def _logging(): + logging.basicConfig( + format='%(asctime)s | %(levelname)s | %(message)s', + datefmt='%Y-%m-%d %H:%M:%S%z', + level=logging.DEBUG) + try: + yield + except Exception as e: + logging.exception(e) + raise + + +def main(argv=None): + with _logging(): + args = _parse_args(argv) + if not _run_stress_test(args): + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/test/stress/test.sh b/test/stress/test.sh new file mode 100755 index 0000000..8aa3c1d --- /dev/null +++ b/test/stress/test.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash + +# Copyright (c) 2020 Egor Tensin +# This file is part of the "math-server" project. +# For details, see https://github.com/egor-tensin/math-server. +# Distributed under the MIT License. + +set -o errexit -o nounset -o pipefail +shopt -s inherit_errexit lastpipe + +script_name="$( basename -- "${BASH_SOURCE[0]}" )" +readonly script_name +script_dir="$( dirname -- "${BASH_SOURCE[0]}" )" +script_dir="$( cd -- "$script_dir" && pwd )" +readonly script_dir + +server_path= +client_path= +readonly stress_test_path="$script_dir/test.py" +readonly server_port=16666 +server_pid= + +dump() { + local msg + for msg; do + echo "$script_name: $msg" + done +} + +kill_server() { + dump "Killing the server with PID $server_pid..." + kill "$server_pid" + dump "Waiting for the server to terminate..." + wait "$server_pid" + dump "Done" +} + +run_server() { + dump "Running the server..." + "$server_path" --port "$server_port" & + server_pid="$!" + dump "Its PID is $server_pid" + trap kill_server EXIT +} + +run_stress_test() { + dump "Running stress_test.py..." + "$stress_test_path" \ + --client "$client_path" \ + --port "$server_port" \ + --processes 4 \ + --expressions 1000 +} + +script_usage() { + echo "usage: $script_name SERVER_PATH CLIENT_PATH" +} + +parse_args() { + if [ "$#" -ne 2 ]; then + script_usage >&2 + return 1 + fi + server_path="$1" + client_path="$2" +} + +main() { + parse_args "$@" + run_server + run_stress_test +} + +main "$@" diff --git a/test/stress_test.py b/test/stress_test.py deleted file mode 100755 index 2701db3..0000000 --- a/test/stress_test.py +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright (c) 2019 Egor Tensin -# This file is part of the "math-server" project. -# For details, see https://github.com/egor-tensin/math-server. -# Distributed under the MIT License. - -'''This is a stupid script to feed the server with somewhat random arithmetic -expressions. -''' - -import argparse -from contextlib import contextmanager -import logging -import math -from multiprocessing import Pool -import random -import subprocess -from subprocess import PIPE -import sys -from timeit import default_timer - - -class Client: - DEFAULT_PATH = 'math-client' - DEFAULT_HOST = 'localhost' - DEFAULT_PORT = 18000 - - def __init__(self, path=None, host=None, port=None): - if path is None: - path = Client.DEFAULT_PATH - if host is None: - host = Client.DEFAULT_HOST - if port is None: - port = Client.DEFAULT_PORT - self._path = path - self._host = host - self._port = port - - def get_command_line(self): - return [self._path, '--host', self._host, '--port', str(self._port)] - - -@contextmanager -def timer(description): - start = default_timer() - try: - yield - finally: - duration = default_timer() - start - logging.info('%s: %.03f seconds', description, duration) - - -class Expr: - def __init__(self, impl): - self._impl = impl - - def __str__(self): - return self._impl - - def eval(self): - try: - return eval(self._impl) - except Exception: - logging.error("Couldn't evaluate expression: %s", self._impl) - raise - - -class ExprGen: - _OPERATORS = '+', '-', '*', '/' - _MIN_NUMOF_OPERATORS = 10 - _MAX_NUMOF_OPERATORS = 1000 - _MIN_NUMBER = int(-10e10) - _MAX_NUMBER = int(10e10) - - @staticmethod - def _random_operator(): - return ExprGen._OPERATORS[random.randrange(len(ExprGen._OPERATORS))] - - @staticmethod - def _random_number(): - return random.randint(ExprGen._MIN_NUMBER, ExprGen._MAX_NUMBER) - - @staticmethod - def gen_expression(): - numof_operators = random.randrange(ExprGen._MIN_NUMOF_OPERATORS, - ExprGen._MAX_NUMOF_OPERATORS + 1) - expr = '' - for _ in range(numof_operators): - expr += f"{ExprGen._random_number()} {ExprGen._random_operator()} " - expr += str(ExprGen._random_number()) - return Expr(expr) - - -class Input: - def __init__(self, expr_lst): - self._expr_lst = expr_lst - - @staticmethod - def generate(n): - if n < 1: - raise ValueError('input length must be positive') - return Input([ExprGen.gen_expression() for i in range(n)]) - - def stdin(self): - return '\n'.join(map(str, self._expr_lst)) + '\n' - - def expected_output(self): - return [expr.eval() for expr in self._expr_lst] - - -def _float_lists_equal(xs, ys): - if len(xs) != len(ys): - return False - for i in range(len(xs)): - if not math.isclose(xs[i], ys[i]): - return False - return True - - -class Output: - def __init__(self, stdout): - self._stdout = stdout - - def __eq__(self, other): - return self._stdout == other._stdout - - def __str__(self): - return self._stdout - - def parse(self): - values = [] - for line in self._stdout.split('\n')[:-1]: - try: - n = float(line) - except ValueError: - logging.error('Not a number: %s', line) - raise - values.append(n) - return values - - def equal_to_expected(self, expected): - this = self.parse() - if not _float_lists_equal(this, expected): - logging.error("Actual output doesn't match expected output") - logging.error('Expected output (length %d):\n%s', len(expected), expected) - logging.error('Actual output (length %d):\n%s', len(this), this) - return False - return True - - -def _run_client(client, stdin): - with _logging(): - cmd = client.get_command_line() - with timer('Client invocation'): - result = subprocess.run(cmd, stdout=PIPE, stderr=PIPE, input=stdin, - universal_newlines=True, check=True) - return Output(result.stdout) - - -def _run_clients(numof_clients, client, stdin): - with Pool(numof_clients) as pool: - results = pool.starmap(_run_client, [(client, stdin) for _ in range(numof_clients)]) - if len(results) != numof_clients: - raise RuntimeError(f'expected {numof_clients} results, got {len(results)}') - return results - - -def _run_stress_test(args): - client = Client(args.client, args.host, args.port) - input = Input.generate(args.numof_expressions) - stdin = input.stdin() - expected_output = input.expected_output() - actual_outputs = _run_clients(args.numof_processes, client, stdin) - - # Check that all outputs are equal to each other: - assert actual_outputs - actual_output, rest = actual_outputs[0], actual_outputs[1:] - for other in rest: - if actual_output == other: - continue - logging.error("Client outputs don't match, this should never happen") - logging.error('For example, this:\n%s', actual_output) - logging.error('... is not equal to this:\n%s', other) - return False - - # Check that the first output is equal to the expected output: - return actual_output.equal_to_expected(expected_output) - - -def _parse_positive_int(s): - try: - n = int(s) - except ValueError: - raise argparse.ArgumentTypeError(f'must be a positive integer: {s}') - if n < 1: - raise argparse.ArgumentTypeError(f'must be a positive integer: {s}') - return n - - -def _parse_args(argv=None): - if argv is None: - argv = sys.argv[1:] - - parser = argparse.ArgumentParser( - description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter) - - parser.add_argument('--host', '-H', metavar='HOST', - default=Client.DEFAULT_HOST, - help='server host') - parser.add_argument('--port', '-p', metavar='PORT', - type=int, default=Client.DEFAULT_PORT, - help='server port') - parser.add_argument('--processes', '-n', metavar='N', - dest='numof_processes', - type=_parse_positive_int, default=1, - help='number of processes') - parser.add_argument('--expressions', '-e', metavar='N', - dest='numof_expressions', - type=_parse_positive_int, default=1, - help='number of expressions') - parser.add_argument('--client', '-c', metavar='PATH', - default=Client.DEFAULT_PATH, - help='path to the client executable') - - return parser.parse_args(argv) - - -@contextmanager -def _logging(): - logging.basicConfig( - format='%(asctime)s | %(levelname)s | %(message)s', - datefmt='%Y-%m-%d %H:%M:%S%z', - level=logging.DEBUG) - try: - yield - except Exception as e: - logging.exception(e) - raise - - -def main(argv=None): - with _logging(): - args = _parse_args(argv) - if not _run_stress_test(args): - sys.exit(1) - - -if __name__ == '__main__': - main() diff --git a/test/stress_test.sh b/test/stress_test.sh deleted file mode 100755 index 3566701..0000000 --- a/test/stress_test.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2020 Egor Tensin -# This file is part of the "math-server" project. -# For details, see https://github.com/egor-tensin/math-server. -# Distributed under the MIT License. - -set -o errexit -o nounset -o pipefail -shopt -s inherit_errexit lastpipe - -script_name="$( basename -- "${BASH_SOURCE[0]}" )" -readonly script_name -script_dir="$( dirname -- "${BASH_SOURCE[0]}" )" -script_dir="$( cd -- "$script_dir" && pwd )" -readonly script_dir - -server_path= -client_path= -readonly stress_test_path="$script_dir/stress_test.py" -readonly server_port=16666 -server_pid= - -dump() { - local msg - for msg; do - echo "$script_name: $msg" - done -} - -kill_server() { - dump "Killing the server with PID $server_pid..." - kill "$server_pid" - dump "Waiting for the server to terminate..." - wait "$server_pid" - dump "Done" -} - -run_server() { - dump "Running the server..." - "$server_path" --port "$server_port" & - server_pid="$!" - dump "Its PID is $server_pid" - trap kill_server EXIT -} - -run_stress_test() { - dump "Running stress_test.py..." - "$stress_test_path" \ - --client "$client_path" \ - --port "$server_port" \ - --processes 4 \ - --expressions 1000 -} - -script_usage() { - echo "usage: $script_name SERVER_PATH CLIENT_PATH" -} - -parse_args() { - if [ "$#" -ne 2 ]; then - script_usage >&2 - return 1 - fi - server_path="$1" - client_path="$2" -} - -main() { - parse_args "$@" - run_server - run_stress_test -} - -main "$@" -- cgit v1.3