| 1 | #!/usr/bin/python3
 | 
|---|
| 2 | """
 | 
|---|
| 3 | Python Script to implement R.M.I.T. testing : Randomized Multiple Interleaved Trials
 | 
|---|
| 4 | 
 | 
|---|
| 5 | ./rmit.py run COMMAND CANDIDATES
 | 
|---|
| 6 | -t trials
 | 
|---|
| 7 | -o option:values
 | 
|---|
| 8 | """
 | 
|---|
| 9 | 
 | 
|---|
| 10 | 
 | 
|---|
| 11 | import argparse
 | 
|---|
| 12 | import datetime
 | 
|---|
| 13 | import itertools
 | 
|---|
| 14 | import json
 | 
|---|
| 15 | import os
 | 
|---|
| 16 | import random
 | 
|---|
| 17 | import re
 | 
|---|
| 18 | import subprocess
 | 
|---|
| 19 | import sys
 | 
|---|
| 20 | 
 | 
|---|
| 21 | 
 | 
|---|
| 22 | def parse_range(x):
 | 
|---|
| 23 |     result = []
 | 
|---|
| 24 |     for part in x.split(','):
 | 
|---|
| 25 |         if '-' in part:
 | 
|---|
| 26 |             a, b = part.split('-')
 | 
|---|
| 27 |             a, b = int(a), int(b)
 | 
|---|
| 28 |             result.extend(range(a, b + 1))
 | 
|---|
| 29 |         else:
 | 
|---|
| 30 |             a = int(part)
 | 
|---|
| 31 |             result.append(a)
 | 
|---|
| 32 |     return result
 | 
|---|
| 33 | 
 | 
|---|
| 34 | class DependentOpt:
 | 
|---|
| 35 |         def __init__(self, key, value):
 | 
|---|
| 36 |                 self.key = key
 | 
|---|
| 37 |                 self.value = value
 | 
|---|
| 38 |                 self.vars = re.findall("[a-zA-Z]", value)
 | 
|---|
| 39 | 
 | 
|---|
| 40 | def parse_option(key, values):
 | 
|---|
| 41 |         try:
 | 
|---|
| 42 |                 num = int(values)
 | 
|---|
| 43 |                 return key, [num]
 | 
|---|
| 44 |         except:
 | 
|---|
| 45 |                 pass
 | 
|---|
| 46 | 
 | 
|---|
| 47 |         if re.search("^[0-9-,]+$", values):
 | 
|---|
| 48 |                 values = parse_range(values)
 | 
|---|
| 49 |                 return key, [v for v in values]
 | 
|---|
| 50 |         else:
 | 
|---|
| 51 |                 return key, DependentOpt(key, values)
 | 
|---|
| 52 | 
 | 
|---|
| 53 | def eval_one(fmt, vals):
 | 
|---|
| 54 |         orig = fmt
 | 
|---|
| 55 |         for k, v in vals:
 | 
|---|
| 56 |                 fmt = fmt.replace(k, str(v))
 | 
|---|
| 57 | 
 | 
|---|
| 58 |         if not re.search("^[0-9-/*+ ]+$", fmt):
 | 
|---|
| 59 |                 print('ERROR: pattern option {} (interpreted as {}) could not be evaluated'.format(orig, fmt), file=sys.stderr)
 | 
|---|
| 60 |                 sys.exit(1)
 | 
|---|
| 61 | 
 | 
|---|
| 62 |         return eval(fmt)
 | 
|---|
| 63 | 
 | 
|---|
| 64 | def eval_options(opts):
 | 
|---|
| 65 |         dependents = [d for d in opts.values() if type(d) is DependentOpt]
 | 
|---|
| 66 |         processed = []
 | 
|---|
| 67 |         nopts = []
 | 
|---|
| 68 |         for d in dependents:
 | 
|---|
| 69 |                 processed.append(d.key)
 | 
|---|
| 70 |                 lists = []
 | 
|---|
| 71 |                 for dvar in d.vars:
 | 
|---|
| 72 |                         if not dvar in opts.keys():
 | 
|---|
| 73 |                                 print('ERROR: extra pattern option {}:{} uses unknown key {}'.format(d.key,d.value,dvar), file=sys.stderr)
 | 
|---|
| 74 |                                 sys.exit(1)
 | 
|---|
| 75 | 
 | 
|---|
| 76 |                         lists.append([(dvar, o) for o in opts[dvar]])
 | 
|---|
| 77 |                         processed.append(dvar)
 | 
|---|
| 78 | 
 | 
|---|
| 79 |                 kopt = []
 | 
