From 9d2decb2108cdfaa3176bbf0f5692331d27608bf Mon Sep 17 00:00:00 2001 From: Graeme A Stewart Date: Mon, 7 Sep 2020 20:32:10 +0200 Subject: [PATCH] Reformat and lint files with black and flake8 (#168) * Clean up of main plotting script Format with black; fix many, many defects identified with pylint * Change script names to conform with PEP8 Python prefers snake_case for script and module names and this fixes pylint complaining about this * Add flake8 and black targets black target will reformat all Python scripts flake8 will run linter on all Python scripts * Reformat plotting script to improve linting Passes flake8 (required) and most of pylint (recommended) * Reformat to improve linting Pass flake8 (required) and most of pylint (recommended) * Add notes on Python formatting black should be used for formatting scripts must be error free with flake8 * Add flake8 run to actions * Fix network test and trailing spaces * Fix strange unicode chars in print string * Fix name of Python2 http server module * Refactor to avoid globals pylint checks pushed an implementation where most code was inside the main() function, however this caused a problem with unittest that will only run tests in the global namespace, but use of globals is rather nasty This reimplementaion keeps the argument parsing and test case generation in a main-ish function, but returns the test case and then invokes unittest.main() which is a much nicer implementation * Fix syntax for strategy * Remove strategy matrix Only need to run flake8 on one platform * Whitelist codefactor/bandit urlopen() The urlopen() call in these tests is to the http server that was setup by the unittest itself, so we know that it's ok * Fix flake8 error This was an accident, but it proved that the flake8 GH action works as we want! Co-authored-by: Graeme A Stewart --- .flake8 | 7 + .github/scripts/flake8.sh | 4 + .github/workflows/flake8.yml | 37 ++++ CMakeLists.txt | 3 +- cmake/python-format.cmake | 21 ++ doc/CONTRIBUTING.md | 8 +- package/scripts/prmon_plot.py | 400 ++++++++++++++++++++-------------- package/tests/CMakeLists.txt | 50 ++--- package/tests/httpBlock.py | 35 --- package/tests/httpBlock2.py | 35 --- package/tests/http_block.py | 40 ++++ package/tests/http_block2.py | 40 ++++ package/tests/netBurner.py | 39 ---- package/tests/netBurner2.py | 40 ---- package/tests/net_burner.py | 49 +++++ package/tests/net_burner2.py | 50 +++++ package/tests/testCOUNT.py | 71 ------ package/tests/testCPU.py | 96 -------- package/tests/testEXIT.py | 38 ---- package/tests/testIO.py | 60 ----- package/tests/testMEM.py | 70 ------ package/tests/testNET.py | 73 ------- package/tests/testNET2.py | 74 ------- package/tests/test_count.py | 95 ++++++++ package/tests/test_cpu.py | 154 +++++++++++++ package/tests/test_exit.py | 57 +++++ package/tests/test_io.py | 102 +++++++++ package/tests/test_mem.py | 111 ++++++++++ package/tests/test_net.py | 113 ++++++++++ package/tests/test_net2.py | 112 ++++++++++ 30 files changed, 1264 insertions(+), 820 deletions(-) create mode 100644 .flake8 create mode 100755 .github/scripts/flake8.sh create mode 100644 .github/workflows/flake8.yml create mode 100644 cmake/python-format.cmake delete mode 100755 package/tests/httpBlock.py delete mode 100755 package/tests/httpBlock2.py create mode 100755 package/tests/http_block.py create mode 100755 package/tests/http_block2.py delete mode 100755 package/tests/netBurner.py delete mode 100755 package/tests/netBurner2.py create mode 100755 package/tests/net_burner.py create mode 100755 package/tests/net_burner2.py delete mode 100755 package/tests/testCOUNT.py delete mode 100755 package/tests/testCPU.py delete mode 100755 package/tests/testEXIT.py delete mode 100755 package/tests/testIO.py delete mode 100755 package/tests/testMEM.py delete mode 100755 package/tests/testNET.py delete mode 100755 package/tests/testNET2.py create mode 100755 package/tests/test_count.py create mode 100755 package/tests/test_cpu.py create mode 100755 package/tests/test_exit.py create mode 100755 package/tests/test_io.py create mode 100755 package/tests/test_mem.py create mode 100755 package/tests/test_net.py create mode 100755 package/tests/test_net2.py diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..4d91861 --- /dev/null +++ b/.flake8 @@ -0,0 +1,7 @@ +# Minimal flake8 tweak to be compatible with black's line length +# of 88 characters +# https://black.readthedocs.io/en/stable/the_black_code_style.html#line-length + +[flake8] +max-line-length = 88 +extend-ignore = E203 diff --git a/.github/scripts/flake8.sh b/.github/scripts/flake8.sh new file mode 100755 index 0000000..8aab45a --- /dev/null +++ b/.github/scripts/flake8.sh @@ -0,0 +1,4 @@ +#! /bin/sh +# Configure and run flake8 target +cmake /mnt +cmake --build . --target flake8 diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml new file mode 100644 index 0000000..ed28074 --- /dev/null +++ b/.github/workflows/flake8.yml @@ -0,0 +1,37 @@ +# Name of the workflow +name: flake8 + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master and stable branches +on: + push: + branches: [ master, stable ] + pull_request: + branches: [ master, stable ] + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # Main building and testing + build-test: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out our repository under $GITHUB_WORKSPACE, so our job can access it + - uses: actions/checkout@v2 + + # Sets up useful environment variables + - name: Setup environment variables + run: | + echo "::set-env name=DIMAGE::hepsoftwarefoundation/u20-dev" + env: + PLATFORM: u20-dev + + # Pulls the associated Docker image + - name: Docker pull + run: docker pull $DIMAGE + + # Builds the code and runs the test + - name: Flake8 Lint + run: docker run -v $(pwd):/mnt $DIMAGE /mnt/.github/scripts/flake8.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index a320e81..2ec426d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -94,5 +94,6 @@ add_subdirectory(package) #--- create uninstall target --------------------------------------------------- include(cmake/prmonUninstall.cmake) -#--- clang-format target ------------------------------------------------------- +#--- code format targets ------------------------------------------------------- include(cmake/clang-format.cmake) +include(cmake/python-format.cmake) diff --git a/cmake/python-format.cmake b/cmake/python-format.cmake new file mode 100644 index 0000000..effd7c3 --- /dev/null +++ b/cmake/python-format.cmake @@ -0,0 +1,21 @@ +# Additional target to run python linters and formatters on python scripts +# +# Requires black/flake8 to be available in the environment + + +# Get all Python files +file(GLOB_RECURSE ALL_PYTHON_FILES *.py) + +# Black is rather simple because there are no options... +add_custom_target( + black + COMMAND black + ${ALL_PYTHON_FILES} +) + +add_custom_target( + flake8 + COMMAND flake8 + --config=${CMAKE_CURRENT_SOURCE_DIR}/.flake8 + ${ALL_PYTHON_FILES} +) diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md index f2a365e..5adb89a 100644 --- a/doc/CONTRIBUTING.md +++ b/doc/CONTRIBUTING.md @@ -40,10 +40,16 @@ repository. - the feature should be supported by an integration test - the code should be commented as needed so that others can understand it -- code should be formatted using the *Google* style of `clang-format` +- C++ code should be formatted using the *Google* style of `clang-format` - i.e. `clang-format --style=Google ...` - There is a `clang-format` CMake target that will do this automatically so we recommend that is run before making the pull request +- Python code should be formatted with `black` (following the Scikit-HEP + recommendation) and should also be clean with the `flake8` linter + - See the `.flake8` file for the configuration to be compatible with + `black` + - There are `black` and `flake8` build targets that will run the + formatter and linter respectively We will review the code before accepting the pull request. diff --git a/package/scripts/prmon_plot.py b/package/scripts/prmon_plot.py index a3f4c0c..c302d0b 100755 --- a/package/scripts/prmon_plot.py +++ b/package/scripts/prmon_plot.py @@ -1,4 +1,5 @@ #! /usr/bin/env python3 +"""prmon output data plotting script""" import argparse import sys @@ -8,196 +9,265 @@ import pandas as pd import numpy as np import matplotlib as mpl - mpl.use('Agg') + + mpl.use("Agg") import matplotlib.pyplot as plt - plt.style.use('seaborn-whitegrid') + + plt.style.use("seaborn-whitegrid") except ImportError: - print('{0: <8}:: This script needs numpy, pandas and matplotlib.'.format('ERROR')) - print('{0: <8}:: Looks like at least one of these modules is missing.'.format('ERROR')) - print('{0: <8}:: Please install them first and then retry.'.format('ERROR')) + print("{0: <8}:: This script needs numpy, pandas and matplotlib.".format("ERROR")) + print( + "{0: <8}:: Looks like at least one of these modules is missing.".format("ERROR") + ) + print("{0: <8}:: Please install them first and then retry.".format("ERROR")) sys.exit(-1) # Define the labels/units for beautification # First allowed unit is the default -allowedunits = {'vmem':['kb','b','mb','gb'], - 'pss':['kb','b','mb','gb'], - 'rss':['kb','b','mb','gb'], - 'swap':['kb','b','mb','gb'], - 'utime':['sec','min','hour'], - 'stime':['sec','min','hour'], - 'wtime':['sec','min','hour'], - 'rchar':['b','kb','mb','gb'], - 'wchar':['b','kb','mb','gb'], - 'read_bytes':['b','kb','mb','gb'], - 'write_bytes':['b','kb','mb','gb'], - 'rx_packets':['1'], - 'tx_packets':['1'], - 'rx_bytes':['b','kb','mb','gb'], - 'tx_bytes':['b','kb','mb','gb'], - 'nprocs':['1'], - 'nthreads':['1'], - 'gpufbmem':['kb','b','mb','gb'], - 'gpumempct':['%'], - 'gpusmpct':['%'], - 'ngpus':['1']} - -axisnames = {'vmem':'Memory', - 'pss':'Memory', - 'rss':'Memory', - 'swap':'Memory', - 'utime':'CPU-time', - 'stime':'CPU-time', - 'wtime':'Wall-time', - 'rchar':'I/O', - 'wchar':'I/O', - 'read_bytes':'I/O', - 'write_bytes':'I/O', - 'rx_packets':'Network', - 'tx_packets':'Network', - 'rx_bytes':'Network', - 'tx_bytes':'Network', - 'nprocs':'Count', - 'nthreads':'Count', - 'gpufbmem':'Memory', - 'gpumempct':'Memory', - 'gpusmpct':'Streaming Multiprocessors', - 'ngpus':'Count'} - -legendnames = {'vmem':'Virtual Memory', - 'pss':'Proportional Set Size', - 'rss':'Resident Set Size', - 'swap':'Swap Size', - 'utime':'User CPU-time', - 'stime':'System CPU-time', - 'wtime':'Wall-time', - 'rchar':'I/O Read (rchar)', - 'wchar':'I/O Written (wchar)', - 'read_bytes':'I/O Read (read_bytes)', - 'write_bytes':'I/O Written (write_bytes)', - 'rx_packets':'Network Received (packets)', - 'tx_packets':'Network Transmitted (packets)', - 'rx_bytes':'Network Received (bytes)', - 'tx_bytes':'Network Transmitted (bytes)', - 'nprocs':'Number of Processes', - 'nthreads':'Number of Threads', - 'gpufbmem':'GPU Memory', - 'gpumempct':'GPU Memory', - 'gpusmpct':'GPU Streaming Multiprocessors', - 'ngpus':'Number of GPUs'} - -multipliers = {'SEC': 1., - 'MIN': 60., - 'HOUR': 60.*60., - 'B': 1., - 'KB': 1024., - 'MB': 1024.*1024., - 'GB': 1024.*1024.*1024., - '1': 1., - '%': 1.} - -# A few basic functions for labels/ conversions -def get_axis_label(nom, denom = None): - label = axisnames[nom] +ALLOWEDUNITS = { + "vmem": ["kb", "b", "mb", "gb"], + "pss": ["kb", "b", "mb", "gb"], + "rss": ["kb", "b", "mb", "gb"], + "swap": ["kb", "b", "mb", "gb"], + "utime": ["sec", "min", "hour"], + "stime": ["sec", "min", "hour"], + "wtime": ["sec", "min", "hour"], + "rchar": ["b", "kb", "mb", "gb"], + "wchar": ["b", "kb", "mb", "gb"], + "read_bytes": ["b", "kb", "mb", "gb"], + "write_bytes": ["b", "kb", "mb", "gb"], + "rx_packets": ["1"], + "tx_packets": ["1"], + "rx_bytes": ["b", "kb", "mb", "gb"], + "tx_bytes": ["b", "kb", "mb", "gb"], + "nprocs": ["1"], + "nthreads": ["1"], + "gpufbmem": ["kb", "b", "mb", "gb"], + "gpumempct": ["%"], + "gpusmpct": ["%"], + "ngpus": ["1"], +} + +AXISNAME = { + "vmem": "Memory", + "pss": "Memory", + "rss": "Memory", + "swap": "Memory", + "utime": "CPU-time", + "stime": "CPU-time", + "wtime": "Wall-time", + "rchar": "I/O", + "wchar": "I/O", + "read_bytes": "I/O", + "write_bytes": "I/O", + "rx_packets": "Network", + "tx_packets": "Network", + "rx_bytes": "Network", + "tx_bytes": "Network", + "nprocs": "Count", + "nthreads": "Count", + "gpufbmem": "Memory", + "gpumempct": "Memory", + "gpusmpct": "Streaming Multiprocessors", + "ngpus": "Count", +} + +LEGENDNAMES = { + "vmem": "Virtual Memory", + "pss": "Proportional Set Size", + "rss": "Resident Set Size", + "swap": "Swap Size", + "utime": "User CPU-time", + "stime": "System CPU-time", + "wtime": "Wall-time", + "rchar": "I/O Read (rchar)", + "wchar": "I/O Written (wchar)", + "read_bytes": "I/O Read (read_bytes)", + "write_bytes": "I/O Written (write_bytes)", + "rx_packets": "Network Received (packets)", + "tx_packets": "Network Transmitted (packets)", + "rx_bytes": "Network Received (bytes)", + "tx_bytes": "Network Transmitted (bytes)", + "nprocs": "Number of Processes", + "nthreads": "Number of Threads", + "gpufbmem": "GPU Memory", + "gpumempct": "GPU Memory", + "gpusmpct": "GPU Streaming Multiprocessors", + "ngpus": "Number of GPUs", +} + +MULTIPLIERS = { + "SEC": 1.0, + "MIN": 60.0, + "HOUR": 60.0 * 60.0, + "B": 1.0, + "KB": 1024.0, + "MB": 1024.0 * 1024.0, + "GB": 1024.0 * 1024.0 * 1024.0, + "1": 1.0, + "%": 1.0, +} + + +# A few basic functions for labels and conversions +def get_axis_label(nom, denom=None): + """Generate axis label from variable and units""" + label = AXISNAME[nom] if denom: - label = '$\Delta$'+label+'/$\Delta$'+axisnames[denom] + label = r"$\Delta$" + label + r"/$\Delta$" + AXISNAME[denom] return label + def get_multiplier(label, unit): - return multipliers[allowedunits[label][0].upper()]/multipliers[unit] + """Get the multiplication constant for a label""" + return MULTIPLIERS[ALLOWEDUNITS[label][0].upper()] / MULTIPLIERS[unit] + -# Main function -if '__main__' in __name__: +def main(): + """prmon plotting main function""" # Default xvar, xunit, yvar, and yunit - default_xvar, default_xunit = 'wtime', allowedunits['wtime'][0].upper() - default_yvar, default_yunit = 'pss', allowedunits['pss'][0].upper() + default_xvar, default_xunit = "wtime", ALLOWEDUNITS["wtime"][0].upper() + default_yvar, default_yunit = "pss", ALLOWEDUNITS["pss"][0].upper() # Parse the user input - parser = argparse.ArgumentParser(description = 'Configurable plotting script') - parser.add_argument('--input', type = str, default = 'prmon.txt', - help = 'PrMon TXT output that will be used as input') - parser.add_argument('--output', type = str, default = None, - help = 'name of the output image without the file extension') - parser.add_argument('--xvar', type = str, default = default_xvar, - help = 'name of the variable to be plotted in the x-axis') - parser.add_argument('--xunit', nargs = '?', default = default_xunit, - choices=['SEC', 'MIN', 'HOUR', 'B', 'KB', 'MB', 'GB', '1','%'], - help = 'unit of the variable to be plotted in the x-axis') - parser.add_argument('--yvar', type = str, default = default_yvar, - help = 'name(s) of the variable(s) to be plotted in the y-axis' - ' (comma seperated list is accepted)') - parser.add_argument('--yunit', nargs = '?', default = default_yunit, - choices=['SEC', 'MIN', 'HOUR', 'B', 'KB', 'MB', 'GB', '1','%'], - help = 'unit of the variable(s) to be plotted in the y-axis') - parser.add_argument('--stacked', dest = 'stacked', action = 'store_true', - help = 'stack plots if specified') - parser.add_argument('--diff', dest = 'diff', action = 'store_true', - help = 'plot the ratio of the discrete differences of ' - ' the elements for yvars and xvars if specified (i.e. dy/dx)') - parser.add_argument('--otype', nargs = '?', default = 'png', - choices=['png', 'pdf', 'svg'], - help = 'format of the output image') - parser.set_defaults(stacked = False) - parser.set_defaults(diff = False) + parser = argparse.ArgumentParser(description="Configurable plotting script") + parser.add_argument( + "--input", + type=str, + default="prmon.txt", + help="PrMon TXT output that will be used as input", + ) + parser.add_argument( + "--output", + type=str, + default=None, + help="name of the output image without the file extension", + ) + parser.add_argument( + "--xvar", + type=str, + default=default_xvar, + help="name of the variable to be plotted in the x-axis", + ) + parser.add_argument( + "--xunit", + nargs="?", + default=default_xunit, + choices=["SEC", "MIN", "HOUR", "B", "KB", "MB", "GB", "1", "%"], + help="unit of the variable to be plotted in the x-axis", + ) + parser.add_argument( + "--yvar", + type=str, + default=default_yvar, + help="name(s) of the variable(s) to be plotted in the y-axis" + " (comma seperated list is accepted)", + ) + parser.add_argument( + "--yunit", + nargs="?", + default=default_yunit, + choices=["SEC", "MIN", "HOUR", "B", "KB", "MB", "GB", "1", "%"], + help="unit of the variable(s) to be plotted in the y-axis", + ) + parser.add_argument( + "--stacked", + dest="stacked", + action="store_true", + help="stack plots if specified", + ) + parser.add_argument( + "--diff", + dest="diff", + action="store_true", + help="plot the ratio of the discrete differences of " + " the elements for yvars and xvars if specified (i.e. dy/dx)", + ) + parser.add_argument( + "--otype", + nargs="?", + default="png", + choices=["png", "pdf", "svg"], + help="format of the output image", + ) + parser.set_defaults(stacked=False) + parser.set_defaults(diff=False) args = parser.parse_args() # Check the input file exists if not os.path.exists(args.input): - print('{0: <8}:: Input file {1} does not exists'.format('ERROR', args.input)) + print("{0: <8}:: Input file {1} does not exists".format("ERROR", args.input)) sys.exit(-1) # Load the data - data = pd.read_csv(args.input, sep = '\t') - data['Time'] = pd.to_datetime(data['Time'], unit = 's') + data = pd.read_csv(args.input, sep="\t") + data["Time"] = pd.to_datetime(data["Time"], unit="s") # Check the variables are in data if args.xvar not in list(data): - print('{0: <8}:: Variable {1} is not available in data'.format('ERROR',args.xvar)) + print( + "{0: <8}:: Variable {1} is not available in data".format("ERROR", args.xvar) + ) sys.exit(-1) - ylist = args.yvar.split(',') + ylist = args.yvar.split(",") for carg in ylist: if carg not in list(data): - print('{0: <8}:: Variable {1} is not available in data'.format('ERROR',carg)) + print( + "{0: <8}:: Variable {1} is not available in data".format("ERROR", carg) + ) sys.exit(-1) # Check the consistency of variables and units # If they don't match, reset the units to defaults - firstXVariable = args.xvar.split(',')[0] - if args.xunit.lower() not in allowedunits[firstXVariable]: + first_x_variable = args.xvar.split(",")[0] + if args.xunit.lower() not in ALLOWEDUNITS[first_x_variable]: old_xunit = args.xunit - args.xunit = allowedunits[firstXVariable][0].upper() - print('{0: <8}:: Changing xunit from {1} to {2} for consistency'.format('WARNING', - old_xunit, - args.xunit)) - firstYVariable = args.yvar.split(',')[0] - if args.yunit.lower() not in allowedunits[firstYVariable]: + args.xunit = ALLOWEDUNITS[first_x_variable][0].upper() + print( + "{0: <8}:: Changing xunit from {1} to {2} for consistency".format( + "WARNING", old_xunit, args.xunit + ) + ) + first_y_variable = args.yvar.split(",")[0] + if args.yunit.lower() not in ALLOWEDUNITS[first_y_variable]: old_yunit = args.yunit - args.yunit = allowedunits[firstYVariable][0].upper() - print('{0: <8}:: Changing yunit from {1} to {2} for consistency'.format('WARNING', - old_yunit, - args.yunit)) + args.yunit = ALLOWEDUNITS[first_y_variable][0].upper() + print( + "{0: <8}:: Changing yunit from {1} to {2} for consistency".format( + "WARNING", old_yunit, args.yunit + ) + ) # Check if the user is trying to plot variables with inconsistent units # If so simply print a message to warn them - if len(set([allowedunits[i][0] for i in args.xvar.split(',')])) > 1: - print('{0: <8}:: Elements in xvar have inconsistent units, beware!'.format('WARNING')) + if len({ALLOWEDUNITS[i][0] for i in args.xvar.split(",")}) > 1: + print( + "{0: <8}:: Elements in xvar have inconsistent units, beware!".format( + "WARNING" + ) + ) - if len(set([allowedunits[i][0] for i in args.yvar.split(',')])) > 1: - print('{0: <8}:: Elements in yvar have inconsistent units, beware!'.format('WARNING')) + if len({ALLOWEDUNITS[i][0] for i in args.yvar.split(",")}) > 1: + print( + "{0: <8}:: Elements in yvar have inconsistent units, beware!".format( + "WARNING" + ) + ) # Labels and output information xlabel = args.xvar - ylabel = '' + ylabel = "" for carg in ylist: if ylabel: - ylabel += '_' + ylabel += "_" ylabel += carg.lower() if args.diff: - ylabel = 'diff_' + ylabel + ylabel = "diff_" + ylabel if not args.output: - output = 'PrMon_{}_vs_{}.{}'.format(xlabel,ylabel,args.otype) + output = "PrMon_{}_vs_{}.{}".format(xlabel, ylabel, args.otype) else: - output = '{}.{}'.format(args.output,args.otype) + output = "{}.{}".format(args.output, args.otype) # Calculate the multipliers xmultiplier = get_multiplier(xlabel, args.xunit) @@ -205,42 +275,48 @@ def get_multiplier(label, unit): # Here comes the figure and data extraction fig, ax1 = plt.subplots() - xdata = np.array(data[xlabel])*xmultiplier + xdata = np.array(data[xlabel]) * xmultiplier ydlist = [] for carg in ylist: if args.diff: - num = np.array(data[carg].diff())*ymultiplier - denom = np.array(data[xlabel].diff())*xmultiplier - ratio = np.where(denom != 0, num/denom, np.nan) + num = np.array(data[carg].diff()) * ymultiplier + denom = np.array(data[xlabel].diff()) * xmultiplier + ratio = np.where(denom != 0, num / denom, np.nan) ydlist.append(ratio) else: - ydlist.append(np.array(data[carg])*ymultiplier) + ydlist.append(np.array(data[carg]) * ymultiplier) if args.stacked: ydata = np.vstack(ydlist) - plt.stackplot(xdata, ydata, lw = 2, labels = [legendnames[val] for val in ylist], alpha = 0.6) + plt.stackplot( + xdata, ydata, lw=2, labels=[LEGENDNAMES[val] for val in ylist], alpha=0.6 + ) else: - for cidx,cdata in enumerate(ydlist): - plt.plot(xdata, cdata, lw = 2, label = legendnames[ylist[cidx]]) + for cidx, cdata in enumerate(ydlist): + plt.plot(xdata, cdata, lw=2, label=LEGENDNAMES[ylist[cidx]]) plt.legend(loc=0) - if 'Time' in xlabel: - formatter = mpl.dates.DateFormatter('%H:%M:%S') + if "Time" in xlabel: + formatter = mpl.dates.DateFormatter("%H:%M:%S") ax1.xaxis.set_major_formatter(formatter) fxlabel = get_axis_label(xlabel) - fxunit = args.xunit + fxunit = args.xunit if args.diff: - fylabel = get_axis_label(ylist[0],xlabel) + fylabel = get_axis_label(ylist[0], xlabel) if args.yunit == args.xunit: - fyunit = '1' + fyunit = "1" else: - fyunit = args.yunit+'/'+args.xunit + fyunit = args.yunit + "/" + args.xunit else: fylabel = get_axis_label(ylist[0]) - fyunit = args.yunit - plt.title('Plot of {} vs {}'.format(fxlabel, fylabel), y = 1.05) - plt.xlabel((fxlabel+' ['+fxunit+']') if fxunit != '1' else fxlabel) - plt.ylabel((fylabel+' ['+fyunit+']') if fyunit != '1' else fylabel) + fyunit = args.yunit + plt.title("Plot of {} vs {}".format(fxlabel, fylabel), y=1.05) + plt.xlabel((fxlabel + " [" + fxunit + "]") if fxunit != "1" else fxlabel) + plt.ylabel((fylabel + " [" + fyunit + "]") if fyunit != "1" else fylabel) plt.tight_layout() fig.savefig(output) - print('{0: <8}:: Saved output into {1}'.format('INFO', output)) + print("{0: <8}:: Saved output into {1}".format("INFO", output)) sys.exit(0) + + +if "__main__" in __name__: + main() diff --git a/package/tests/CMakeLists.txt b/package/tests/CMakeLists.txt index 16baa57..ae15c7e 100644 --- a/package/tests/CMakeLists.txt +++ b/package/tests/CMakeLists.txt @@ -31,14 +31,14 @@ function(script_install) ) endfunction(script_install) -script_install(SCRIPT testCPU.py) -script_install(SCRIPT testIO.py) -script_install(SCRIPT testNET.py) -script_install(SCRIPT testMEM.py) -script_install(SCRIPT netBurner.py) -script_install(SCRIPT httpBlock.py DESTINATION cgi-bin) -script_install(SCRIPT testCOUNT.py) -script_install(SCRIPT testEXIT.py) +script_install(SCRIPT test_cpu.py) +script_install(SCRIPT test_io.py) +script_install(SCRIPT test_net.py) +script_install(SCRIPT test_mem.py) +script_install(SCRIPT net_burner.py) +script_install(SCRIPT http_block.py DESTINATION cgi-bin) +script_install(SCRIPT test_count.py) +script_install(SCRIPT test_exit.py) # Setup the target version of Python we will use for testing set(PYTHON_TEST "python3" CACHE STRING "Python binary to use for tests") @@ -47,39 +47,39 @@ message(STATUS "Setting Python test binary to '${PYTHON_TEST}' (use -DPYTHON_TES # Python2 has a different version of the network test # as the modules are significantly different from Python3 if(${PYTHON_TEST} MATCHES "python2") - script_install(SCRIPT testNET2.py) - script_install(SCRIPT netBurner2.py) - script_install(SCRIPT httpBlock2.py DESTINATION cgi-bin) + script_install(SCRIPT test_net2.py) + script_install(SCRIPT net_burner2.py) + script_install(SCRIPT http_block2.py DESTINATION cgi-bin) endif() # CPU Tests -add_test(NAME testCPUsingle COMMAND ${PYTHON_TEST} testCPU.py --threads 1 --procs 1) -add_test(NAME testCPUmultithread COMMAND ${PYTHON_TEST} testCPU.py --threads 2 --procs 1) -add_test(NAME testCPUmultiproc COMMAND ${PYTHON_TEST} testCPU.py --threads 1 --procs 2 --child-fraction 0.5 --time 12) -add_test(NAME testCPUinvoke COMMAND ${PYTHON_TEST} testCPU.py --invoke) +add_test(NAME testCPUsingle COMMAND ${PYTHON_TEST} test_cpu.py --threads 1 --procs 1) +add_test(NAME testCPUmultithread COMMAND ${PYTHON_TEST} test_cpu.py --threads 2 --procs 1) +add_test(NAME testCPUmultiproc COMMAND ${PYTHON_TEST} test_cpu.py --threads 1 --procs 2 --child-fraction 0.5 --time 12) +add_test(NAME testCPUinvoke COMMAND ${PYTHON_TEST} test_cpu.py --invoke) # IO Tests -add_test(NAME basicIOsingle COMMAND ${PYTHON_TEST} testIO.py --usleep 100 --io 10) -add_test(NAME basicIOmultithread COMMAND ${PYTHON_TEST} testIO.py --usleep 100 --io 10 --threads 2) -add_test(NAME basicIOmultiproc COMMAND ${PYTHON_TEST} testIO.py --usleep 100 --io 10 --procs 2) +add_test(NAME basicIOsingle COMMAND ${PYTHON_TEST} test_io.py --usleep 100 --io 10) +add_test(NAME basicIOmultithread COMMAND ${PYTHON_TEST} test_io.py --usleep 100 --io 10 --threads 2) +add_test(NAME basicIOmultiproc COMMAND ${PYTHON_TEST} test_io.py --usleep 100 --io 10 --procs 2) # Net Tests # These have to be different for python2 and python3 if(${PYTHON_TEST} MATCHES "python3") - add_test(NAME basicNET COMMAND ${PYTHON_TEST} testNET.py) + add_test(NAME basicNET COMMAND ${PYTHON_TEST} test_net.py) elseif(${PYTHON_TEST} MATCHES "python2") - add_test(NAME basicNET COMMAND ${PYTHON_TEST} testNET2.py) + add_test(NAME basicNET COMMAND ${PYTHON_TEST} test_net2.py) endif() # Memory Tests -add_test(NAME singleMem COMMAND ${PYTHON_TEST} testMEM.py --procs 1) -add_test(NAME childMem COMMAND ${PYTHON_TEST} testMEM.py --procs 4) +add_test(NAME singleMem COMMAND ${PYTHON_TEST} test_mem.py --procs 1) +add_test(NAME childMem COMMAND ${PYTHON_TEST} test_mem.py --procs 4) # Process and thread counting Tests -add_test(NAME basicCOUNT COMMAND ${PYTHON_TEST} testCOUNT.py --procs 2 --threads 2) +add_test(NAME basicCOUNT COMMAND ${PYTHON_TEST} test_count.py --procs 2 --threads 2) # Units check test -add_test(NAME testUnits COMMAND ${PYTHON_TEST} testCPU.py --units --time 3 --slack 0 --interval 1) +add_test(NAME testUnits COMMAND ${PYTHON_TEST} test_cpu.py --units --time 3 --slack 0 --interval 1) # Test passing the child exit code works -add_test(NAME testExitCode COMMAND ${PYTHON_TEST} testEXIT.py --exit-code 43) +add_test(NAME testExitCode COMMAND ${PYTHON_TEST} test_exit.py --exit-code 43) diff --git a/package/tests/httpBlock.py b/package/tests/httpBlock.py deleted file mode 100755 index 2d68176..0000000 --- a/package/tests/httpBlock.py +++ /dev/null @@ -1,35 +0,0 @@ -#! /usr/bin/env python3 -# -# Copyright (C) 2018-2020 CERN -# License Apache2 - see LICENCE file -# -# Simple CGI script that delivers a known block -# of data to the caller over HTTP -# -# One GET/POST parameter is recognised, which is "blocks" -# that specifies how many 1KB blocks are returned to the -# client (defaults to 1000, thus ~1MB delivered) - -import cgi -import cgitb -import sys - -cgitb.enable() - -print("Content-Type: text/plain\n") - -form = cgi.FieldStorage() -blocks = form.getfirst("blocks", default="1000") -try: - blocks = int(blocks) -except ValueError: - print("Invalid block value") - sys.exit(1) - -myString = "somehow the world seems more curious than when i was a child xx\n" -myBlock = myString * 16 # 1KB - -for i in range(blocks): - print(myBlock) - -sys.exit(0) diff --git a/package/tests/httpBlock2.py b/package/tests/httpBlock2.py deleted file mode 100755 index be4c25b..0000000 --- a/package/tests/httpBlock2.py +++ /dev/null @@ -1,35 +0,0 @@ -#! /usr/bin/env python2 -# -# Copyright (C) 2018-2020 CERN -# License Apache2 - see LICENCE file -# -# Simple CGI script that delivers a known block -# of data to the caller over HTTP -# -# One GET/POST parameter is recognised, which is "blocks" -# that specifies how many 1KB blocks are returned to the -# client (defaults to 1000, thus ~1MB delivered) - -import cgi -import cgitb -import sys - -cgitb.enable() - -print("Content-Type: text/plain\n") - -form = cgi.FieldStorage() -blocks = form.getfirst("blocks", default="1000") -try: - blocks = int(blocks) -except ValueError: - print("Invalid block value") - sys.exit(1) - -myString = "somehow the world seems more curious than when i was a child xx\n" -myBlock = myString * 16 # 1KB - -for i in range(blocks): - print(myBlock) - -sys.exit(0) diff --git a/package/tests/http_block.py b/package/tests/http_block.py new file mode 100755 index 0000000..df19cb5 --- /dev/null +++ b/package/tests/http_block.py @@ -0,0 +1,40 @@ +#! /usr/bin/env python3 +# +# Copyright (C) 2018-2020 CERN +# License Apache2 - see LICENCE file + +"""Simple CGI script that delivers a known block + of data to the caller over HTTP + + One GET/POST parameter is recognised, which is "blocks" + that specifies how many 1KB blocks are returned to the + client (defaults to 1000, thus ~1MB delivered)""" + +import cgi +import cgitb +import sys + + +def main(): + """main function""" + cgitb.enable() + + print("Content-Type: text/plain\n") + + form = cgi.FieldStorage() + blocks = form.getfirst("blocks", default="1000") + try: + blocks = int(blocks) + except ValueError: + print("Invalid block value") + sys.exit(1) + + my_string = "somehow the world seems more curious than when i was a child xx\n" + my_block = my_string * 16 # 1KB + + for _ in range(blocks): + print(my_block) + + +if "__main__" in __name__: + main() diff --git a/package/tests/http_block2.py b/package/tests/http_block2.py new file mode 100755 index 0000000..edc6ca4 --- /dev/null +++ b/package/tests/http_block2.py @@ -0,0 +1,40 @@ +#! /usr/bin/env python2 +# +# Copyright (C) 2018-2020 CERN +# License Apache2 - see LICENCE file + +"""Simple CGI script that delivers a known block + of data to the caller over HTTP + + One GET/POST parameter is recognised, which is "blocks" + that specifies how many 1KB blocks are returned to the + client (defaults to 1000, thus ~1MB delivered)""" + +import cgi +import cgitb +import sys + + +def main(): + """main function""" + cgitb.enable() + + print("Content-Type: text/plain\n") + + form = cgi.FieldStorage() + blocks = form.getfirst("blocks", default="1000") + try: + blocks = int(blocks) + except ValueError: + print("Invalid block value") + sys.exit(1) + + my_string = "somehow the world seems more curious than when i was a child xx\n" + my_block = my_string * 16 # 1KB + + for _ in range(blocks): + print(my_block) + + +if "__main__" in __name__: + main() diff --git a/package/tests/netBurner.py b/package/tests/netBurner.py deleted file mode 100755 index b508478..0000000 --- a/package/tests/netBurner.py +++ /dev/null @@ -1,39 +0,0 @@ -#! /usr/bin/env python3 -# -# Copyright (C) 2018-2020 CERN -# License Apache2 - see LICENCE file -# -# Request network data for prmon unittest - -import argparse -import time -import urllib.request - -def getNetData(host="localhost", port="8000", blocks=None): - url = "http://" + host + ":" + str(port) + "/cgi-bin/httpBlock.py" - if blocks: - url += "?blocks=" + str(blocks) - response = urllib.request.urlopen(url) - html = response.read() - print("Read {0} bytes".format(len(html))) - return len(html) - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="Network data burner") - parser.add_argument('--port', type=int, default=8000) - parser.add_argument('--host', default="localhost") - parser.add_argument('--blocks', type=int) - parser.add_argument('--requests', type=int, default=10) - parser.add_argument('--sleep', type=float, default=0.1) - parser.add_argument('--pause', type=float, default=3) - args = parser.parse_args() - - time.sleep(args.pause) - - readBytes = 0 - for req in range(args.requests): - time.sleep(args.sleep) - readBytes += getNetData(args.host, args.port, args.blocks) - - print("Read total of {0} bytes in {1} requests".format(readBytes, args.requests)) - time.sleep(args.pause) diff --git a/package/tests/netBurner2.py b/package/tests/netBurner2.py deleted file mode 100755 index 54ab4ff..0000000 --- a/package/tests/netBurner2.py +++ /dev/null @@ -1,40 +0,0 @@ -#! /usr/bin/env python2 -# -# Copyright (C) 2018-2020 CERN -# License Apache2 - see LICENCE file -# -# Request network data for prmon unittest (Python2 version) -from __future__ import print_function, unicode_literals - -import argparse -import time -import urllib2 - -def getNetData(host="localhost", port="8000", blocks=None): - url = "http://" + host + ":" + str(port) + "/cgi-bin/httpBlock2.py" - if blocks: - url += "?blocks=" + str(blocks) - response = urllib2.urlopen(url) - html = response.read() - print("Read {0} bytes".format(len(html))) - return len(html) - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="Network data burner") - parser.add_argument('--port', type=int, default=8000) - parser.add_argument('--host', default="localhost") - parser.add_argument('--blocks', type=int) - parser.add_argument('--requests', type=int, default=10) - parser.add_argument('--sleep', type=float, default=0.1) - parser.add_argument('--pause', type=float, default=3) - args = parser.parse_args() - - time.sleep(args.pause) - - readBytes = 0 - for req in range(args.requests): - time.sleep(args.sleep) - readBytes += getNetData(args.host, args.port, args.blocks) - - print("Read total of {0} bytes in {1} requests".format(readBytes, args.requests)) - time.sleep(args.pause) diff --git a/package/tests/net_burner.py b/package/tests/net_burner.py new file mode 100755 index 0000000..5afcf09 --- /dev/null +++ b/package/tests/net_burner.py @@ -0,0 +1,49 @@ +#! /usr/bin/env python3 +# +# Copyright (C) 2018-2020 CERN +# License Apache2 - see LICENCE file + +"""Request network data for prmon unittest""" + +import argparse +import time +import urllib.request + + +def get_net_data(host="localhost", port="8000", blocks=None): + """read network data from local http server""" + url = "http://" + host + ":" + str(port) + "/cgi-bin/http_block.py" + if blocks: + url += "?blocks=" + str(blocks) + # This URL is fine as it's from the server we setup + response = urllib.request.urlopen(url) # nosec + html = response.read() + print("Read {0} bytes".format(len(html))) + return len(html) + + +def main(): + """parse arguments and read data""" + parser = argparse.ArgumentParser(description="Network data burner") + parser.add_argument("--port", type=int, default=8000) + parser.add_argument("--host", default="localhost") + parser.add_argument("--blocks", type=int) + parser.add_argument("--requests", type=int, default=10) + parser.add_argument("--sleep", type=float, default=0.1) + parser.add_argument("--pause", type=float, default=3) + args = parser.parse_args() + + time.sleep(args.pause) + + read_bytes = 0 + for _ in range(args.requests): + time.sleep(args.sleep) + read_bytes += get_net_data(args.host, args.port, args.blocks) + + print("Read total of {0} bytes in {1} requests".format(read_bytes, args.requests)) + + time.sleep(args.pause) + + +if __name__ == "__main__": + main() diff --git a/package/tests/net_burner2.py b/package/tests/net_burner2.py new file mode 100755 index 0000000..7b7f52e --- /dev/null +++ b/package/tests/net_burner2.py @@ -0,0 +1,50 @@ +#! /usr/bin/env python2 +# +# Copyright (C) 2018-2020 CERN +# License Apache2 - see LICENCE file + +"""Request network data for prmon unittest (Python2 version)""" +from __future__ import print_function, unicode_literals + +import argparse +import time +import urllib2 + + +def get_net_data(host="localhost", port="8000", blocks=None): + """read network data from local http server""" + url = "http://" + host + ":" + str(port) + "/cgi-bin/http_block2.py" + if blocks: + url += "?blocks=" + str(blocks) + # This URL is fine as it's from the server we setup + response = urllib2.urlopen(url) # nosec + html = response.read() + print("Read {0} bytes".format(len(html))) + return len(html) + + +def main(): + """parse arguments and read data""" + parser = argparse.ArgumentParser(description="Network data burner") + parser.add_argument("--port", type=int, default=8000) + parser.add_argument("--host", default="localhost") + parser.add_argument("--blocks", type=int) + parser.add_argument("--requests", type=int, default=10) + parser.add_argument("--sleep", type=float, default=0.1) + parser.add_argument("--pause", type=float, default=3) + args = parser.parse_args() + + time.sleep(args.pause) + + read_bytes = 0 + for _ in range(args.requests): + time.sleep(args.sleep) + read_bytes += get_net_data(args.host, args.port, args.blocks) + + print("Read total of {0} bytes in {1} requests".format(read_bytes, args.requests)) + + time.sleep(args.pause) + + +if __name__ == "__main__": + main() diff --git a/package/tests/testCOUNT.py b/package/tests/testCOUNT.py deleted file mode 100755 index 1eaa30d..0000000 --- a/package/tests/testCOUNT.py +++ /dev/null @@ -1,71 +0,0 @@ -#! /usr/bin/env python3 -# -# Copyright (C) 2018-2020 CERN -# License Apache2 - see LICENCE file - -import argparse -import json -import os -import subprocess -import sys -import unittest - -def setupConfigurableTest(threads=1, procs=1, time=10.0, interval=1, invoke=False): - '''Wrap the class definition in a function to allow arguments to be passed''' - class configurableProcessMonitor(unittest.TestCase): - def test_runTestWithParams(self): - burn_cmd = ['./burner', '--time', str(time)] - if threads != 1: - burn_cmd.extend(['--threads', str(threads)]) - if procs != 1: - burn_cmd.extend(['--procs', str(procs)]) - - prmon_cmd = ['../prmon', '--interval', str(interval)] - if invoke: - prmon_cmd.append('--') - prmon_cmd.extend(burn_cmd) - prmon_p = subprocess.Popen(prmon_cmd, shell = False) - - prmon_rc = prmon_p.wait() - else: - burn_p = subprocess.Popen(burn_cmd, shell = False) - - prmon_cmd.extend(['--pid', str(burn_p.pid)]) - prmon_p = subprocess.Popen(prmon_cmd, shell = False) - - burn_rc = burn_p.wait() - prmon_rc = prmon_p.wait() - - self.assertEqual(prmon_rc, 0, "Non-zero return code from prmon") - - with open("prmon.json") as infile: - prmonJSON = json.load(infile) - - # Simple Process and Thread counting test - totPROC = prmonJSON["Max"]["nprocs"] - totTHREAD = prmonJSON["Max"]["nthreads"] - expectPROC = procs - expectTHREAD = procs*threads - self.assertAlmostEqual(totPROC, expectPROC, msg = "Inconsistent value for number of processes " - "(expected {0}, got {1})".format(expectPROC, totPROC)) - self.assertAlmostEqual(totTHREAD, expectTHREAD, msg = "Inconsistent value for number of total threads " - "(expected {0}, got {1}".format(expectTHREAD, totTHREAD)) - - return configurableProcessMonitor - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="Configurable test runner") - parser.add_argument('--threads', type=int, default=1) - parser.add_argument('--procs', type=int, default=1) - parser.add_argument('--time', type=float, default=10) - parser.add_argument('--interval', type=int, default=1) - parser.add_argument('--invoke', dest='invoke', action='store_true', default=False) - - args = parser.parse_args() - # Stop unittest from being confused by the arguments - sys.argv=sys.argv[:1] - - cpm = setupConfigurableTest(args.threads,args.procs,args.time,args.interval,args.invoke) - - unittest.main() diff --git a/package/tests/testCPU.py b/package/tests/testCPU.py deleted file mode 100755 index a3b9251..0000000 --- a/package/tests/testCPU.py +++ /dev/null @@ -1,96 +0,0 @@ -#! /usr/bin/env python3 -# -# Copyright (C) 2018-2020 CERN -# License Apache2 - see LICENCE file - -import argparse -import json -import os -import subprocess -import sys -import unittest - -def setupConfigurableTest(threads=1, procs=1, child_fraction=1.0, time=10.0, - slack=0.75, interval=1, invoke=False, units=False): - '''Wrap the class definition in a function to allow arguments to be passed''' - class configurableProcessMonitor(unittest.TestCase): - def test_runTestWithParams(self): - burn_cmd = ['./burner', '--time', str(time)] - if threads != 1: - burn_cmd.extend(['--threads', str(threads)]) - if procs != 1: - burn_cmd.extend(['--procs', str(procs)]) - if child_fraction != 1.0: - burn_cmd.extend(['--child-fraction', str(child_fraction)]) - - prmon_cmd = ['../prmon', '--interval', str(interval)] - if units: - prmon_cmd.append('--units') - if invoke: - prmon_cmd.append('--') - prmon_cmd.extend(burn_cmd) - prmon_p = subprocess.Popen(prmon_cmd, shell = False) - - prmon_rc = prmon_p.wait() - else: - burn_p = subprocess.Popen(burn_cmd, shell = False) - - prmon_cmd.extend(['--pid', str(burn_p.pid)]) - prmon_p = subprocess.Popen(prmon_cmd, shell = False) - - burn_rc = burn_p.wait() - prmon_rc = prmon_p.wait() - - self.assertEqual(prmon_rc, 0, "Non-zero return code from prmon") - - with open("prmon.json") as infile: - prmonJSON = json.load(infile) - - # CPU time tests - totCPU = prmonJSON["Max"]["utime"] + prmonJSON["Max"]["stime"] - expectCPU = (1.0 + (procs-1)*child_fraction) * time * threads - self.assertLessEqual(totCPU, expectCPU, "Too high value for CPU time " - "(expected maximum of {0}, got {1})".format(expectCPU, totCPU)) - self.assertGreaterEqual(totCPU, expectCPU*slack, "Too low value for CPU time " - "(expected minimum of {0}, got {1}".format(expectCPU*slack, totCPU)) - # Wall time tests - totWALL = prmonJSON["Max"]["wtime"] - self.assertLessEqual(totWALL, time, "Too high value for wall time " - "(expected maximum of {0}, got {1})".format(time, totWALL)) - self.assertGreaterEqual(totWALL, time*slack, "Too low value for wall time " - "(expected minimum of {0}, got {1}".format(time*slack, totWALL)) - - # Unit test - if units: - for group in ("Max", "Avg"): - value_params = set(prmonJSON[group].keys()) - unit_params = set(prmonJSON["Units"][group].keys()) - missing = value_params - unit_params - self.assertEqual(len(missing), 0, - "Wrong number of unit values for '{0}' - missing parameters are {1}".format(group, missing)) - extras = unit_params - value_params - self.assertEqual(len(extras), 0, - "Wrong number of unit values for '{0}' - extra parameters are {1}".format(group, extras)) - - return configurableProcessMonitor - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="Configurable test runner") - parser.add_argument('--threads', type=int, default=1) - parser.add_argument('--procs', type=int, default=1) - parser.add_argument('--child-fraction', type=float, default=1.0) - parser.add_argument('--time', type=float, default=10) - parser.add_argument('--slack', type=float, default=0.7) - parser.add_argument('--interval', type=int, default=1) - parser.add_argument('--invoke', action='store_true') - parser.add_argument('--units', action='store_true') - - args = parser.parse_args() - # Stop unittest from being confused by the arguments - sys.argv=sys.argv[:1] - - cpm = setupConfigurableTest(args.threads,args.procs,args.child_fraction,args.time, - args.slack,args.interval,args.invoke,args.units) - - unittest.main() diff --git a/package/tests/testEXIT.py b/package/tests/testEXIT.py deleted file mode 100755 index b0cd1e0..0000000 --- a/package/tests/testEXIT.py +++ /dev/null @@ -1,38 +0,0 @@ -#! /usr/bin/env python3 -# -# Copyright (C) 2018-2020 CERN -# License Apache2 - see LICENCE file - -import argparse -import subprocess -import sys -import unittest - -def setupConfigurableTest(exit_code = 0): - '''Wrap the class definition in a function to allow arguments to be passed''' - class configurableProcessMonitor(unittest.TestCase): - def test_runTestWithParams(self): - child_cmd = ['sh', '-c', 'sleep 3 && exit {0}'.format(exit_code)] - - prmon_cmd = ['../prmon', '--interval', '1'] - prmon_cmd.append('--') - prmon_cmd.extend(child_cmd) - prmon_p = subprocess.Popen(prmon_cmd, shell = False) - prmon_rc = prmon_p.wait() - - self.assertEqual(prmon_rc, exit_code, "Wrong return code from prmon (expected {0}".format(exit_code)) - - return configurableProcessMonitor - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="Configurable test runner") - parser.add_argument('--exit-code', type=int, default=0) - - args = parser.parse_args() - # Stop unittest from being confused by the arguments - sys.argv=sys.argv[:1] - - cpm = setupConfigurableTest(args.exit_code) - - unittest.main() diff --git a/package/tests/testIO.py b/package/tests/testIO.py deleted file mode 100755 index cc851f2..0000000 --- a/package/tests/testIO.py +++ /dev/null @@ -1,60 +0,0 @@ -#! /usr/bin/env python3 -# -# Copyright (C) 2018-2020 CERN -# License Apache2 - see LICENCE file - -import argparse -import json -import os -import subprocess -import sys -import unittest - -def setupConfigurableTest(io=10, threads=1, procs=1, usleep=10, pause=1, slack=0.95, interval=1): - '''Wrap the class definition in a function to allow arguments to be passed''' - class configurableProcessMonitor(unittest.TestCase): - def test_runTestWithParams(self): - burn_cmd = ['./io-burner', '--io', str(io)] - burn_cmd.extend(['--threads', str(threads)]) - burn_cmd.extend(['--procs', str(procs)]) - burn_cmd.extend(['--usleep', str(usleep)]) - burn_cmd.extend(['--pause', str(pause)]) - burn_p = subprocess.Popen(burn_cmd, shell = False) - - prmon_cmd = ['../prmon', '--interval', str(interval), '--pid', str(burn_p.pid)] - prmon_p = subprocess.Popen(prmon_cmd, shell = False) - - burn_rc = burn_p.wait() - prmon_rc = prmon_p.wait() - - self.assertEqual(prmon_rc, 0, "Non-zero return code from prmon") - - with open("prmon.json") as infile: - prmonJSON = json.load(infile) - - # IO tests - expectedBytes = io*threads*procs*slack * 1.0e6 - self.assertGreaterEqual(prmonJSON["Max"]["wchar"], expectedBytes, "Too low value for IO bytes written " - "(expected minimum of {0}, got {1})".format(expectedBytes, prmonJSON["Max"]["wchar"])) - self.assertGreaterEqual(prmonJSON["Max"]["rchar"], expectedBytes, "Too low value for IO bytes read " - "(expected minimum of {0}, got {1})".format(expectedBytes, prmonJSON["Max"]["rchar"])) - - return configurableProcessMonitor - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="Configurable test runner") - parser.add_argument('--threads', type=int, default=1) - parser.add_argument('--procs', type=int, default=1) - parser.add_argument('--io', type=int, default=10) - parser.add_argument('--usleep', type=int, default=10) - parser.add_argument('--pause', type=float, default=1) - parser.add_argument('--slack', type=float, default=0.9) - parser.add_argument('--interval', type=int, default=1) - args = parser.parse_args() - # Stop unittest from being confused by the arguments - sys.argv=sys.argv[:1] - - cpm = setupConfigurableTest(args.io, args.threads, args.procs, args.usleep, args.pause, args.slack, args.interval) - - unittest.main() diff --git a/package/tests/testMEM.py b/package/tests/testMEM.py deleted file mode 100755 index b9ffc37..0000000 --- a/package/tests/testMEM.py +++ /dev/null @@ -1,70 +0,0 @@ -#! /usr/bin/env python3 -# -# Copyright (C) 2018-2020 CERN -# License Apache2 - see LICENCE file - -import argparse -import json -import os -import subprocess -import sys -import unittest - - -def setupConfigurableTest(procs=4, malloc_mb = 100, write_fraction=0.5, sleep=10, slack=0.9, interval=1): - '''Wrap the class definition in a function to allow arguments to be passed''' - class configurableProcessMonitor(unittest.TestCase): - def checkMemoryLimits(self, name, value, expected, slack): - max_value = expected * (1.0 + (1.0-slack)) - min_value = expected * slack - self.assertLess(value, max_value, "Too high a value for {0} " - "(expected maximum of {1}, got {2})".format(name, max_value, value)) - self.assertGreater(value, min_value, "Too low a value for {0} " - "(expected maximum of {1}, got {2})".format(name, min_value, value)) - - def test_runTestWithParams(self): - burn_cmd = ['./mem-burner', '--sleep', str(sleep), '--malloc', str(malloc_mb), '--writef', str(write_fraction)] - if procs != 1: - burn_cmd.extend(['--procs', str(procs)]) - - prmon_cmd = ['../prmon', '--interval', str(interval), '--'] - prmon_cmd.extend(burn_cmd) - prmon_p = subprocess.Popen(prmon_cmd, shell = False) - - prmon_rc = prmon_p.wait() - - self.assertEqual(prmon_rc, 0, "Non-zero return code from prmon") - - with open("prmon.json") as infile: - prmonJSON = json.load(infile) - - # Memory tests - vmemExpect = malloc_mb * procs * 1024 + 10000 * procs # Small uplift for program itself - self.checkMemoryLimits("vmem", prmonJSON["Max"]["vmem"], vmemExpect, slack) - - rssExpect = malloc_mb * procs * 1024 * write_fraction - self.checkMemoryLimits("rss", prmonJSON["Max"]["rss"], rssExpect, slack) - - pssExpect = malloc_mb * 1024 * write_fraction - self.checkMemoryLimits("pss", prmonJSON["Max"]["pss"], pssExpect, slack) - - - return configurableProcessMonitor - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="Configurable memory test runner") - parser.add_argument('--procs', type=int, default=4) - parser.add_argument('--malloc', type=int, default=100) - parser.add_argument('--writef', type=float, default=0.5) - parser.add_argument('--sleep', type=int, default=10) - parser.add_argument('--slack', type=float, default=0.85) - parser.add_argument('--interval', type=int, default=1) - - args = parser.parse_args() - # Stop unittest from being confused by the arguments - sys.argv=sys.argv[:1] - - cpm = setupConfigurableTest(args.procs,args.malloc,args.writef,args.sleep,args.slack,args.interval) - - unittest.main() diff --git a/package/tests/testNET.py b/package/tests/testNET.py deleted file mode 100755 index ecb0737..0000000 --- a/package/tests/testNET.py +++ /dev/null @@ -1,73 +0,0 @@ -#! /usr/bin/env python3 -# -# Copyright (C) 2018-2020 CERN -# License Apache2 - see LICENCE file -# -# Test script for network IO - -import argparse -import json -import os -import signal -import subprocess -import sys -import unittest - -def setupConfigurableTest(blocks=None, requests=10, sleep=None, pause=None, slack=0.95, interval=1): - '''Wrap the class definition in a function to allow arguments to be passed''' - class configurableProcessMonitor(unittest.TestCase): - def setUp(self): - # Start the simple python HTTP CGI server - httpServerCmd = ['python3', '-m', 'http.server', '--cgi'] - self.httpServer = subprocess.Popen(httpServerCmd, shell = False) - - def tearDown(self): - os.kill(self.httpServer.pid, signal.SIGTERM) - - def test_runTestWithParams(self): - burn_cmd = ['python3', './netBurner.py'] - if (requests): - burn_cmd.extend(['--requests', str(requests)]) - if (pause): - burn_cmd.extend(['--pause', str(pause)]) - if (sleep): - burn_cmd.extend(['--sleep', str(sleep)]) - if blocks: - burn_cmd.extend(['--blocks', str(blocks)]) - burn_p = subprocess.Popen(burn_cmd, shell=False) - prmon_cmd = ['../prmon', '--interval', str(interval), '--pid', str(burn_p.pid)] - prmon_p = subprocess.Popen(prmon_cmd, shell = False) - - burn_rc = burn_p.wait() - prmon_rc = prmon_p.wait() - - self.assertEqual(prmon_rc, 0, "Non-zero return code from prmon") - - with open("prmon.json") as infile: - prmonJSON = json.load(infile) - - # Network tests - expectedBytes = 1025000 * requests * slack - self.assertGreaterEqual(prmonJSON["Max"]["rx_bytes"], expectedBytes, "Too low value for rx bytes " - "(expected minimum of {0}, got {1})".format(expectedBytes, prmonJSON["Max"]["rx_bytes"])) - self.assertGreaterEqual(prmonJSON["Max"]["tx_bytes"], expectedBytes, "Too low value for tx bytes " - "(expected minimum of {0}, got {1})".format(expectedBytes, prmonJSON["Max"]["tx_bytes"])) - - return configurableProcessMonitor - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="Configurable test runner for network access") - parser.add_argument('--blocks', type=int) - parser.add_argument('--requests', type=int, default=10) - parser.add_argument('--sleep', type=float) - parser.add_argument('--pause', type=float) - parser.add_argument('--slack', type=float, default=0.8) - parser.add_argument('--interval', type=int, default=1) - args = parser.parse_args() - # Stop unittest from being confused by the arguments - sys.argv=sys.argv[:1] - - cpm = setupConfigurableTest(args.blocks, args.requests, args.sleep, args.pause, args.slack, args.interval) - - unittest.main() diff --git a/package/tests/testNET2.py b/package/tests/testNET2.py deleted file mode 100755 index 4c75124..0000000 --- a/package/tests/testNET2.py +++ /dev/null @@ -1,74 +0,0 @@ -#! /usr/bin/env python2 -# -# Copyright (C) 2018-2020 CERN -# License Apache2 - see LICENCE file -# -# Test script for network IO (Python2 version) -from __future__ import print_function, unicode_literals - -import argparse -import json -import os -import signal -import subprocess -import sys -import unittest - -def setupConfigurableTest(blocks=None, requests=10, sleep=None, pause=None, slack=0.95, interval=1): - '''Wrap the class definition in a function to allow arguments to be passed''' - class configurableProcessMonitor(unittest.TestCase): - def setUp(self): - # Start the simple python HTTP CGI server - httpServerCmd = ['python', '-m', 'CGIHTTPServer'] - self.httpServer = subprocess.Popen(httpServerCmd, shell = False) - - def tearDown(self): - os.kill(self.httpServer.pid, signal.SIGTERM) - - def test_runTestWithParams(self): - burn_cmd = ['python', './netBurner2.py'] - if (requests): - burn_cmd.extend(['--requests', str(requests)]) - if (pause): - burn_cmd.extend(['--pause', str(pause)]) - if (sleep): - burn_cmd.extend(['--sleep', str(sleep)]) - if blocks: - burn_cmd.extend(['--blocks', str(blocks)]) - burn_p = subprocess.Popen(burn_cmd, shell=False) - prmon_cmd = ['../prmon', '--interval', str(interval), '--pid', str(burn_p.pid)] - prmon_p = subprocess.Popen(prmon_cmd, shell = False) - - burn_rc = burn_p.wait() - prmon_rc = prmon_p.wait() - - self.assertEqual(prmon_rc, 0, "Non-zero return code from prmon") - - with open("prmon.json") as infile: - prmonJSON = json.load(infile) - - # Network tests - expectedBytes = 1025000 * requests * slack - self.assertGreaterEqual(prmonJSON["Max"]["rx_bytes"], expectedBytes, "Too low value for rx bytes " - "(expected minimum of {0}, got {1})".format(expectedBytes, prmonJSON["Max"]["rx_bytes"])) - self.assertGreaterEqual(prmonJSON["Max"]["tx_bytes"], expectedBytes, "Too low value for tx bytes " - "(expected minimum of {0}, got {1})".format(expectedBytes, prmonJSON["Max"]["tx_bytes"])) - - return configurableProcessMonitor - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="Configurable test runner for network access") - parser.add_argument('--blocks', type=int) - parser.add_argument('--requests', type=int, default=10) - parser.add_argument('--sleep', type=float) - parser.add_argument('--pause', type=float) - parser.add_argument('--slack', type=float, default=0.90) - parser.add_argument('--interval', type=int, default=1) - args = parser.parse_args() - # Stop unittest from being confused by the arguments - sys.argv=sys.argv[:1] - - cpm = setupConfigurableTest(args.blocks, args.requests, args.sleep, args.pause, args.slack, args.interval) - - unittest.main() diff --git a/package/tests/test_count.py b/package/tests/test_count.py new file mode 100755 index 0000000..12c43cb --- /dev/null +++ b/package/tests/test_count.py @@ -0,0 +1,95 @@ +#! /usr/bin/env python3 +# +# Copyright (C) 2018-2020 CERN +# License Apache2 - see LICENCE file + +"""prmon test harness to count number of processes""" + +import argparse +import json +import subprocess +import sys +import unittest + + +def setup_configurable_test(threads=1, procs=1, time=10.0, interval=1, invoke=False): + """Wrap the class definition in a function to allow arguments to be passed""" + + class ConfigurableProcessMonitor(unittest.TestCase): + """Test class for a specific set of parameters""" + + def test_run_test_with_params(self): + """Actual test runner""" + burn_cmd = ["./burner", "--time", str(time)] + if threads != 1: + burn_cmd.extend(["--threads", str(threads)]) + if procs != 1: + burn_cmd.extend(["--procs", str(procs)]) + + prmon_cmd = ["../prmon", "--interval", str(interval)] + if invoke: + prmon_cmd.append("--") + prmon_cmd.extend(burn_cmd) + prmon_p = subprocess.Popen(prmon_cmd, shell=False) + + prmon_rc = prmon_p.wait() + else: + burn_p = subprocess.Popen(burn_cmd, shell=False) + + prmon_cmd.extend(["--pid", str(burn_p.pid)]) + prmon_p = subprocess.Popen(prmon_cmd, shell=False) + + _ = burn_p.wait() + prmon_rc = prmon_p.wait() + + self.assertEqual(prmon_rc, 0, "Non-zero return code from prmon") + + with open("prmon.json") as infile: + prmon_json = json.load(infile) + + # Simple Process and Thread counting test + total_proc = prmon_json["Max"]["nprocs"] + total_thread = prmon_json["Max"]["nthreads"] + expect_proc = procs + expect_thread = procs * threads + self.assertAlmostEqual( + total_proc, + expect_proc, + msg="Inconsistent value for number of processes " + "(expected {0}, got {1})".format(expect_proc, total_proc), + ) + self.assertAlmostEqual( + total_thread, + expect_thread, + msg="Inconsistent value for number of total threads " + "(expected {0}, got {1}".format(expect_thread, total_thread), + ) + + return ConfigurableProcessMonitor + + +def main_parse_args_and_get_test(): + """Parse arguments and call test class generator + returning the test case (which is unusual for a + main() function)""" + parser = argparse.ArgumentParser(description="Configurable test runner") + parser.add_argument("--threads", type=int, default=1) + parser.add_argument("--procs", type=int, default=1) + parser.add_argument("--time", type=float, default=10) + parser.add_argument("--interval", type=int, default=1) + parser.add_argument("--invoke", dest="invoke", action="store_true", default=False) + + args = parser.parse_args() + # Stop unittest from being confused by the arguments + sys.argv = sys.argv[:1] + + return setup_configurable_test( + args.threads, args.procs, args.time, args.interval, args.invoke + ) + + +if __name__ == "__main__": + # As unitest will only run tests in the global namespace + # we return the test instance from main() + the_test = main_parse_args_and_get_test() + unittest.main() diff --git a/package/tests/test_cpu.py b/package/tests/test_cpu.py new file mode 100755 index 0000000..5581664 --- /dev/null +++ b/package/tests/test_cpu.py @@ -0,0 +1,154 @@ +#! /usr/bin/env python3 +# +# Copyright (C) 2018-2020 CERN +# License Apache2 - see LICENCE file + +"""Test harness for process and thread burning tests""" + +import argparse +import json +import subprocess +import sys +import unittest + + +def setup_configurable_test( + threads=1, + procs=1, + child_fraction=1.0, + time=10.0, + slack=0.75, + interval=1, + invoke=False, + units=False, +): + """Wrap the class definition in a function to allow arguments to be passed""" + + class ConfigurableProcessMonitor(unittest.TestCase): + """Test class for a specific set of parameters""" + + def test_run_test_with_params(self): + """Actual test runner""" + burn_cmd = ["./burner", "--time", str(time)] + if threads != 1: + burn_cmd.extend(["--threads", str(threads)]) + if procs != 1: + burn_cmd.extend(["--procs", str(procs)]) + if child_fraction != 1.0: + burn_cmd.extend(["--child-fraction", str(child_fraction)]) + + prmon_cmd = ["../prmon", "--interval", str(interval)] + if units: + prmon_cmd.append("--units") + if invoke: + prmon_cmd.append("--") + prmon_cmd.extend(burn_cmd) + prmon_p = subprocess.Popen(prmon_cmd, shell=False) + + prmon_rc = prmon_p.wait() + else: + burn_p = subprocess.Popen(burn_cmd, shell=False) + + prmon_cmd.extend(["--pid", str(burn_p.pid)]) + prmon_p = subprocess.Popen(prmon_cmd, shell=False) + + _ = burn_p.wait() + prmon_rc = prmon_p.wait() + + self.assertEqual(prmon_rc, 0, "Non-zero return code from prmon") + + with open("prmon.json") as infile: + prmon_json = json.load(infile) + + # CPU time tests + total_cpu = prmon_json["Max"]["utime"] + prmon_json["Max"]["stime"] + expect_cpu = (1.0 + (procs - 1) * child_fraction) * time * threads + self.assertLessEqual( + total_cpu, + expect_cpu, + "Too high value for CPU time " + "(expected maximum of {0}, got {1})".format(expect_cpu, total_cpu), + ) + self.assertGreaterEqual( + total_cpu, + expect_cpu * slack, + "Too low value for CPU time " + "(expected minimum of {0}, got {1}".format( + expect_cpu * slack, total_cpu + ), + ) + # Wall time tests + total_wall = prmon_json["Max"]["wtime"] + self.assertLessEqual( + total_wall, + time, + "Too high value for wall time " + "(expected maximum of {0}, got {1})".format(time, total_wall), + ) + self.assertGreaterEqual( + total_wall, + time * slack, + "Too low value for wall time " + "(expected minimum of {0}, got {1}".format( + time * slack, total_wall + ), + ) + + # Unit test + if units: + for group in ("Max", "Avg"): + value_params = set(prmon_json[group].keys()) + unit_params = set(prmon_json["Units"][group].keys()) + missing = value_params - unit_params + self.assertEqual( + len(missing), + 0, + "Wrong number of unit values for '{0}'".format(group) + + " - missing parameters are {0}".format(missing), + ) + extras = unit_params - value_params + self.assertEqual( + len(extras), + 0, + "Wrong number of unit values for '{0}'".format(group) + + " - extra parameters are {0}".format(extras), + ) + + return ConfigurableProcessMonitor + + +def main_parse_args_and_get_test(): + """Parse arguments and call test class generator + returning the test case (which is unusual for a + main() function)""" + parser = argparse.ArgumentParser(description="Configurable test runner") + parser.add_argument("--threads", type=int, default=1) + parser.add_argument("--procs", type=int, default=1) + parser.add_argument("--child-fraction", type=float, default=1.0) + parser.add_argument("--time", type=float, default=10) + parser.add_argument("--slack", type=float, default=0.7) + parser.add_argument("--interval", type=int, default=1) + parser.add_argument("--invoke", action="store_true") + parser.add_argument("--units", action="store_true") + + args = parser.parse_args() + # Stop unittest from being confused by the arguments + sys.argv = sys.argv[:1] + + return setup_configurable_test( + args.threads, + args.procs, + args.child_fraction, + args.time, + args.slack, + args.interval, + args.invoke, + args.units, + ) + + +if __name__ == "__main__": + # As unitest will only run tests in the global namespace + # we return the test instance from main() + the_test = main_parse_args_and_get_test() + unittest.main() diff --git a/package/tests/test_exit.py b/package/tests/test_exit.py new file mode 100755 index 0000000..e462f3c --- /dev/null +++ b/package/tests/test_exit.py @@ -0,0 +1,57 @@ +#! /usr/bin/env python3 +# +# Copyright (C) 2018-2020 CERN +# License Apache2 - see LICENCE file + +"""prmon test case for monitored process exit code""" + +import argparse +import subprocess +import sys +import unittest + + +def setup_configurable_test(exit_code=0): + """Wrap the class definition in a function to allow arguments to be passed""" + + class ConfigurableProcessMonitor(unittest.TestCase): + """Wrap the class definition in a function to allow arguments to be passed""" + + def test_run_test_with_params(self): + """Actual test runner""" + child_cmd = ["sh", "-c", "sleep 3 && exit {0}".format(exit_code)] + + prmon_cmd = ["../prmon", "--interval", "1"] + prmon_cmd.append("--") + prmon_cmd.extend(child_cmd) + prmon_p = subprocess.Popen(prmon_cmd, shell=False) + prmon_rc = prmon_p.wait() + + self.assertEqual( + prmon_rc, + exit_code, + "Wrong return code from prmon (expected {0}".format(exit_code), + ) + + return ConfigurableProcessMonitor + + +def main_parse_args_and_get_test(): + """Parse arguments and call test class generator + returning the test case (which is unusual for a + main() function)""" + parser = argparse.ArgumentParser(description="Configurable test runner") + parser.add_argument("--exit-code", type=int, default=0) + + args = parser.parse_args() + # Stop unittest from being confused by the arguments + sys.argv = sys.argv[:1] + + return setup_configurable_test(args.exit_code) + + +if __name__ == "__main__": + # As unitest will only run tests in the global namespace + # we return the test instance from main() + the_test = main_parse_args_and_get_test() + unittest.main() diff --git a/package/tests/test_io.py b/package/tests/test_io.py new file mode 100755 index 0000000..cb800bc --- /dev/null +++ b/package/tests/test_io.py @@ -0,0 +1,102 @@ +#! /usr/bin/env python3 +# +# Copyright (C) 2018-2020 CERN +# License Apache2 - see LICENCE file + +"""prmon test for measuring i/o""" + +import argparse +import json +import subprocess +import sys +import unittest + + +def setup_configurable_test( + io=10, threads=1, procs=1, usleep=10, pause=1, slack=0.95, interval=1 +): + """Wrap the class definition in a function to allow arguments to be passed""" + + class ConfigurableProcessMonitor(unittest.TestCase): + """Test class for a specific set of parameters""" + + def test_run_test_with_params(self): + """Actual test runner""" + burn_cmd = ["./io-burner", "--io", str(io)] + burn_cmd.extend(["--threads", str(threads)]) + burn_cmd.extend(["--procs", str(procs)]) + burn_cmd.extend(["--usleep", str(usleep)]) + burn_cmd.extend(["--pause", str(pause)]) + burn_p = subprocess.Popen(burn_cmd, shell=False) + + prmon_cmd = [ + "../prmon", + "--interval", + str(interval), + "--pid", + str(burn_p.pid), + ] + prmon_p = subprocess.Popen(prmon_cmd, shell=False) + + _ = burn_p.wait() + prmon_rc = prmon_p.wait() + + self.assertEqual(prmon_rc, 0, "Non-zero return code from prmon") + + with open("prmon.json") as infile: + prmon_json = json.load(infile) + + # IO tests + expected_bytes = io * threads * procs * slack * 1.0e6 + self.assertGreaterEqual( + prmon_json["Max"]["wchar"], + expected_bytes, + "Too low value for IO bytes written " + "(expected minimum of {0}, got {1})".format( + expected_bytes, prmon_json["Max"]["wchar"] + ), + ) + self.assertGreaterEqual( + prmon_json["Max"]["rchar"], + expected_bytes, + "Too low value for IO bytes read " + "(expected minimum of {0}, got {1})".format( + expected_bytes, prmon_json["Max"]["rchar"] + ), + ) + + return ConfigurableProcessMonitor + + +def main_parse_args_and_get_test(): + """Parse arguments and call test class generator + returning the test case (which is unusual for a + main() function)""" + parser = argparse.ArgumentParser(description="Configurable test runner") + parser.add_argument("--threads", type=int, default=1) + parser.add_argument("--procs", type=int, default=1) + parser.add_argument("--io", type=int, default=10) + parser.add_argument("--usleep", type=int, default=10) + parser.add_argument("--pause", type=float, default=1) + parser.add_argument("--slack", type=float, default=0.9) + parser.add_argument("--interval", type=int, default=1) + args = parser.parse_args() + # Stop unittest from being confused by the arguments + sys.argv = sys.argv[:1] + + return setup_configurable_test( + args.io, + args.threads, + args.procs, + args.usleep, + args.pause, + args.slack, + args.interval, + ) + + +if __name__ == "__main__": + # As unitest will only run tests in the global namespace + # we return the test instance from main() + the_test = main_parse_args_and_get_test() + unittest.main() diff --git a/package/tests/test_mem.py b/package/tests/test_mem.py new file mode 100755 index 0000000..9771c7a --- /dev/null +++ b/package/tests/test_mem.py @@ -0,0 +1,111 @@ +#! /usr/bin/env python3 +# +# Copyright (C) 2018-2020 CERN +# License Apache2 - see LICENCE file + +"""prmon unitest for memory consumption""" + +import argparse +import json +import subprocess +import sys +import unittest + + +def setup_configurable_test( + procs=4, malloc_mb=100, write_fraction=0.5, sleep=10, slack=0.9, interval=1 +): + """Wrap the class definition in a function to allow arguments to be passed""" + + class ConfigurableProcessMonitor(unittest.TestCase): + """Test class for a specific set of parameters""" + + def check_memory_limits(self, name, value, expected, slack): + """Check values against expectation with a tolerance""" + max_value = expected * (1.0 + (1.0 - slack)) + min_value = expected * slack + self.assertLess( + value, + max_value, + "Too high a value for {0} " + "(expected maximum of {1}, got {2})".format(name, max_value, value), + ) + self.assertGreater( + value, + min_value, + "Too low a value for {0} " + "(expected maximum of {1}, got {2})".format(name, min_value, value), + ) + + def test_run_test_with_params(self): + """Actual test runner""" + burn_cmd = [ + "./mem-burner", + "--sleep", + str(sleep), + "--malloc", + str(malloc_mb), + "--writef", + str(write_fraction), + ] + if procs != 1: + burn_cmd.extend(["--procs", str(procs)]) + + prmon_cmd = ["../prmon", "--interval", str(interval), "--"] + prmon_cmd.extend(burn_cmd) + prmon_p = subprocess.Popen(prmon_cmd, shell=False) + + prmon_rc = prmon_p.wait() + + self.assertEqual(prmon_rc, 0, "Non-zero return code from prmon") + + with open("prmon.json") as infile: + prmon_json = json.load(infile) + + # Memory tests + vmwm_expect = ( + malloc_mb * procs * 1024 + 10000 * procs + ) # Small uplift for program itself + self.check_memory_limits( + "vmem", prmon_json["Max"]["vmem"], vmwm_expect, slack + ) + + rss_expect = malloc_mb * procs * 1024 * write_fraction + self.check_memory_limits( + "rss", prmon_json["Max"]["rss"], rss_expect, slack + ) + + pss_expect = malloc_mb * 1024 * write_fraction + self.check_memory_limits( + "pss", prmon_json["Max"]["pss"], pss_expect, slack + ) + + return ConfigurableProcessMonitor + + +def main_parse_args_and_get_test(): + """Parse arguments and call test class generator + returning the test case (which is unusual for a + main() function)""" + parser = argparse.ArgumentParser(description="Configurable memory test runner") + parser.add_argument("--procs", type=int, default=4) + parser.add_argument("--malloc", type=int, default=100) + parser.add_argument("--writef", type=float, default=0.5) + parser.add_argument("--sleep", type=int, default=10) + parser.add_argument("--slack", type=float, default=0.85) + parser.add_argument("--interval", type=int, default=1) + + args = parser.parse_args() + # Stop unittest from being confused by the arguments + sys.argv = sys.argv[:1] + + return setup_configurable_test( + args.procs, args.malloc, args.writef, args.sleep, args.slack, args.interval + ) + + +if __name__ == "__main__": + # As unitest will only run tests in the global namespace + # we return the test instance from main() + the_test = main_parse_args_and_get_test() + unittest.main() diff --git a/package/tests/test_net.py b/package/tests/test_net.py new file mode 100755 index 0000000..3770a32 --- /dev/null +++ b/package/tests/test_net.py @@ -0,0 +1,113 @@ +#! /usr/bin/env python3 +# +# Copyright (C) 2018-2020 CERN +# License Apache2 - see LICENCE file + +"""Test harness script for network IO""" + +import argparse +import json +import signal +import os +import subprocess +import sys +import unittest + +THE_TEST = None + + +def setup_configurable_test( + blocks=None, requests=10, sleep=None, pause=None, slack=0.95, interval=1 +): + """Wrap the class definition in a function to allow arguments to be passed""" + + class ConfigurableProcessMonitor(unittest.TestCase): + """Test class for a specific set of parameters""" + + def setUp(self): + """Start the simple python HTTP CGI server""" + http_server_cmd = ["python3", "-m", "http.server", "--cgi"] + self.http_server = subprocess.Popen(http_server_cmd, shell=False) + + def tearDown(self): + """Kill http server""" + os.kill(self.http_server.pid, signal.SIGTERM) + + def test_run_test_with_params(self): + """Test class for a specific set of parameters""" + burn_cmd = ["python3", "./net_burner.py"] + if requests: + burn_cmd.extend(["--requests", str(requests)]) + if pause: + burn_cmd.extend(["--pause", str(pause)]) + if sleep: + burn_cmd.extend(["--sleep", str(sleep)]) + if blocks: + burn_cmd.extend(["--blocks", str(blocks)]) + burn_p = subprocess.Popen(burn_cmd, shell=False) + prmon_cmd = [ + "../prmon", + "--interval", + str(interval), + "--pid", + str(burn_p.pid), + ] + prmon_p = subprocess.Popen(prmon_cmd, shell=False) + + _ = burn_p.wait() + prmon_rc = prmon_p.wait() + + self.assertEqual(prmon_rc, 0, "Non-zero return code from prmon") + + with open("prmon.json") as infile: + prmon_json = json.load(infile) + + # Network tests + expected_bytes = 1025000 * requests * slack + self.assertGreaterEqual( + prmon_json["Max"]["rx_bytes"], + expected_bytes, + "Too low value for rx bytes " + "(expected minimum of {0}, got {1})".format( + expected_bytes, prmon_json["Max"]["rx_bytes"] + ), + ) + self.assertGreaterEqual( + prmon_json["Max"]["tx_bytes"], + expected_bytes, + "Too low value for tx bytes " + "(expected minimum of {0}, got {1})".format( + expected_bytes, prmon_json["Max"]["tx_bytes"] + ), + ) + + return ConfigurableProcessMonitor + + +def main_parse_args_and_get_test(): + """Parse arguments and call test class generator + returning the test case (which is unusual for a + main() function)""" + parser = argparse.ArgumentParser( + description="Configurable test runner for network access" + ) + parser.add_argument("--blocks", type=int) + parser.add_argument("--requests", type=int, default=10) + parser.add_argument("--sleep", type=float) + parser.add_argument("--pause", type=float) + parser.add_argument("--slack", type=float, default=0.8) + parser.add_argument("--interval", type=int, default=1) + args = parser.parse_args() + # Stop unittest from being confused by the arguments + sys.argv = sys.argv[:1] + + return setup_configurable_test( + args.blocks, args.requests, args.sleep, args.pause, args.slack, args.interval + ) + + +if __name__ == "__main__": + # As unitest will only run tests in the global namespace + # we return the test instance from main() + the_test = main_parse_args_and_get_test() + unittest.main() diff --git a/package/tests/test_net2.py b/package/tests/test_net2.py new file mode 100755 index 0000000..bbf361d --- /dev/null +++ b/package/tests/test_net2.py @@ -0,0 +1,112 @@ +#! /usr/bin/env python2 +# +# Copyright (C) 2018-2020 CERN +# License Apache2 - see LICENCE file +# +# Test script for network IO (Python2 version) +from __future__ import print_function, unicode_literals + +import argparse +import json +import os +import signal +import subprocess +import sys +import unittest + + +def setup_configurable_test( + blocks=None, requests=10, sleep=None, pause=None, slack=0.95, interval=1 +): + """Wrap the class definition in a function to allow arguments to be passed""" + + class ConfigurableProcessMonitor(unittest.TestCase): + """Test class for a specific set of parameters""" + + def setUp(self): + """Start the simple python HTTP CGI server""" + http_server_cmd = ["python2", "-m", "CGIHTTPServer"] + self.http_server = subprocess.Popen(http_server_cmd, shell=False) + + def tearDown(self): + """Kill http server""" + os.kill(self.http_server.pid, signal.SIGTERM) + + def test_run_test_with_params(self): + """Test class for a specific set of parameters""" + burn_cmd = ["python", "./net_burner2.py"] + if requests: + burn_cmd.extend(["--requests", str(requests)]) + if pause: + burn_cmd.extend(["--pause", str(pause)]) + if sleep: + burn_cmd.extend(["--sleep", str(sleep)]) + if blocks: + burn_cmd.extend(["--blocks", str(blocks)]) + burn_p = subprocess.Popen(burn_cmd, shell=False) + prmon_cmd = [ + "../prmon", + "--interval", + str(interval), + "--pid", + str(burn_p.pid), + ] + prmon_p = subprocess.Popen(prmon_cmd, shell=False) + + _ = burn_p.wait() + prmon_rc = prmon_p.wait() + + self.assertEqual(prmon_rc, 0, "Non-zero return code from prmon") + + with open("prmon.json") as infile: + prmon_json = json.load(infile) + + # Network tests + expected_bytes = 1025000 * requests * slack + self.assertGreaterEqual( + prmon_json["Max"]["rx_bytes"], + expected_bytes, + "Too low value for rx bytes " + "(expected minimum of {0}, got {1})".format( + expected_bytes, prmon_json["Max"]["rx_bytes"] + ), + ) + self.assertGreaterEqual( + prmon_json["Max"]["tx_bytes"], + expected_bytes, + "Too low value for tx bytes " + "(expected minimum of {0}, got {1})".format( + expected_bytes, prmon_json["Max"]["tx_bytes"] + ), + ) + + return ConfigurableProcessMonitor + + +def main_parse_args_and_get_test(): + """Parse arguments and call test class generator + returning the test case (which is unusual for a + main() function)""" + parser = argparse.ArgumentParser( + description="Configurable test runner for network access" + ) + parser.add_argument("--blocks", type=int) + parser.add_argument("--requests", type=int, default=10) + parser.add_argument("--sleep", type=float) + parser.add_argument("--pause", type=float) + parser.add_argument("--slack", type=float, default=0.90) + parser.add_argument("--interval", type=int, default=1) + args = parser.parse_args() + # Stop unittest from being confused by the arguments + sys.argv = sys.argv[:1] + + return setup_configurable_test( + args.blocks, args.requests, args.sleep, args.pause, args.slack, args.interval + ) + + +if __name__ == "__main__": + # As unitest will only run tests in the global namespace + # we return the test instance from main() + the_test = main_parse_args_and_get_test() + unittest.main()