run_tests.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. #!/usr/bin/env python3
  2. # Copyright 2012-2016 The Meson development team
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. # Unless required by applicable law or agreed to in writing, software
  8. # distributed under the License is distributed on an "AS IS" BASIS,
  9. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. # See the License for the specific language governing permissions and
  11. # limitations under the License.
  12. from glob import glob
  13. import os, subprocess, shutil, sys, signal
  14. from io import StringIO
  15. from ast import literal_eval
  16. import sys, tempfile
  17. from mesonbuild import environment
  18. from mesonbuild import mesonlib
  19. from mesonbuild import mlog
  20. from mesonbuild import mesonmain
  21. from mesonbuild.mesonlib import stringlistify
  22. from mesonbuild.scripts import meson_test, meson_benchmark
  23. import argparse
  24. import xml.etree.ElementTree as ET
  25. import time
  26. import multiprocessing
  27. import concurrent.futures as conc
  28. from mesonbuild.coredata import backendlist
  29. class TestResult:
  30. def __init__(self, msg, stdo, stde, mlog, conftime=0, buildtime=0, testtime=0):
  31. self.msg = msg
  32. self.stdo = stdo
  33. self.stde = stde
  34. self.mlog = mlog
  35. self.conftime = conftime
  36. self.buildtime = buildtime
  37. self.testtime = testtime
  38. class AutoDeletedDir():
  39. def __init__(self, d):
  40. self.dir = d
  41. def __enter__(self):
  42. os.makedirs(self.dir, exist_ok=True)
  43. return self.dir
  44. def __exit__(self, _type, value, traceback):
  45. # On Windows, shutil.rmtree fails sometimes, because 'the directory is not empty'.
  46. # Retrying fixes this.
  47. # That's why we don't use tempfile.TemporaryDirectory, but wrap the deletion in the AutoDeletedDir class.
  48. retries = 5
  49. for i in range(0, retries):
  50. try:
  51. shutil.rmtree(self.dir)
  52. return
  53. except OSError:
  54. if i == retries-1:
  55. raise
  56. time.sleep(0.1 * (2**i))
  57. passing_tests = 0
  58. failing_tests = 0
  59. skipped_tests = 0
  60. failing_logs = []
  61. print_debug = 'MESON_PRINT_TEST_OUTPUT' in os.environ
  62. meson_command = os.path.join(os.getcwd(), 'meson')
  63. if not os.path.exists(meson_command):
  64. meson_command += '.py'
  65. if not os.path.exists(meson_command):
  66. raise RuntimeError('Could not find main Meson script to run.')
  67. class StopException(Exception):
  68. def __init__(self):
  69. super().__init__('Stopped by user')
  70. stop = False
  71. def stop_handler(signal, frame):
  72. global stop
  73. stop = True
  74. signal.signal(signal.SIGINT, stop_handler)
  75. signal.signal(signal.SIGTERM, stop_handler)
  76. #unity_flags = ['--unity']
  77. unity_flags = []
  78. backend_flags = None
  79. compile_commands = None
  80. test_commands = None
  81. install_commands = None
  82. def setup_commands(backend):
  83. global backend_flags, compile_commands, test_commands, install_commands
  84. msbuild_exe = shutil.which('msbuild')
  85. if backend == 'vs2010' or (backend is None and msbuild_exe is not None):
  86. backend_flags = ['--backend=vs2010']
  87. compile_commands = ['msbuild']
  88. test_commands = ['msbuild', 'RUN_TESTS.vcxproj']
  89. install_commands = []
  90. elif backend == 'vs2015':
  91. backend_flags = ['--backend=vs2015']
  92. compile_commands = ['msbuild']
  93. test_commands = ['msbuild', 'RUN_TESTS.vcxproj']
  94. install_commands = []
  95. elif backend == 'xcode' or (backend is None and mesonlib.is_osx()):
  96. backend_flags = ['--backend=xcode']
  97. compile_commands = ['xcodebuild']
  98. test_commands = ['xcodebuild', '-target', 'RUN_TESTS']
  99. install_commands = []
  100. else:
  101. backend_flags = []
  102. ninja_command = environment.detect_ninja()
  103. if ninja_command is None:
  104. raise RuntimeError('Could not find Ninja v1.6 or newer')
  105. if print_debug:
  106. compile_commands = [ninja_command, '-v']
  107. else:
  108. compile_commands = [ninja_command]
  109. compile_commands += ['-w', 'dupbuild=err']
  110. test_commands = [ninja_command, 'test', 'benchmark']
  111. install_commands = [ninja_command, 'install']
  112. def get_relative_files_list_from_dir(fromdir):
  113. paths = []
  114. for (root, _, files) in os.walk(fromdir):
  115. reldir = os.path.relpath(root, start=fromdir)
  116. for f in files:
  117. path = os.path.join(reldir, f).replace('\\', '/')
  118. if path.startswith('./'):
  119. path = path[2:]
  120. paths.append(path)
  121. return paths
  122. def platform_fix_exe_name(fname):
  123. if not fname.endswith('?exe'):
  124. return fname
  125. fname = fname[:-4]
  126. if mesonlib.is_windows():
  127. return fname + '.exe'
  128. return fname
  129. def validate_install(srcdir, installdir):
  130. # List of installed files
  131. info_file = os.path.join(srcdir, 'installed_files.txt')
  132. # If this exists, the test does not install any other files
  133. noinst_file = 'usr/no-installed-files'
  134. expected = {}
  135. found = {}
  136. ret_msg = ''
  137. # Generate list of expected files
  138. if os.path.exists(os.path.join(installdir, noinst_file)):
  139. expected[noinst_file] = False
  140. elif os.path.exists(info_file):
  141. with open(info_file) as f:
  142. for line in f:
  143. expected[platform_fix_exe_name(line.strip())] = False
  144. # Check if expected files were found
  145. for fname in expected:
  146. if os.path.exists(os.path.join(installdir, fname)):
  147. expected[fname] = True
  148. for (fname, found) in expected.items():
  149. if not found:
  150. ret_msg += 'Expected file {0} missing.\n'.format(fname)
  151. # Check if there are any unexpected files
  152. found = get_relative_files_list_from_dir(installdir)
  153. for fname in found:
  154. if fname not in expected and not fname.endswith('.pdb'):
  155. ret_msg += 'Extra file {0} found.\n'.format(fname)
  156. return ret_msg
  157. def log_text_file(logfile, testdir, stdo, stde):
  158. global stop, executor, futures
  159. logfile.write('%s\nstdout\n\n---\n' % testdir)
  160. logfile.write(stdo)
  161. logfile.write('\n\n---\n\nstderr\n\n---\n')
  162. logfile.write(stde)
  163. logfile.write('\n\n---\n\n')
  164. if print_debug:
  165. print(stdo)
  166. print(stde, file=sys.stderr)
  167. if stop:
  168. print("Aborting..")
  169. for f in futures:
  170. f[2].cancel()
  171. executor.shutdown()
  172. raise StopException()
  173. def run_configure_inprocess(commandlist):
  174. old_stdout = sys.stdout
  175. sys.stdout = mystdout = StringIO()
  176. old_stderr = sys.stderr
  177. sys.stderr = mystderr = StringIO()
  178. try:
  179. returncode = mesonmain.run(commandlist[0], commandlist[1:])
  180. finally:
  181. sys.stdout = old_stdout
  182. sys.stderr = old_stderr
  183. return (returncode, mystdout.getvalue(), mystderr.getvalue())
  184. def run_test_inprocess(testdir):
  185. old_stdout = sys.stdout
  186. sys.stdout = mystdout = StringIO()
  187. old_stderr = sys.stderr
  188. sys.stderr = mystderr = StringIO()
  189. old_cwd = os.getcwd()
  190. os.chdir(testdir)
  191. try:
  192. returncode_test = meson_test.run(['meson-private/meson_test_setup.dat'])
  193. returncode_benchmark = meson_benchmark.run(['meson-private/meson_benchmark_setup.dat'])
  194. finally:
  195. sys.stdout = old_stdout
  196. sys.stderr = old_stderr
  197. os.chdir(old_cwd)
  198. return (max(returncode_test, returncode_benchmark), mystdout.getvalue(), mystderr.getvalue())
  199. def parse_test_args(testdir):
  200. args = []
  201. try:
  202. with open(os.path.join(testdir, 'test_args.txt'), 'r') as f:
  203. content = f.read()
  204. try:
  205. args = literal_eval(content)
  206. except Exception:
  207. raise Exception('Malformed test_args file.')
  208. args = stringlistify(args)
  209. except FileNotFoundError:
  210. pass
  211. return args
  212. def run_test(skipped, testdir, extra_args, flags, compile_commands, install_commands, should_succeed):
  213. if skipped:
  214. return None
  215. with AutoDeletedDir(tempfile.mkdtemp(prefix='b ', dir='.')) as build_dir:
  216. with AutoDeletedDir(tempfile.mkdtemp(prefix='i ', dir=os.getcwd())) as install_dir:
  217. try:
  218. return _run_test(testdir, build_dir, install_dir, extra_args, flags, compile_commands, install_commands, should_succeed)
  219. finally:
  220. mlog.shutdown() # Close the log file because otherwise Windows wets itself.
  221. def _run_test(testdir, test_build_dir, install_dir, extra_args, flags, compile_commands, install_commands, should_succeed):
  222. test_args = parse_test_args(testdir)
  223. gen_start = time.time()
  224. gen_command = [meson_command, '--prefix', '/usr', '--libdir', 'lib', testdir, test_build_dir]\
  225. + flags + test_args + extra_args
  226. (returncode, stdo, stde) = run_configure_inprocess(gen_command)
  227. try:
  228. logfile = os.path.join(test_build_dir, 'meson-logs/meson-log.txt')
  229. with open(logfile, errors='ignore') as f:
  230. mesonlog = f.read()
  231. except Exception:
  232. mesonlog = 'No meson-log.txt found.'
  233. gen_time = time.time() - gen_start
  234. if not should_succeed:
  235. if returncode != 0:
  236. return TestResult('', stdo, stde, mesonlog, gen_time)
  237. return TestResult('Test that should have failed succeeded', stdo, stde, mesonlog, gen_time)
  238. if returncode != 0:
  239. return TestResult('Generating the build system failed.', stdo, stde, mesonlog, gen_time)
  240. if 'msbuild' in compile_commands[0]:
  241. sln_name = glob(os.path.join(test_build_dir, '*.sln'))[0]
  242. comp = compile_commands + [os.path.split(sln_name)[-1]]
  243. else:
  244. comp = compile_commands
  245. build_start = time.time()
  246. pc = subprocess.Popen(comp, cwd=test_build_dir,
  247. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  248. (o, e) = pc.communicate()
  249. build_time = time.time() - build_start
  250. stdo += o.decode(sys.stdout.encoding)
  251. stde += e.decode(sys.stdout.encoding)
  252. if pc.returncode != 0:
  253. return TestResult('Compiling source code failed.', stdo, stde, mesonlog, gen_time, build_time)
  254. test_start = time.time()
  255. # Note that we don't test that running e.g. 'ninja test' actually
  256. # works. One hopes that this is a common enough happening that
  257. # it is picked up immediately on development.
  258. (returncode, tstdo, tstde) = run_test_inprocess(test_build_dir)
  259. test_time = time.time() - test_start
  260. stdo += tstdo
  261. stde += tstde
  262. if returncode != 0:
  263. return TestResult('Running unit tests failed.', stdo, stde, mesonlog, gen_time, build_time, test_time)
  264. if len(install_commands) == 0:
  265. return TestResult('', '', '', gen_time, build_time, test_time)
  266. else:
  267. env = os.environ.copy()
  268. env['DESTDIR'] = install_dir
  269. pi = subprocess.Popen(install_commands, cwd=test_build_dir, env=env,
  270. stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  271. (o, e) = pi.communicate()
  272. stdo += o.decode(sys.stdout.encoding)
  273. stde += e.decode(sys.stdout.encoding)
  274. if pi.returncode != 0:
  275. return TestResult('Running install failed.', stdo, stde, mesonlog, gen_time, build_time, test_time)
  276. return TestResult(validate_install(testdir, install_dir), stdo, stde, mesonlog, gen_time, build_time, test_time)
  277. def gather_tests(testdir):
  278. tests = [t.replace('\\', '/').split('/', 2)[2] for t in glob(os.path.join(testdir, '*'))]
  279. testlist = [(int(t.split()[0]), t) for t in tests]
  280. testlist.sort()
  281. tests = [os.path.join(testdir, t[1]) for t in testlist]
  282. return tests
  283. def have_d_compiler():
  284. if shutil.which("ldc2"):
  285. return True
  286. elif shutil.which("ldc"):
  287. return True
  288. elif shutil.which("gdc"):
  289. return True
  290. elif shutil.which("dmd"):
  291. return True
  292. return False
  293. def detect_tests_to_run():
  294. all_tests = []
  295. all_tests.append(('common', gather_tests('test cases/common'), False))
  296. all_tests.append(('failing', gather_tests('test cases/failing'), False))
  297. all_tests.append(('prebuilt', gather_tests('test cases/prebuilt'), False))
  298. all_tests.append(('platform-osx', gather_tests('test cases/osx'), False if mesonlib.is_osx() else True))
  299. all_tests.append(('platform-windows', gather_tests('test cases/windows'), False if mesonlib.is_windows() else True))
  300. all_tests.append(('platform-linux', gather_tests('test cases/linuxlike'), False if not (mesonlib.is_osx() or mesonlib.is_windows()) else True))
  301. all_tests.append(('framework', gather_tests('test cases/frameworks'), False if not mesonlib.is_osx() and not mesonlib.is_windows() else True))
  302. all_tests.append(('java', gather_tests('test cases/java'), False if not mesonlib.is_osx() and shutil.which('javac') else True))
  303. all_tests.append(('C#', gather_tests('test cases/csharp'), False if shutil.which('mcs') else True))
  304. all_tests.append(('vala', gather_tests('test cases/vala'), False if shutil.which('valac') else True))
  305. all_tests.append(('rust', gather_tests('test cases/rust'), False if shutil.which('rustc') else True))
  306. all_tests.append(('d', gather_tests('test cases/d'), False if have_d_compiler() else True))
  307. all_tests.append(('objective c', gather_tests('test cases/objc'), False if not mesonlib.is_windows() else True))
  308. all_tests.append(('fortran', gather_tests('test cases/fortran'), False if shutil.which('gfortran') else True))
  309. all_tests.append(('swift', gather_tests('test cases/swift'), False if shutil.which('swiftc') else True))
  310. all_tests.append(('python3', gather_tests('test cases/python3'), False if shutil.which('python3') else True))
  311. return all_tests
  312. def run_tests(extra_args):
  313. global passing_tests, failing_tests, stop, executor, futures
  314. all_tests = detect_tests_to_run()
  315. logfile = open('meson-test-run.txt', 'w', encoding="utf_8")
  316. junit_root = ET.Element('testsuites')
  317. conf_time = 0
  318. build_time = 0
  319. test_time = 0
  320. executor = conc.ProcessPoolExecutor(max_workers=multiprocessing.cpu_count())
  321. for name, test_cases, skipped in all_tests:
  322. current_suite = ET.SubElement(junit_root, 'testsuite', {'name' : name, 'tests' : str(len(test_cases))})
  323. if skipped:
  324. print('\nNot running %s tests.\n' % name)
  325. else:
  326. print('\nRunning %s tests.\n' % name)
  327. futures = []
  328. for t in test_cases:
  329. # Jenkins screws us over by automatically sorting test cases by name
  330. # and getting it wrong by not doing logical number sorting.
  331. (testnum, testbase) = os.path.split(t)[-1].split(' ', 1)
  332. testname = '%.3d %s' % (int(testnum), testbase)
  333. result = executor.submit(run_test, skipped, t, extra_args, unity_flags + backend_flags, compile_commands, install_commands, name != 'failing')
  334. futures.append((testname, t, result))
  335. for (testname, t, result) in futures:
  336. result = result.result()
  337. if result is None:
  338. print('Skipping:', t)
  339. current_test = ET.SubElement(current_suite, 'testcase', {'name' : testname,
  340. 'classname' : name})
  341. ET.SubElement(current_test, 'skipped', {})
  342. global skipped_tests
  343. skipped_tests += 1
  344. else:
  345. without_install = "" if len(install_commands) > 0 else " (without install)"
  346. if result.msg != '':
  347. print('Failed test%s: %s' % (without_install, t))
  348. print('Reason:', result.msg)
  349. failing_tests += 1
  350. failing_logs.append(result.mlog)
  351. else:
  352. print('Succeeded test%s: %s' % (without_install, t))
  353. passing_tests += 1
  354. conf_time += result.conftime
  355. build_time += result.buildtime
  356. test_time += result.testtime
  357. total_time = conf_time + build_time + test_time
  358. log_text_file(logfile, t, result.stdo, result.stde)
  359. current_test = ET.SubElement(current_suite, 'testcase', {'name' : testname,
  360. 'classname' : name,
  361. 'time' : '%.3f' % total_time})
  362. if result.msg != '':
  363. ET.SubElement(current_test, 'failure', {'message' : result.msg})
  364. stdoel = ET.SubElement(current_test, 'system-out')
  365. stdoel.text = result.stdo
  366. stdeel = ET.SubElement(current_test, 'system-err')
  367. stdeel.text = result.stde
  368. print("\nTotal configuration time: %.2fs" % conf_time)
  369. print("Total build time: %.2fs" % build_time)
  370. print("Total test time: %.2fs" % test_time)
  371. ET.ElementTree(element=junit_root).write('meson-test-run.xml', xml_declaration=True, encoding='UTF-8')
  372. def check_file(fname):
  373. linenum = 1
  374. with open(fname, 'rb') as f:
  375. lines = f.readlines()
  376. for line in lines:
  377. if b'\t' in line:
  378. print("File %s contains a literal tab on line %d. Only spaces are permitted." % (fname, linenum))
  379. sys.exit(1)
  380. if b'\r' in line:
  381. print("File %s contains DOS line ending on line %d. Only unix-style line endings are permitted." % (fname, linenum))
  382. sys.exit(1)
  383. linenum += 1
  384. def check_format():
  385. for (root, _, files) in os.walk('.'):
  386. for file in files:
  387. if file.endswith('.py') or file.endswith('.build') or file == 'meson_options.txt':
  388. fullname = os.path.join(root, file)
  389. check_file(fullname)
  390. def pbcompile(compiler, source, objectfile):
  391. if compiler == 'cl':
  392. cmd = [compiler, '/nologo', '/Fo'+objectfile, '/c', source]
  393. else:
  394. cmd = [compiler, '-c', source, '-o', objectfile]
  395. subprocess.check_call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  396. def generate_pb_object(compiler, object_suffix):
  397. source = 'test cases/prebuilt/1 object/source.c'
  398. objectfile = 'test cases/prebuilt/1 object/prebuilt.' + object_suffix
  399. pbcompile(compiler, source, objectfile)
  400. return objectfile
  401. def generate_pb_static(compiler, object_suffix, static_suffix):
  402. source = 'test cases/prebuilt/2 static/libdir/best.c'
  403. objectfile = 'test cases/prebuilt/2 static/libdir/best.' + object_suffix
  404. stlibfile = 'test cases/prebuilt/2 static/libdir/libbest.' + static_suffix
  405. pbcompile(compiler, source, objectfile)
  406. if compiler == 'cl':
  407. linker = ['lib', '/NOLOGO', '/OUT:' + stlibfile, objectfile]
  408. else:
  409. linker = ['ar', 'csr', stlibfile, objectfile]
  410. subprocess.check_call(linker)
  411. os.unlink(objectfile)
  412. return stlibfile
  413. def generate_prebuilt():
  414. static_suffix = 'a'
  415. if shutil.which('cl'):
  416. compiler = 'cl'
  417. static_suffix = 'lib'
  418. elif shutil.which('cc'):
  419. compiler = 'cc'
  420. elif shutil.which('gcc'):
  421. compiler = 'gcc'
  422. else:
  423. raise RuntimeError("Could not find C compiler.")
  424. if mesonlib.is_windows():
  425. object_suffix = 'obj'
  426. else:
  427. object_suffix = 'o'
  428. objectfile = generate_pb_object(compiler, object_suffix)
  429. stlibfile = generate_pb_static(compiler, object_suffix, static_suffix)
  430. return (objectfile, stlibfile)
  431. if __name__ == '__main__':
  432. parser = argparse.ArgumentParser(description="Run the test suite of Meson.")
  433. parser.add_argument('extra_args', nargs='*',
  434. help='arguments that are passed directly to Meson (remember to have -- before these).')
  435. parser.add_argument('--backend', default=None, dest='backend',
  436. choices = backendlist)
  437. options = parser.parse_args()
  438. setup_commands(options.backend)
  439. script_dir = os.path.split(__file__)[0]
  440. if script_dir != '':
  441. os.chdir(script_dir)
  442. check_format()
  443. pbfiles = generate_prebuilt()
  444. try:
  445. run_tests(options.extra_args)
  446. except StopException:
  447. pass
  448. for f in pbfiles:
  449. os.unlink(f)
  450. print('\nTotal passed tests:', passing_tests)
  451. print('Total failed tests:', failing_tests)
  452. print('Total skipped tests:', skipped_tests)
  453. if failing_tests > 0 and ('TRAVIS' in os.environ or 'APPVEYOR' in os.environ):
  454. print('\nMesonlogs of failing tests\n')
  455. for l in failing_logs:
  456. print(l, '\n')
  457. sys.exit(failing_tests)