|---|
| 80 |                 for vals in list(itertools.product(*lists)):
 | 
|---|
| 81 |                         res = ['-{}'.format(d.key), "{}".format(eval_one(d.value, vals))]
 | 
|---|
| 82 |                         for k, v in vals:
 | 
|---|
| 83 |                                 res.extend(['-{}'.format(k), "{}".format(v)])
 | 
|---|
| 84 |                         kopt.append(res)
 | 
|---|
| 85 |                 nopts.append(kopt)
 | 
|---|
| 86 | 
 | 
|---|
| 87 | 
 | 
|---|
| 88 |         for k, vals in opts.items():
 | 
|---|
| 89 |                 if k not in processed:
 | 
|---|
| 90 |                         kopt = []
 | 
|---|
| 91 |                         for v in vals:
 | 
|---|
| 92 |                                 kopt.append(['-{}'.format(k), "{}".format(v)])
 | 
|---|
| 93 |                         nopts.append(kopt)
 | 
|---|
| 94 | 
 | 
|---|
| 95 |         return nopts
 | 
|---|
| 96 | 
 | 
|---|
| 97 | def actions_eta(actions):
 | 
|---|
| 98 |         time = 0
 | 
|---|
| 99 |         for a in actions:
 | 
|---|
| 100 |                 i = 0
 | 
|---|
| 101 |                 while i < len(a):
 | 
|---|
| 102 |                         if a[i] == '-d':
 | 
|---|
| 103 |                                 i += 1
 | 
|---|
| 104 |                                 if i != len(a):
 | 
|---|
| 105 |                                         time += int(a[i])
 | 
|---|
| 106 |                         i += 1
 | 
|---|
| 107 |         return time
 | 
|---|
| 108 | 
 | 
|---|
| 109 | if __name__ == "__main__":
 | 
|---|
| 110 |         # ================================================================================
 | 
|---|
| 111 |         # parse command line arguments
 | 
|---|
| 112 |         formats = ['raw', 'csv', 'json']
 | 
|---|
| 113 |         parser = argparse.ArgumentParser(description='Python Script to implement R.M.I.T. testing : Randomized Multiple Interleaved Trials')
 | 
|---|
| 114 |         parser.add_argument('--list', help='List all the commands that would be run', action='store_true')
 | 
|---|
| 115 |         parser.add_argument('--file', nargs='?', type=argparse.FileType('w'), default=sys.stdout)
 | 
|---|
| 116 |         parser.add_argument('--trials', help='Number of trials to run per combinaison', type=int, default=3)
 | 
|---|
| 117 |         parser.add_argument('command', metavar='command', type=str, nargs=1, help='the command prefix to run')
 | 
|---|
| 118 |         parser.add_argument('candidates', metavar='candidates', type=str, nargs='*', help='the candidate suffix to run')
 | 
|---|
| 119 | 
 | 
|---|
| 120 |         try:
 | 
|---|
| 121 |                 options, unknown =  parser.parse_known_args()
 | 
|---|
| 122 | 
 | 
|---|
| 123 |                 options.option = []
 | 
|---|
| 124 |                 while unknown:
 | 
|---|
| 125 |                         key = unknown.pop(0)
 | 
|---|
| 126 |                         val = unknown.pop(0)
 | 
|---|
| 127 | 
 | 
|---|
| 128 |                         if key[0] != '-':
 | 
|---|
| 129 |                                 raise ValueError
 | 
|---|
| 130 | 
 | 
|---|
| 131 |                         options.option.append((key[1:], val))
 | 
|---|
| 132 | 
 | 
|---|
| 133 |         except:
 | 
|---|
| 134 |                 print('ERROR: invalid arguments', file=sys.stderr)
 | 
|---|
| 135 |                 parser.print_help(sys.stderr)
 | 
|---|
| 136 |                 sys.exit(1)
 | 
|---|
| 137 | 
 | 
|---|
| 138 |         # ================================================================================
 | 
|---|
| 139 |         # Identify the commands to run
 | 
|---|
| 140 |         command = './' + options.command[0]
 | 
|---|
| 141 |         if options.candidates:
 | 
|---|
| 142 |                 commands = [command + "-" + c for c in options.candidates]
 | 
|---|
| 143 |         else:
 | 
|---|
| 144 |                 commands = [command]
 | 
|---|
| 145 |         for c in commands:
 | 
|---|
| 146 |                 if not os.path.isfile(c):
 | 
|---|
| 147 |                         print('ERROR: invalid command {}, file does not exist'.format(c), file=sys.stderr)
 | 
|---|
| 148 |                         sys.exit(1)
 | 
