1 # Copyright 2021-2022. The MBI project. All rights reserved.
2 # This program is free software; you can redistribute it and/or modify it under the terms of the license (GNU GPL).
15 def ensure_image(self, params="", dockerparams=""):
16 """Verify that this is executed from the right docker image, and complain if not."""
17 if os.path.exists("/MBI") or os.path.exists("trust_the_installation"):
18 print("This seems to be a MBI docker image. Good.")
20 print("Please run this script in a MBI docker image. Run these commands:")
21 print(" docker build -f Dockerfile -t mpi-bugs-initiative:latest . # Only the first time")
22 print(f" docker run -it --rm --name MIB --volume $(pwd):/MBI {dockerparams}mpi-bugs-initiative /MBI/MBI.py {params}")
25 def build(self, rootdir, cached=True):
26 """Rebuilds the tool binaries. By default, we try to reuse the existing build."""
27 print("Nothing to do to rebuild the tool binaries.")
29 def setup(self, rootdir):
31 Ensure that this tool (previously built) is usable in this environment: setup the PATH, etc.
32 This is called only once for all tests, from the logs directory.
36 def run(self, execcmd, filename, binary, num_id, timeout, batchinfo):
37 """Compile that test code and anaylse it with the Tool if needed (a cache system should be used)"""
42 Clean the results of all test runs: remove temp files and binaries.
43 This is called only once for all tests, from the logs directory.
47 def parse(self, cachefile):
48 """Read the result of a previous run from the cache, and compute the test outcome"""
51 # Associate all possible detailed outcome to a given error scope. Scopes must be sorted alphabetically.
53 # scope limited to one call
54 'InvalidBuffer':'AInvalidParam', 'InvalidCommunicator':'AInvalidParam', 'InvalidDatatype':'AInvalidParam', 'InvalidRoot':'AInvalidParam', 'InvalidTag':'AInvalidParam', 'InvalidWindow':'AInvalidParam', 'InvalidOperator':'AInvalidParam', 'InvalidOtherArg':'AInvalidParam', 'ActualDatatype':'AInvalidParam',
55 'InvalidSrcDest':'AInvalidParam',
57 # 'OutOfInitFini':'BInitFini',
58 'CommunicatorLeak':'BResLeak', 'DatatypeLeak':'BResLeak', 'GroupLeak':'BResLeak', 'OperatorLeak':'BResLeak', 'TypeLeak':'BResLeak', 'RequestLeak':'BResLeak',
59 'MissingStart':'BReqLifecycle', 'MissingWait':'BReqLifecycle',
60 'MissingEpoch':'BEpochLifecycle','DoubleEpoch':'BEpochLifecycle',
61 'LocalConcurrency':'BLocalConcurrency',
63 'CallMatching':'DMatch',
64 'CommunicatorMatching':'CMatch', 'DatatypeMatching':'CMatch', 'OperatorMatching':'CMatch', 'RootMatching':'CMatch', 'TagMatching':'CMatch',
65 'MessageRace':'DRace',
67 'GlobalConcurrency':'DGlobalConcurrency',
69 'BufferingHazard':'EBufferingHazard',
73 'AInvalidParam':'single call',
74 'BResLeak':'single process',
75 # 'BInitFini':'single process',
76 'BReqLifecycle':'single process',
77 'BEpochLifecycle':'single process',
78 'BLocalConcurrency':'single process',
79 'CMatch':'multi-processes',
80 'DRace':'multi-processes',
81 'DMatch':'multi-processes',
82 'DGlobalConcurrency':'multi-processes',
83 'EBufferingHazard':'system',
84 'FOK':'correct executions'
88 'AInvalidParam':'Invalid parameter',
89 'BResLeak':'Resource leak',
90 # 'BInitFini':'MPI call before initialization/after finalization',
91 'BReqLifecycle':'Request lifecycle',
92 'BLocalConcurrency':'Local concurrency',
93 'CMatch':'Parameter matching',
94 'DMatch':"Call ordering",
95 'DRace':'Message race',
96 'DGlobalConcurrency':'Global concurrency',
97 'EBufferingHazard':'Buffering hazard',
98 'FOK':"Correct execution",
100 'aislinn':'Aislinn', 'civl':'CIVL', 'hermes':'Hermes', 'isp':'ISP', 'itac':'ITAC', 'simgrid':'Mc SimGrid', 'smpi':'SMPI', 'smpivg':'SMPI+VG', 'mpisv':'MPI-SV', 'must':'MUST', 'parcoach':'PARCOACH'
103 def parse_one_code(filename):
105 Reads the header of the provided filename, and extract a list of todo item, each of them being a (cmd, expect, test_num) tupple.
106 The test_num is useful to build a log file containing both the binary and the test_num, when there is more than one test in the same binary.
110 with open(filename, "r") as input_file:
111 state = 0 # 0: before header; 1: in header; 2; after header
113 for line in input_file:
114 if re.match(".*BEGIN_MBI_TESTS.*", line):
118 raise ValueError(f"MBI_TESTS header appears a second time at line {line_num}: \n{line}")
119 elif re.match(".*END_MBI_TESTS.*", line):
123 raise ValueError(f"Unexpected end of MBI_TESTS header at line {line_num}: \n{line}")
124 if state == 1 and re.match(r'\s+\$ ?.*', line):
125 m = re.match(r'\s+\$ ?(.*)', line)
127 nextline = next(input_file)
129 if re.match('[ |]*OK *', nextline):
132 m = re.match('[ |]*ERROR: *(.*)', nextline)
135 f"\n{filename}:{line_num}: MBI parse error: Test not followed by a proper 'ERROR' line:\n{line}{nextline}")
138 if detail not in possible_details:
140 f"\n{filename}:{line_num}: MBI parse error: Detailled outcome {detail} is not one of the allowed ones.")
142 nextline = next(input_file)
143 m = re.match('[ |]*(.*)', nextline)
145 raise ValueError(f"\n{filename}:{line_num}: MBI parse error: Expected diagnostic of the test not found.\n")
146 diagnostic = m.group(1)
148 test = {'filename': filename, 'id': test_num, 'cmd': cmd, 'expect': expect, 'detail': detail, 'diagnostic': diagnostic}
149 res.append(test.copy())
154 raise ValueError(f"MBI_TESTS header not found in file '{filename}'.")
156 raise ValueError(f"MBI_TESTS header not properly ended in file '{filename}'.")
159 raise ValueError(f"No test found in {filename}. Please fix it.")
162 def categorize(tool, toolname, test_id, expected):
163 outcome = tool.parse(test_id)
165 if not os.path.exists(f'{test_id}.elapsed') and not os.path.exists(f'logs/{toolname}/{test_id}.elapsed'):
166 if outcome == 'failure':
169 raise ValueError(f"Invalid test result: {test_id}.txt exists but not {test_id}.elapsed")
171 with open(f'{test_id}.elapsed' if os.path.exists(f'{test_id}.elapsed') else f'logs/{toolname}/{test_id}.elapsed', 'r') as infile:
172 elapsed = infile.read()
174 # Properly categorize this run
175 if outcome == 'timeout':
176 res_category = 'timeout'
178 diagnostic = 'hard timeout'
180 diagnostic = f'timeout after {elapsed} sec'
181 elif outcome == 'failure' or outcome == 'segfault':
182 res_category = 'failure'
183 diagnostic = 'tool error, or test not run'
184 elif outcome == 'UNIMPLEMENTED':
185 res_category = 'unimplemented'
186 diagnostic = 'coverage issue'
187 elif outcome == 'other':
188 res_category = 'other'
189 diagnostic = 'inconclusive run'
190 elif expected == 'OK':
192 res_category = 'TRUE_NEG'
193 diagnostic = 'correctly reported no error'
195 res_category = 'FALSE_POS'
196 diagnostic = 'reported an error in a correct code'
197 elif expected == 'ERROR':
199 res_category = 'FALSE_NEG'
200 diagnostic = 'failed to detect an error'
202 res_category = 'TRUE_POS'
203 diagnostic = 'correctly detected an error'
205 raise ValueError(f"Unexpected expectation: {expected} (must be OK or ERROR)")
207 return (res_category, elapsed, diagnostic, outcome)
210 def run_cmd(buildcmd, execcmd, cachefile, filename, binary, timeout, batchinfo, read_line_lambda=None):
212 Runs the test on need. Returns True if the test was ran, and False if it was cached.
214 The result is cached if possible, and the test is rerun only if the `test.txt` (containing the tool output) or the `test.elapsed` (containing the timing info) do not exist, or if `test.md5sum` (containing the md5sum of the code to compile) does not match.
217 - buildcmd and execcmd are shell commands to run. buildcmd can be any shell line (incuding && groups), but execcmd must be a single binary to run.
218 - cachefile is the name of the test
219 - filename is the source file containing the code
220 - binary the file name in which to compile the code
221 - batchinfo: something like "1/1" to say that this run is the only batch (see -b parameter of MBI.py)
222 - read_line_lambda: a lambda to which each line of the tool output is feed ASAP. It allows MUST to interrupt the execution when a deadlock is reported.
224 if os.path.exists(f'{cachefile}.txt') and os.path.exists(f'{cachefile}.elapsed') and os.path.exists(f'{cachefile}.md5sum'):
225 hash_md5 = hashlib.md5()
226 with open(filename, 'rb') as sourcefile:
227 for chunk in iter(lambda: sourcefile.read(4096), b""):
228 hash_md5.update(chunk)
229 newdigest = hash_md5.hexdigest()
230 with open(f'{cachefile}.md5sum', 'r') as md5file:
231 olddigest = md5file.read()
232 #print(f'Old digest: {olddigest}; New digest: {newdigest}')
233 if olddigest == newdigest:
234 print(f" (result cached -- digest: {olddigest})")
236 os.remove(f'{cachefile}.txt')
238 print(f"Wait up to {timeout} seconds")
240 start_time = time.time()
242 output = f"No need to compile {binary}.c (batchinfo:{batchinfo})\n\n"
244 output = f"Compiling {binary}.c (batchinfo:{batchinfo})\n\n"
245 output += f"$ {buildcmd}\n"
247 compil = subprocess.run(buildcmd, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
248 if compil.stdout is not None:
249 output += str(compil.stdout, errors='replace')
250 if compil.returncode != 0:
251 output += f"Compilation of {binary}.c raised an error (retcode: {compil.returncode})"
252 for line in (output.split('\n')):
253 print(f"| {line}", file=sys.stderr)
254 with open(f'{cachefile}.elapsed', 'w') as outfile:
255 outfile.write(str(time.time() - start_time))
256 with open(f'{cachefile}.txt', 'w') as outfile:
257 outfile.write(output)
260 output += f"\n\nExecuting the command\n $ {execcmd}\n"
261 for line in (output.split('\n')):
262 print(f"| {line}", file=sys.stderr)
264 # We run the subprocess and parse its output line by line, so that we can kill it as soon as it detects a timeout
265 process = subprocess.Popen(shlex.split(execcmd), stdout=subprocess.PIPE,
266 stderr=subprocess.STDOUT, preexec_fn=os.setsid)
267 poll_obj = select.poll()
268 poll_obj.register(process.stdout, select.POLLIN)
271 pgid = os.getpgid(pid) # We need that to forcefully kill subprocesses when leaving
273 if poll_obj.poll(5): # Something to read? Do check the timeout status every 5 sec if not
274 line = process.stdout.readline()
275 # From byte array to string, replacing non-representable strings with question marks
276 line = str(line, errors='replace')
277 output = output + line
278 print(f"| {line}", end='', file=sys.stderr)
279 if read_line_lambda != None:
280 read_line_lambda(line, process)
281 if time.time() - start_time > timeout:
282 with open(f'{cachefile}.timeout', 'w') as outfile:
283 outfile.write(f'{time.time() - start_time} seconds')
285 if process.poll() is not None: # The subprocess ended. Grab all existing output, and return
287 while line != None and line != '':
288 line = process.stdout.readline()
290 # From byte array to string, replacing non-representable strings with question marks
291 line = str(line, errors='replace')
292 output = output + line
293 print(f"| {line}", end='', file=sys.stderr)
297 # We want to clean all forked processes in all cases, no matter whether they are still running (timeout) or supposed to be off. The runners easily get clogged with zombies :(
299 os.killpg(pgid, signal.SIGTERM) # Terminate all forked processes, to make sure it's clean whatever the tool does
300 process.terminate() # No op if it's already stopped but useful on timeouts
301 time.sleep(0.2) # allow some time for the tool to finish its childs
302 os.killpg(pgid, signal.SIGKILL) # Finish 'em all, manually
303 os.kill(pid, signal.SIGKILL) # die! die! die!
304 except ProcessLookupError:
305 pass # OK, it's gone now
307 elapsed = time.time() - start_time
311 status = f"Command killed by signal {-rc}, elapsed time: {elapsed}\n"
313 status = f"Command return code: {rc}, elapsed time: {elapsed}\n"
317 with open(f'{cachefile}.elapsed', 'w') as outfile:
318 outfile.write(str(elapsed))
320 with open(f'{cachefile}.txt', 'w') as outfile:
321 outfile.write(output)
322 with open(f'{cachefile}.md5sum', 'w') as outfile:
323 hashed = hashlib.md5()
324 with open(filename, 'rb') as sourcefile:
325 for chunk in iter(lambda: sourcefile.read(4096), b""):
327 outfile.write(hashed.hexdigest())