1#!/usr/bin/env python 2# Copyright 2015 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import glob 7import os 8import subprocess 9import sys 10 11 12class ClangPluginTest(object): 13 """Test harness for clang plugins.""" 14 15 def __init__(self, test_base, clang_path, plugin_path, plugin_name, 16 reset_results): 17 """Constructor. 18 19 Args: 20 test_base: Path to the directory containing the tests. 21 clang_path: Path to the clang binary. 22 plugin_path: Optional path to the plugin binary. May be None, such as on 23 Windows, where the plugin is built directly into the clang 24 binary. 25 plugin_name: Name of the plugin. 26 reset_results: If true, resets expected results to the actual test output. 27 """ 28 self._test_base = test_base 29 self._clang_path = clang_path 30 self._plugin_path = plugin_path 31 self._plugin_name = plugin_name 32 self._reset_results = reset_results 33 34 def AddPluginArg(self, clang_cmd, plugin_arg): 35 """Helper to add an argument for the tested plugin.""" 36 clang_cmd.extend(['-Xclang', '-plugin-arg-%s' % self._plugin_name, 37 '-Xclang', plugin_arg]) 38 39 def AdjustClangArguments(self, clang_cmd): 40 """Tests can override this to customize the command line for clang.""" 41 pass 42 43 def Run(self): 44 """Runs the tests. 45 46 The working directory is temporarily changed to self._test_base while 47 running the tests. 48 49 Returns: the number of failing tests. 50 """ 51 print 'Using clang %s...' % self._clang_path 52 print 'Using plugin %s...' % self._plugin_path 53 54 os.chdir(self._test_base) 55 56 clang_cmd = [self._clang_path, '-c', '-std=c++11'] 57 if self._plugin_path: 58 clang_cmd.extend(['-Xclang', '-load', '-Xclang', self._plugin_path]) 59 clang_cmd.extend(['-Xclang', '-add-plugin', '-Xclang', self._plugin_name]) 60 self.AdjustClangArguments(clang_cmd) 61 62 passing = [] 63 failing = [] 64 tests = glob.glob('*.cpp') 65 for test in tests: 66 sys.stdout.write('Testing %s... ' % test) 67 test_name, _ = os.path.splitext(test) 68 69 cmd = clang_cmd[:] 70 try: 71 # Some tests need to run with extra flags. 72 cmd.extend(file('%s.flags' % test_name).read().split()) 73 except IOError: 74 pass 75 cmd.append(test) 76 77 failure_message = self.RunOneTest(test_name, cmd) 78 if failure_message: 79 print 'failed: %s' % failure_message 80 failing.append(test_name) 81 else: 82 print 'passed!' 83 passing.append(test_name) 84 85 print 'Ran %d tests: %d succeeded, %d failed' % ( 86 len(passing) + len(failing), len(passing), len(failing)) 87 for test in failing: 88 print ' %s' % test 89 return len(failing) 90 91 def RunOneTest(self, test_name, cmd): 92 try: 93 actual = subprocess.check_output(cmd, stderr=subprocess.STDOUT) 94 except subprocess.CalledProcessError as e: 95 # Some plugin tests intentionally trigger compile errors, so just ignore 96 # an exit code that indicates failure. 97 actual = e.output 98 except Exception as e: 99 return 'could not execute %s (%s)' % (cmd, e) 100 101 return self.ProcessOneResult(test_name, actual) 102 103 def ProcessOneResult(self, test_name, actual): 104 """Tests can override this for custom result processing.""" 105 # On Windows, clang emits CRLF as the end of line marker. Normalize it to LF 106 # to match posix systems. 107 actual = actual.replace('\r\n', '\n') 108 109 result_file = '%s.txt%s' % (test_name, '' if self._reset_results else 110 '.actual') 111 try: 112 expected = open('%s.txt' % test_name).read() 113 except IOError: 114 open(result_file, 'w').write(actual) 115 return 'no expected file found' 116 117 if expected != actual: 118 open(result_file, 'w').write(actual) 119 error = 'expected and actual differed\n' 120 error += 'Actual:\n' + actual 121 error += 'Expected:\n' + expected 122 return error 123