|---|
| 149 | 
 | 
|---|
| 150 |                 if not os.access(c, os.X_OK):
 | 
|---|
| 151 |                         print('ERROR: invalid command {}, file not executable'.format(c), file=sys.stderr)
 | 
|---|
| 152 |                         sys.exit(1)
 | 
|---|
| 153 | 
 | 
|---|
| 154 | 
 | 
|---|
| 155 |         # ================================================================================
 | 
|---|
| 156 |         # Identify the options to run
 | 
|---|
| 157 |         opts = dict([parse_option(k, v) for k, v in options.option])
 | 
|---|
| 158 | 
 | 
|---|
| 159 |         # Evaluate the options (options can depend on the value of other options)
 | 
|---|
| 160 |         opts = eval_options(opts)
 | 
|---|
| 161 | 
 | 
|---|
| 162 |         # ================================================================================
 | 
|---|
| 163 |         # Figure out all the combinations to run
 | 
|---|
| 164 |         actions = []
 | 
|---|
| 165 |         for p in itertools.product(range(options.trials), commands, *opts):
 | 
|---|
| 166 |                 act = [p[1]]
 | 
|---|
| 167 |                 for o in p[2:]:
 | 
|---|
| 168 |                         act.extend(o)
 | 
|---|
| 169 |                 actions.append(act)
 | 
|---|
| 170 | 
 | 
|---|
| 171 |         # ================================================================================
 | 
|---|
| 172 |         # Figure out all the combinations to run
 | 
|---|
| 173 |         if options.list:
 | 
|---|
| 174 |                 for a in actions:
 | 
|---|
| 175 |                         print(" ".join(a))
 | 
|---|
| 176 |                 sys.exit(0)
 | 
|---|
| 177 | 
 | 
|---|
| 178 | 
 | 
|---|
| 179 |         # ================================================================================
 | 
|---|
| 180 |         # Prepare to run
 | 
|---|
| 181 | 
 | 
|---|
| 182 |         # find expected time
 | 
|---|
| 183 |         time = actions_eta(actions)
 | 
|---|
| 184 |         print("Running {} trials{}".format(len(actions), "" if time == 0 else " (expecting to take {})".format(str(datetime.timedelta(seconds=int(time)))) ))
 | 
|---|
| 185 | 
 | 
|---|
| 186 |         random.shuffle(actions)
 | 
|---|
| 187 | 
 | 
|---|
| 188 |         # ================================================================================
 | 
|---|
| 189 |         # Run
 | 
|---|
| 190 |         options.file.write("[")
 | 
|---|
| 191 |         first = True
 | 
|---|
| 192 |         for i, a in enumerate(actions):
 | 
|---|
| 193 |                 sa = " ".join(a)
 | 
|---|
| 194 |                 if first:
 | 
|---|
| 195 |                         first = False
 | 
|---|
| 196 |                 else:
 | 
|---|
| 197 |                         options.file.write(",")
 | 
|---|
| 198 |                 if options.file != sys.stdout:
 | 
|---|
| 199 |                         print("{}/{} : {}          \r".format(i, len(actions), sa), end = '')
 | 
|---|
| 200 |                 fields = {}
 | 
|---|
| 201 |                 with subprocess.Popen( a, stdout  = subprocess.PIPE, stderr  = subprocess.PIPE) as proc:
 | 
|---|
| 202 |                         out, err = proc.communicate()
 | 
|---|
| 203 |                         if proc.returncode != 0:
 | 
|---|
| 204 |                                 print("ERROR: command '{}' encountered error, returned code {}".format(sa, proc.returncode), file=sys.stderr)
 | 
|---|
| 205 |                                 print(err.decode("utf-8"))
 | 
|---|
| 206 |                                 sys.exit(1)
 | 
|---|
| 207 |                         for s in out.decode("utf-8").splitlines():
 | 
|---|
| 208 |                                 match = re.search("^(.*):(.*)$", s)
 | 
|---|
| 209 |                                 if match:
 | 
|---|
| 210 |                                         fields[match.group(1).strip()] = float(match.group(2).strip().replace(',',''))
 | 
|---|
| 211 | 
 | 
|---|
| 212 |                 options.file.write(json.dumps([a[0][2:], sa, fields]))
 | 
|---|
| 213 |                 options.file.flush()
 | 
|---|
| 214 | 
 | 
|---|
| 215 |         options.file.write("]\n")
 | 
|---|
| 216 | 
 | 
|---|
| 217 |         if options.file != sys.stdout:
 | 
|---|
| 218 |                 print("Done                                                                                ")
 | 
|---|