[LTP] [PATCH 1/2] scripts: Add simple script for calculating timeouts

Andrea Cervesato andrea.cervesato@suse.com
Tue Jan 21 14:12:23 CET 2025


Hi!

I like the general idea, a couple of comments on the python code.

On 1/21/25 13:34, Cyril Hrubis wrote:
> This script parses JSON results from kirk and LTP metadata in order
> calculate timeouts for tests based on the result file and can even patch
> tests automatically.
>
> The script does:
>
> - Take the results and pick all tests that run for longer than 0.5s.
>    Multiplies the time with a constant (currently 1.2) to get a suggested
>    timeout.
>
> - Exclude tests that have runtime defined since these are controller
>    by the runtime (that filters out all fuzzy sync tests).
>
>    There is a special case for timer tests that define runtime only
>    dynamically in the timer library code. This should be possibly fixed
>    with special value for the .runtime in tst_test. E.g.
>    TST_RUNTIME_DYNAMIC for tests that only set runtime in the setup.
>
> - Normalize the timeout per a single filesystem run if test is running for
>    more than one filesystem.
>
> - Tests that do not have a metadata record are old library tests which
>    which cannot be patched but are printed in a separate table if we
>    request a table to be printed.
>
> - If patching option is selected tests are update with newly calculated
>    timeout. By default we only increase timeouts but that can be
>    overrided with the -o option.
>
> Signed-off-by: Cyril Hrubis <chrubis@suse.cz>
> ---
>   scripts/calc_timeouts.py | 133 +++++++++++++++++++++++++++++++++++++++
>   1 file changed, 133 insertions(+)
>   create mode 100755 scripts/calc_timeouts.py
>
> diff --git a/scripts/calc_timeouts.py b/scripts/calc_timeouts.py
> new file mode 100755
> index 000000000..c69ab8f57
> --- /dev/null
> +++ b/scripts/calc_timeouts.py
> @@ -0,0 +1,133 @@
> +#!/usr/bin/python
> +import re
> +import json
> +import getopt
> +import sys
> +from os import system
> +
> +# Top level directory path
> +top_dir = '../'
Better to pass this folder to the script using parameters, also because 
metadata can be found in the install folder as well.
> +
> +# The test runtime is multiplied by this to get a timeout
> +timeout_mul = 1.2
Global variables in python use TIMEOUT_MUL format.
> +
> +def patch(fname, new_timeout, patch_override):
> +
> +    orig_timeout = None
> +    file_path = top_dir + fname
os.path.join()
> +
> +    with open(file_path, 'r') as c_source:
> +        for line in c_source:
> +            timeout = re.search(r'\s*.timeout\s*=\s*(\d+).', line)
This regex should be compiled before loop using re.compile(), otherwise 
re.search() will compile it for each line.
> +            if timeout:
> +                orig_timeout = int(timeout.group(1))
> +
> +    if orig_timeout:
> +        if orig_timeout < new_timeout or patch_override:
> +            print("CHANGE %s timeout %i -> %i" % (fname, orig_timeout, new_timeout))
> +            system("sed -i 's/\\.timeout = [0-9]*/\\.timeout = " + str(new_timeout) + "/' " + file_path)

This can be substituted with a python version of sed, since system() is 
never a good idea, unless we really need to use it for external tools 
which cannot be translated into python. The solution would be:

content = []
matcher = re.match(r'\s*.timeout\s*=\s*(\d+).')
with open(file_path, 'r') as data:
     for line in data:
         # use regex here to find the matching string
         if matcher.search(line):
             content.append(f'timeout = {new_timeout}')
         else:
             content.append(line)

with open(file_path, 'w') as data:
         data.writelines(content)

