-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsanity_test.py
145 lines (125 loc) · 5.39 KB
/
sanity_test.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#!/usr/bin/env python3
"""This file looks at all the directories and files available in the repository.
Then it runs them and makes sure no error occurs. We want to make sure that even when
dealii, dealfem, and python_scripts evolve over time, all of our beloved simulations
can be run and reproduced.
"""
import collections
import enum
import logging
import os
from pathlib import Path
import subprocess
import tempfile
from typing import Optional, Sequence
logger = logging.getLogger(__name__)
def find_shebang_executable(path: Path) -> Optional[str]:
"""Reads the first line of a file and if it has a #!, it finds the corresponding
executable to run the program"""
with open(str(path), 'r') as f:
first_line = f.readline()
if first_line.startswith('#!'):
return first_line[2:].strip().split(' ')[-1]
else:
return None
class RunPython:
"""This class looks for files named run.py and runs them. It can also perform
some static analysis on the files.
"""
glob = '**/run.py'
class Directory(enum.Enum):
"""Encapsulated the current directory when running the simulations.
One option is to always run the simulation where the SOURCE of the simulation is.
the other option is to create a TEMP directory and use that to run the simulation.
"""
SOURCE = 0
TEMP = 1
def __init__(self, run_inside_dir: Directory) -> None:
"""
Args:
run_inside_dir: Run simulation inside this type of directory (SOURCE or TEMP)
"""
self.dir_type = run_inside_dir
@staticmethod
def _choose_interpreter(path: Path) -> str:
"""Given a python file, determines which python interpreter to use"""
suggested_interpreter = find_shebang_executable(path)
if suggested_interpreter is not None:
assert 'python' in suggested_interpreter, \
'Invalid python interpreter: ' + suggested_interpreter
return suggested_interpreter
else:
# For now it assumes all scripts with no shebang are python3 scripts
# TODO: remove this assumption about python3
return 'python3'
def test(self, path: Path, timeout: Optional[float] = None) -> bool:
"""Tests a file. If successful, it returns true.
A timeout can be provided to limit the execution time. However, we assume that
whenever the process times out, it is working correctly (although it is a slow
simulation). Thus reaching timeout causes the test to succeeds. This is a little
counter-intuitive."""
interpreter = RunPython._choose_interpreter(path)
if self.dir_type == self.Directory.SOURCE:
working_dir = str(path.parent)
else:
assert self.dir_type == self.Directory.TEMP
temp_dir = tempfile.TemporaryDirectory()
working_dir = str(temp_dir)
# Tell all simulations to run in a single core manner to help debug error messages
additional_env = {'NPROC': '1'}
program_env = collections.ChainMap(os.environ, additional_env)
proc = subprocess.Popen([interpreter, path.name], env=program_env, cwd=working_dir)
try:
proc.wait(timeout=timeout)
except subprocess.TimeoutExpired:
proc.kill()
logger.info('%s timed out after %f seconds. We assume it is working correctly' +
' but it is an expensive task.', path, timeout)
return True
else:
return proc.returncode == 0
finally:
if self.dir_type == self.Directory.TEMP:
temp_dir.cleanup()
class MyPy:
"""Runs mypy on the pyton file to make sure it is correct in terms of typing"""
glob = '**/*.py'
@staticmethod
def test(path: Path, timeout: Optional[float] = None) -> bool:
from mypy import api
output, error, exitcode = api.run([str(path)])
success = exitcode == 0
if not success:
logger.error('MyPy errors for %s', path)
if output:
logger.error(output.strip())
if error:
logger.error(error.strip())
return success
def find_and_test_all(root_dir: Path, file_templates: Sequence,
timeout: Optional[float] = None) -> bool:
"""Run standard sanity tests on all files.
Args:
root_dir: Find all files that satisfy the template in this directory.
file_tempaltes: A list of templates that we tell us how to find files an how to find
files and how to test them.
timeout: The maximum amount of time spent per file per test.
Returns:
Whether all the tests were successful.
"""
success = True
logger.info('Looking for files in %s.', root_dir)
for method in file_templates:
logger.info('Running test method %s in files %s.', method.__class__.__name__, method.glob)
for myfile in root_dir.glob(method.glob):
logger.info('Testing the file %s.', myfile)
result = method.test(myfile, timeout)
success = success and result
if result:
logger.info('File %s succeeded.', myfile)
else:
logger.error('File %s failed.', myfile)
logger.info('=' * 80)
logger.info('All files tested for %s', method.__class__.__name__)
logger.info('*' * 100)
return success