#!/usr/bin/env python # Copyright 2019 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. from __future__ import print_function import glob import argparse import os import subprocess import sys import shutil script_dir = os.path.dirname(os.path.realpath(__file__)) tool_dir = os.path.abspath(os.path.join(script_dir, '../../pylib')) sys.path.insert(0, tool_dir) from clang import plugin_testing class StackMapTest(plugin_testing.ClangPluginTest): """Test harness for stack map artefact.""" def __init__(self, test_base, llvm_bin_path, libgc_path, ident_sp_pass_path, reg_gc_pass_path,): self._test_base = test_base self._llvm_bin_path = llvm_bin_path self._libgc_path = libgc_path self._ident_sp_pass_path = ident_sp_pass_path self._reg_gc_pass_path = reg_gc_pass_path self._clang_path = os.path.join(llvm_bin_path, 'clang++') self._opt_path = os.path.join(llvm_bin_path, 'opt') self._llc_path = os.path.join(llvm_bin_path, 'llc') self._out_dir = os.path.join( os.path.dirname(os.path.realpath(__file__)), 'out') def build_commands(self, test_name): ll_filename = os.path.join(self._out_dir, "%s.ll" % test_name) ll_with_gc_filename = os.path.join( self._out_dir, "%s_optimised.ll" % test_name) asm_filename = os.path.join(self._out_dir, "%s.s" % test_name) obj_filename = os.path.join(self._out_dir, "%s.o" % test_name) bin_name = os.path.join(self._out_dir, "%s.out" % test_name) # Run the clang++ frontend with -O2 but stop after emitting the IR. There # is a bug in clang which prevents us from running GC related IR phases # directly from the frontend and requires us to split it into multiple # build phases. clang_cmd = [ self._clang_path, '-std=c++14', '-fno-omit-frame-pointer', '-I../', '-Xclang', '-load', '-Xclang', self._ident_sp_pass_path, '-O2', '-S', '-emit-llvm', '-o', ll_filename, '%s.cpp' % test_name ] # Run two passes on the IR. The first selects which functions will be # safepointed, the second inserts statepoint relocation sequences and ends # up in stack maps being generated during the lowering phase. opt_cmd = [ self._opt_path, '-load=%s' % self._reg_gc_pass_path, '-register-gc-fns', '-rewrite-statepoints-for-gc', '-S', '-o', ll_with_gc_filename, ll_filename ] # Note: We must ensure each stage of lowering disables omit frame pointer # optimisation llc_cmd = [ self._llc_path, ll_with_gc_filename, '--frame-pointer=all', '-o', asm_filename ] # The next two stages are required because ToT LLVM emits stackmaps which # are local only to their object file. In a somewhat hacky fix, we # globalise this symbol so that it can be used by the independent GC # runtime library. make_native = [ self._clang_path, '-c', '-o', obj_filename, asm_filename, ] obj_copy = [ 'objcopy', '--globalize-symbol=__LLVM_StackMaps', obj_filename ] # Link the GC runtime and create target executable link_cmd = [ self._clang_path, obj_filename, '-fno-omit-frame-pointer', self._libgc_path, '-o', bin_name ] run_cmd = ['%s' % bin_name] return [ clang_cmd, opt_cmd, llc_cmd, make_native, obj_copy, link_cmd, run_cmd ] def Run(self): """Runs the tests. The working directory is temporarily changed to self._test_base while running the tests. Returns: the number of failing tests. """ print('Using llvm tools in %s...' % self._llvm_bin_path) os.chdir(self._test_base) passing = [] failing = [] tests = glob.glob('*.cpp') # Delete out directory if it already exists if (os.path.exists(self._out_dir)): shutil.rmtree(self._out_dir) os.mkdir(self._out_dir) for test in tests: sys.stdout.write('Testing %s...' % test) test_name, _ = os.path.splitext(test) cmds = self.build_commands(test_name) failure_message = self.RunOneTest(test_name, cmds) if failure_message: print('\n\tfailed: %s' % failure_message) failing.append(test_name) else: print('\tpassed!') passing.append(test_name) print('Ran %d tests: %d succeeded, %d failed' % ( len(passing) + len(failing), len(passing), len(failing))) for test in failing: print(' %s' % test) return len(failing) def RunOneTest(self, test_name, cmds): for cmd in cmds: try: failure_message = "" subprocess.check_output(cmd, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: failure_message = e.output break except Exception as e: return 'could not execute %s (%s)' % (cmd, e) return self.ProcessOneResult(test_name, failure_message) def ProcessOneResult(self, test_name, failure_message): if failure_message: return failure_message.replace('\r\n', '\n') def main(): parser = argparse.ArgumentParser() parser.add_argument( 'llvm_bin_path', help='The path to the llvm tools bin dir.') parser.add_argument('libgc_path', help='The path to the runtime gc library.') parser.add_argument('identify_safepoints_path', help='The path to the identify safepoints IR pass.') parser.add_argument('reg_gc_fns_path', help='The path to the register GC functions IR pass.') args = parser.parse_args() return StackMapTest( os.path.dirname(os.path.realpath(__file__)), args.llvm_bin_path, args.libgc_path, args.identify_safepoints_path, args.reg_gc_fns_path, ).Run() if __name__ == '__main__': sys.exit(main())