> +        else:
> +            print("KEEP   %s timeout %i (new %i)" % (fname, orig_timeout, new_timeout))
> +    else:
> +        print("ADD    %s timeout %i" % (fname, new_timeout))
> +        system("sed -i '/static struct tst_test test = {/a\\\\t.timeout = " + str(new_timeout) + ",' " + file_path)
Same here. Maybe we can create a generic sed() function for both cases.
> +
> +def patch_all(timeouts, patch_override):
> +    for timeout in timeouts:
> +        if timeout[3]:
> +            patch(timeout[3], timeout[1], patch_override)
> +
> +def print_table(timeouts):
> +    timeouts.sort(key=lambda x: x[1], reverse=True)
> +
> +    total = 0;
> +
> +    print("Old library tests\n-----------------\n");
> +    for timeout in timeouts:
> +        if not timeout[2]:
> +            print("%-30s %i" % (timeout[0], timeout[1]))
> +            total+=1
> +
> +    print("\n\t%i tests in total" % total)
> +
> +    total = 0;
> +
> +    print("\nNew library tests\n-----------------\n");
> +    for timeout in timeouts:
> +        if timeout[2]:
> +            print("%-30s %i" % (timeout[0], timeout[1]))
> +            total+=1
> +
> +    print("\n\t%i tests in total" % total)
> +
> +def parse_data(results_path):
> +    timeouts = []
> +
> +    with open(results_path, 'r') as file:
> +        results = json.load(file)
> +
> +    with open(top_dir + 'metadata/ltp.json', 'r') as file:
> +        metadata = json.load(file)
> +
> +    for test in results['results']:
> +        name = test['test_fqn']
> +        duration = test['test']['duration']
> +        # If test runs for all_filesystems normalize the runtime per a single filesystem
> +        filesystems = max(1, test['test']['log'].count('TINFO: Formatting /'))
> +        # Check if test is new library test
> +        test_is_newlib = name in metadata['tests']
> +        # Store test file path
> +        path = None if not test_is_newlib else metadata['tests'][name]['fname']
> +        # Filter out tests with runtime
> +        test_has_runtime = False if not test_is_newlib else 'runtime' in metadata['tests'][name]
> +        # Timer tests define runtime dynamically in timer library
> +        if test_is_newlib and 'sample' in metadata['tests'][name]:
> +            test_has_runtime = True
> +        # Select tests that does not have runtime and are executed for longer time
> +        if not test_has_runtime and duration >= 0.5:
> +            timeouts.append((name, int(timeout_mul * duration/filesystems + 0.5), test_is_newlib, path))
> +
> +    return timeouts

In this case it's better to avoid tuples because they make code more 
difficult to read. Hash is a better approach in general:

data["name"] = name
data["timeout"] = int(timeout_mul * duration/filesystems + 0.5)
data["new_lib"] = test_is_newlib
data["path"] = path

return data

> +
> +def print_help():
> +    print('calc_timeouts.py [OPTION] [RESULT].json')
> +    print('\t-h prints this help')
> +    print('\t-o override test timeouts, by default timeouts are only increased')
> +    print('\t-p patch testcases with updated timeouts')
> +    print('\t-t prints table of tests with suggested timeouts')
> +
> +try:
> +    opts, args = getopt.getopt(sys.argv[1:], "hopt")
> +except:
> +    print_help()
> +    sys.exit(1)
> +
> +opt_print_table = False
> +opt_patch_tests = False
> +opt_patch_override = False
> +
> +for opt,arg in opts:
> +    if opt == '-h':
> +        print_help()
> +        sys.exit(0)
> +    if opt == '-o':
> +        opt_patch_override = True
> +    if opt == '-p':
> +        opt_patch_tests = True
> +    if opt == '-t':
> +        opt_print_table = True
> +
> +if not opt_print_table and not opt_patch_tests:
> +    print("No action selected!\n")
> +    print_help()
> +    sys.exit(1)
> +
> +results = args[0] if args else 'results.json'
> +
> +timeouts = parse_data(results)
> +
> +if opt_print_table:
> +    print_table(timeouts)
> +
> +if opt_patch_tests:
> +    patch_all(timeouts, opt_patch_override)
Better to wrap everything into a main() function and to use:

if __name__ == "__main__":

     run()


Andrea



More information about the ltp mailing list