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',
71 'IHCallMatching':'InputHazard',
76 'AInvalidParam':'single call',
77 'BResLeak':'single process',
78 # 'BInitFini':'single process',
79 'BReqLifecycle':'single process',
80 'BEpochLifecycle':'single process',
81 'BLocalConcurrency':'single process',
82 'CMatch':'multi-processes',
83 'DRace':'multi-processes',
84 'DMatch':'multi-processes',
85 'DGlobalConcurrency':'multi-processes',
86 'EBufferingHazard':'system',
87 'FOK':'correct executions'
91 'AInvalidParam':'Invalid parameter',
92 'BResLeak':'Resource leak',
93 # 'BInitFini':'MPI call before initialization/after finalization',
94 'BReqLifecycle':'Request lifecycle',
95 'BLocalConcurrency':'Local concurrency',
96 'CMatch':'Parameter matching',
97 'DMatch':"Call ordering",
98 'DRace':'Message race',
99 'DGlobalConcurrency':'Global concurrency',
100 'EBufferingHazard':'Buffering hazard',
101 'FOK':"Correct execution",
103 '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'
106 def parse_one_code(filename):
108 Reads the header of the provided filename, and extract a list of todo item, each of them being a (cmd, expect, test_num) tupple.
109 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.
113 with open(filename, "r") as input_file:
114 state = 0 # 0: before header; 1: in header; 2; after header
116 for line in input_file:
117 if re.match(".*BEGIN_MBI_TESTS.*", line):
121 raise ValueError(f"MBI_TESTS header appears a second time at line {line_num}: \n{line}")
122 elif re.match(".*END_MBI_TESTS.*", line):
126 raise ValueError(f"Unexpected end of MBI_TESTS header at line {line_num}: \n{line}")
127 if state == 1 and re.match(r'\s+\$ ?.*', line):
128 m = re.match(r'\s+\$ ?(.*)', line)
130 nextline = next(input_file)
132 if re.match('[ |]*OK *', nextline):
135 m = re.match('[ |]*ERROR: *(.*)', nextline)
138 f"\n{filename}:{line_num}: MBI parse error: Test not followed by a proper 'ERROR' line:\n{line}{nextline}")
141 if detail not in possible_details:
143 f"\n{filename}:{line_num}: MBI parse error: Detailled outcome {detail} is not one of the allowed ones.")
145 nextline = next(input_file)
146 m = re.match('[ |]*(.*)', nextline)
148 raise ValueError(f"\n{filename}:{line_num}: MBI parse error: Expected diagnostic of the test not found.\n")
149 diagnostic = m.group(1)
151 test = {'filename': filename, 'id': test_num, 'cmd': cmd, 'expect': expect, 'detail': detail, 'diagnostic': diagnostic}
152 res.append(test.copy())
157 raise ValueError(f"MBI_TESTS header not found in file '{filename}'.")
159 raise ValueError(f"MBI_TESTS header not properly ended in file '{filename}'.")
162 raise ValueError(f"No test found in {filename}. Please fix it.")
165 def categorize(tool, toolname, test_id, expected):
166 outcome = tool.parse(test_id)
168 if not os.path.exists(f'{test_id}.elapsed') and not os.path.exists(f'logs/{toolname}/{test_id}.elapsed'):
169 if outcome == 'failure':
172 raise ValueError(f"Invalid test result: {test_id}.txt exists but not {test_id}.elapsed")
174 with open(f'{test_id}.elapsed' if os.path.exists(f'{test_id}.elapsed') else f'logs/{toolname}/{test_id}.elapsed', 'r') as infile:
175 elapsed = infile.read()
177 # Properly categorize this run
178 if outcome == 'timeout':
179 res_category = 'timeout'
181 diagnostic = 'hard timeout'
183 diagnostic = f'timeout after {elapsed} sec'
184 elif outcome == 'failure' or outcome == 'segfault':
185 res_category = 'failure'
186 diagnostic = 'tool error, or test not run'
187 elif outcome == 'UNIMPLEMENTED':
188 res_category = 'unimplemented'
189 diagnostic = 'coverage issue'
190 elif outcome == 'other':
191 res_category = 'other'
192 diagnostic = 'inconclusive run'
193 elif expected == 'OK':
195 res_category = 'TRUE_NEG'
196 diagnostic = 'correctly reported no error'
198 res_category = 'FALSE_POS'
199 diagnostic = 'reported an error in a correct code'
200 elif expected == 'ERROR':
202 res_category = 'FALSE_NEG'
203 diagnostic = 'failed to detect an error'
205 res_category = 'TRUE_POS'
206 diagnostic = 'correctly detected an error'
208 raise ValueError(f"Unexpected expectation: {expected} (must be OK or ERROR)")
210 return (res_category, elapsed, diagnostic, outcome)
213 def run_cmd(buildcmd, execcmd, cachefile, filename, binary, timeout, batchinfo, read_line_lambda=None):
215 Runs the test on need. Returns True if the test was ran, and False if it was cached.
217 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.
220 - 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.
221 - cachefile is the name of the test
222 - filename is the source file containing the code
223 - binary the file name in which to compile the code
224 - batchinfo: something like "1/1" to say that this run is the only batch (see -b parameter of MBI.py)
225 - 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.
227 if os.path.exists(f'{cachefile}.txt') and os.path.exists(f'{cachefile}.elapsed') and os.path.exists(f'{cachefile}.md5sum'):
228 hash_md5 = hashlib.md5()
229 with open(filename, 'rb') as sourcefile:
230 for chunk in iter(lambda: sourcefile.read(4096), b""):
231 hash_md5.update(chunk)
232 newdigest = hash_md5.hexdigest()
233 with open(f'{cachefile}.md5sum', 'r') as md5file:
234 olddigest = md5file.read()
235 #print(f'Old digest: {olddigest}; New digest: {newdigest}')
236 if olddigest == newdigest:
237 print(f" (result cached -- digest: {olddigest})")
239 os.remove(f'{cachefile}.txt')
241 print(f"Wait up to {timeout} seconds")
243 start_time = time.time()
245 output = f"No need to compile {binary}.c (batchinfo:{batchinfo})\n\n"
247 output = f"Compiling {binary}.c (batchinfo:{batchinfo})\n\n"
248 output += f"$ {buildcmd}\n"
250 compil = subprocess.run(buildcmd, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
251 if compil.stdout is not None:
252 output += str(compil.stdout, errors='replace')
253 if compil.returncode != 0:
254 output += f"Compilation of {binary}.c raised an error (retcode: {compil.returncode})"
255 for line in (output.split('\n')):
256 print(f"| {line}", file=sys.stderr)
257 with open(f'{cachefile}.elapsed', 'w') as outfile:
258 outfile.write(str(time.time() - start_time))
259 with open(f'{cachefile}.txt', 'w') as outfile:
260 outfile.write(output)
263 output += f"\n\nExecuting the command\n $ {execcmd}\n"
264 for line in (output.split('\n')):
265 print(f"| {line}", file=sys.stderr)
267 # We run the subprocess and parse its output line by line, so that we can kill it as soon as it detects a timeout
268 process = subprocess.Popen(shlex.split(execcmd), stdout=subprocess.PIPE,
269 stderr=subprocess.STDOUT, preexec_fn=os.setsid)
270 poll_obj = select.poll()
271 poll_obj.register(process.stdout, select.POLLIN)
274 pgid = os.getpgid(pid) # We need that to forcefully kill subprocesses when leaving
276 if poll_obj.poll(5): # Something to read? Do check the timeout status every 5 sec if not
277 line = process.stdout.readline()
278 # From byte array to string, replacing non-representable strings with question marks
279 line = str(line, errors='replace')
280 output = output + line
281 print(f"| {line}", end='', file=sys.stderr)
282 if read_line_lambda != None:
283 read_line_lambda(line, process)
284 if time.time() - start_time > timeout:
285 with open(f'{cachefile}.timeout', 'w') as outfile:
286 outfile.write(f'{time.time() - start_time} seconds')
288 if process.poll() is not None: # The subprocess ended. Grab all existing output, and return
290 while line != None and line != '':
291 line = process.stdout.readline()
293 # From byte array to string, replacing non-representable strings with question marks
294 line = str(line, errors='replace')
295 output = output + line
296 print(f"| {line}", end='', file=sys.stderr)
300 # 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 :(
302 os.killpg(pgid, signal.SIGTERM) # Terminate all forked processes, to make sure it's clean whatever the tool does
303 process.terminate() # No op if it's already stopped but useful on timeouts
304 time.sleep(0.2) # allow some time for the tool to finish its childs
305 os.killpg(pgid, signal.SIGKILL) # Finish 'em all, manually
306 os.kill(pid, signal.SIGKILL) # die! die! die!
307 except ProcessLookupError:
308 pass # OK, it's gone now
310 elapsed = time.time() - start_time
314 status = f"Command killed by signal {-rc}, elapsed time: {elapsed}\n"
316 status = f"Command return code: {rc}, elapsed time: {elapsed}\n"
320 with open(f'{cachefile}.elapsed', 'w') as outfile:
321 outfile.write(str(elapsed))
323 with open(f'{cachefile}.txt', 'w') as outfile:
324 outfile.write(output)
325 with open(f'{cachefile}.md5sum', 'w') as outfile:
326 hashed = hashlib.md5()
327 with open(filename, 'rb') as sourcefile:
328 for chunk in iter(lambda: sourcefile.read(4096), b""):
330 outfile.write(hashed.hexdigest())