diff --git a/llvm_test_script/plugin/Clang Plugin test manual.md b/llvm_test_script/plugin/Clang Plugin test manual.md new file mode 100644 index 0000000000000000000000000000000000000000..ca27ae5cdf72bd11f9a3b367c8e54a0f74abfccd --- /dev/null +++ b/llvm_test_script/plugin/Clang Plugin test manual.md @@ -0,0 +1,232 @@ +# Clang Plugin 测试框架指南 + +Clang Plugin测试框架基于Python Unittest2开发,提供 [Clang Plugins](https://clang.llvm.org/docs/ClangPlugins.html) 端测编译和生成物检查等功能。 + +## 1. 测试框架功能介绍 + +### 1.1 测试框架文件结构 + +``` +--| TestCase # 测试用例模块的目录 + --| test.py # 测试用例模块,其中包含了相关的测试用例 + --| test.cpp # 测试用例使用的c/c++测试程序 +--| runtest.py # 测试框架入口 +--| TestBase.py # 测试用例模块基类,所有测试模块须继承该基类,该基类提供基础的编译和检查功能 +``` + +### 1.2 TestBase 功能介绍 + +该类继承于unittest2.TestCase,以便于能够自动的发现用例和收集统计用例执行结果。 + +该类提供如下方法,便于开发者实现Plugin测试用例: + +#### 1.2.1 断言 + +**功能介绍**:提供基础的测试断言方法 + +**函数原型**:pluginAssert(expr, msg) + +**输入参数**: + +​ expr:断言表达式 + +​ msg:断言表达式不成立时的错误消息 + +**返回值**:无 + +#### 1.2.2 编译 + +**功能介绍**:提供基础的编译方法,可配置不同参数编译Plugin测试相关的生成物 + +**函数原型**:compile(self, clang_path, source_file_path, compile_mode, + +​ output_file_name, output_dir=None, + +​ plugin_name=None, plugin_so_path=None, args='' ) + +**输入参数**: + +​ clang_path: clang路径,该编译器须与Plugin编译器保持一致 + +​ source_file_path: 测试用例使用的c/c++测试程序路径 + +​ compile_mode: 支持以下四个编译模式: + +​ '-cc1':clang `-cc1 `加载plugin功能实现核心编译器功能,选择模式时须传入参数plugin_name和plugin_so_path。 + +​ '-o':编译生成可执行的二进制文件 + +​ '-ll':编译生成中间表示.ll文件 + +​ '-bc':编译生成中间表示.bc文件 + +​ output_file_name: 编译生成物的名称 + +​ output_dir: 存放编译生成物的目录。可以为None,此时编译产物存放在source_file_path同一目录中 + +​ plugin_name: Plugin的注册名称,可以为None,即不加载Plugin + +​ plugin_so_path: Plugin动态库存放路径,可以为None,即不加载Plugin + +​ args: 其他编译参数 + +**返回值**: + +​ CompileResult对象 + +​ returncode:编译执行返回代码,0为正常,非0为异常 + +​ out:编译输出,包含正常输出和错误信息 + + + +**功能介绍**:提供更自由的编译方法 + +**函数原型**:simply_compile(self, clang_path, *args ) + +**输入参数**: + +​ clang_path: clang编译器路径 + +​ args: 编译参数 + +**返回值**: + +​ CompileResult对象 + +​ returncode:编译执行返回代码,0为正常,非0为异常 + +​ out:编译输出,包含正常输出和错误信息 + +#### 1.2.4 通用方法 + +**功能介绍**:根据匹配模式,在对应的文件中统计匹配次数 + +**函数原型**:match_num(self, pattern, file) + +**输入参数**: + +​ pattern: 匹配模式 + +​ file:被检索文件的路径 + +**返回值**:匹配次数 + + + +**功能介绍**:根据匹配模式,检索对应文件,返回匹配的对应行内容 + +**函数原型**:match_lines(self, pattern, file) + +**输入参数**: + +​ pattern: 匹配模式 + +​ file:被检索文件的路径 + +**返回值**:匹配的对应行内容 + +## 2. 测试用例编写指南 + +可参考样例用例模块PrintFunctionNames。 + +> 注意:所有的用例模块须继承TestBase基类 + +上述用例模块是针对Plugin:print-fns的测试,该plugin的功能是输出所有的top-level函数名称。 + +其中测试用例test_01:测试plugin print-fns的功能是否符合预期 + +``` +def test_01(self): + source_file_path = os.path.join(os.path.abspath(__file__), "test_1.cpp") + # Build with plugin print-fns + ret = self.compile(clang_path=self._clang_path, + source_file_path=source_file_path, + output_file_name="", + compile_mode='-cc1', + plugin_name=self._plugin_name, + plugin_so_path=self._plugin_so_path) + # Plugin print-fns will output all top-level function names + expect = ('stdout: ;\n' + 'stderr: top-level-decl: "MyTest"\n' + 'top-level-decl: "secondMemberFun"\n' + 'top-level-decl: "printFirstTest"\n' + 'top-level-decl: "printSecondTest"\n' + 'top-level-decl: "main"\n') + # use pluginAsset to check output + self.pluginAssert(ret.returncode == 0, + 'Command process exit with error.') + self.pluginAssert(ret.out == expect, + 'Terminal output does not meet expectations') +``` + +其中测试用例test_02:测试plugin print-fns是否影响最终生成物可执行程序 + +``` +def test_02(self): + source_file_path = os.path.join(os.path.dirname(__file__), "test_2.cpp") + # build with plugin + ret_load = self.compile(self._clang_path, + source_file_path, + '-o', + 'test_load', + None, + self._plugin_name, + self._plugin_so_path) + # build without plugin + ret_unload = self.compile(self._clang_path, + source_file_path, + '-o', + 'test') + + # compare exe + cmp = filecmp.cmp(os.path.join(os.path.dirname(__file__), "test"), + os.path.join(os.path.dirname(__file__), "test_load"), + shallow = False) + + self.pluginAssert(ret_load.returncode == 0, + 'Command process exit with error.') + self.pluginAssert(ret_unload.returncode == 0, + 'Command process exit with error.') + self.pluginAssert(cmp, 'The binary files are not same.') +``` + +### 2.1 样例运行 + +配置PrintFunctionNames/build.sh中的`LLVM_PROJECT_ROOT`变量值为llvm-project根路径 + +执行如下命令: + +``` +$ python3 ./runtest.py -test-path=./PrintFunctionNames +``` + +> 注意:第一次运行需要编译插件,会比较耗时。 + +## 3. 测试运行指南 + +通过执行runtest.py来执行测试。 + +输入参数: + +​ -test-path:指定路径/用例模组文件/用例名,测试框架会自动发现所有符合条件的测试用例 + +示例: + +1. 指定用例所在文件夹 + +``` +$ python3 ./runtest.py -test-path=./PrintFunctionNames +``` + +2. 指定用例文件 + +``` +$ python3 ./runtest.py -test-path=./PrintFunctionNames/plugintest.py +``` + +3. 指定用例名 + +``` +$ python3 ./runtest.py -test-path=./PrintFunctionNames/plugintest.PluginTest.test_01 +``` diff --git a/llvm_test_script/plugin/PrintFunctionNames/PrintFunctionNames.so b/llvm_test_script/plugin/PrintFunctionNames/PrintFunctionNames.so new file mode 100644 index 0000000000000000000000000000000000000000..d356b24bfc5d38b312cd6c9eedcde7cadeb7fed5 Binary files /dev/null and b/llvm_test_script/plugin/PrintFunctionNames/PrintFunctionNames.so differ diff --git a/llvm_test_script/plugin/PrintFunctionNames/build.sh b/llvm_test_script/plugin/PrintFunctionNames/build.sh new file mode 100644 index 0000000000000000000000000000000000000000..f924969ad13071649b7d680e3353d579397f7890 --- /dev/null +++ b/llvm_test_script/plugin/PrintFunctionNames/build.sh @@ -0,0 +1,6 @@ +CURRENT_DIR=$(cd `dirname $0`;pwd) +LLVM_PROJECT_ROOT="/home/ygz/work" +mkdir ${CURRENT_DIR}/build +cd ${CURRENT_DIR}/build +cmake -DCLANG_BUILD_EXAMPLES=ON -DCLANG_PLUGIN_SUPPORT=ON -DLLVM_ENABLE_PROJECTS="clang" -DCMAKE_BUILD_TYPE=Release ${LLVM_PROJECT_ROOT}/toolchain/llvm-project/llvm +make \ No newline at end of file diff --git a/llvm_test_script/plugin/PrintFunctionNames/plugintest.py b/llvm_test_script/plugin/PrintFunctionNames/plugintest.py new file mode 100644 index 0000000000000000000000000000000000000000..b3462be5716768011ff618dfb20de7a4eb1abb08 --- /dev/null +++ b/llvm_test_script/plugin/PrintFunctionNames/plugintest.py @@ -0,0 +1,69 @@ +import filecmp +import os +import subprocess +from Testbase import TestBase + +class PluginTest(TestBase): + + def setUp(self): + self._compiler_path = os.path.join(os.path.dirname(__file__),'build/bin/clang++') + self._plugin_so_path = os.path.join(os.path.dirname(__file__), "build/lib/PrintFunctionNames.so") + self._plugin_name = 'print-fns' + # if clang++ and PrintFunctionNames.so don't exist, + # run build.sh to build clang++ and PrintFunctionNames.so + if not (os.path.exists(self._compiler_path) and os.path.exists(self._plugin_so_path)): + build_sh = os.path.join(os.path.dirname(__file__),'build.sh') + subprocess.run(['chmod', '+x', build_sh]) + ret = subprocess.run(build_sh, shell=True) + if ret.returncode != 0: + raise self.failureException('Build Error') + + def test_01(self): + source_file_path = os.path.join(os.path.dirname(__file__), "test_1.cpp") + # Build with plugin print-fns + ret = self.compile(compiler_path=self._compiler_path, + source_file_path=source_file_path, + output_file_name="", + compile_mode='-cc1', + plugin_name=self._plugin_name, + plugin_so_path=self._plugin_so_path) + # Plugin print-fns will output all top-level function names + expect = ('stdout: ;\n' + 'stderr: top-level-decl: "MyTest"\n' + 'top-level-decl: "secondMemberFun"\n' + 'top-level-decl: "printFirstTest"\n' + 'top-level-decl: "printSecondTest"\n' + 'top-level-decl: "main"\n') + # use pluginAsset to check output + + self.pluginAssert(ret.returncode == 0, + 'Command process exit with error:' + ret.out) + self.pluginAssert(ret.out == expect, + 'Terminal output does not meet expectations') + + def test_02(self): + source_file_path = os.path.join(os.path.dirname(__file__), "test_2.cpp") + # build with plugin + ret_load = self.compile(self._compiler_path, + source_file_path, + '-o', + 'test_load', + None, + self._plugin_name, + self._plugin_so_path) + # build without plugin + ret_unload = self.compile(self._compiler_path, + source_file_path, + '-o', + 'test') + + # compare exe + cmp = filecmp.cmp(os.path.join(os.path.dirname(__file__), "test"), + os.path.join(os.path.dirname(__file__), "test_load"), + shallow = False) + + self.pluginAssert(ret_load.returncode == 0, + 'Command process exit with error:' + ret_load.out) + self.pluginAssert(ret_unload.returncode == 0, + 'Command process exit with error.' + ret_unload.out) + self.pluginAssert(cmp, 'The binary files are not same.') diff --git a/llvm_test_script/plugin/PrintFunctionNames/test_1.cpp b/llvm_test_script/plugin/PrintFunctionNames/test_1.cpp new file mode 100644 index 0000000000000000000000000000000000000000..14c266f68119cfaf6ac96c5074c3ec9f2ea3b95f --- /dev/null +++ b/llvm_test_script/plugin/PrintFunctionNames/test_1.cpp @@ -0,0 +1,27 @@ +class MyTest{ +public: + void firstMemberFun(){ + } + + void secondMemberFun(); + + void thirdMemberFun(); +}; +void MyTest::secondMemberFun(){ +} + +void printFirstTest(){ +} + +void printSecondTest(); + +int main(){ + MyTest myTestClass; + myTestClass.firstMemberFun(); + myTestClass.secondMemberFun(); + myTestClass.thirdMemberFun(); + printFirstTest(); + printSecondTest(); + + return 0; +} \ No newline at end of file diff --git a/llvm_test_script/plugin/PrintFunctionNames/test_2.cpp b/llvm_test_script/plugin/PrintFunctionNames/test_2.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5bee9d8da8b61a7cb68a51984fe1a62e68311a43 --- /dev/null +++ b/llvm_test_script/plugin/PrintFunctionNames/test_2.cpp @@ -0,0 +1,41 @@ +//test_2.cpp +#include + +class MyTest{ +public: + void firstMemberFun(){ + std::cout << "This is first test member function." << std::endl; + } + + void secondMemberFun(){ + std::cout << "This is second test member function." << std::endl; + } + + void thirdMemberFun(){ + std::cout << "This is third test member function." << std::endl; + } +}; + +void printFirstTest(){ + std::cout << "This is first test function." << std::endl; +} + +void printSecondTest(){ + std::cout << "This is second test function." << std::endl; +} + +void printThirdTest(){ + std::cout << "This is third test function." << std::endl; +} + +int main(){ + MyTest myTestClass; + myTestClass.firstMemberFun(); + myTestClass.secondMemberFun(); + myTestClass.thirdMemberFun(); + printFirstTest(); + printSecondTest(); + printThirdTest(); + + return 0; +} \ No newline at end of file diff --git a/llvm_test_script/plugin/Testbase.py b/llvm_test_script/plugin/Testbase.py new file mode 100644 index 0000000000000000000000000000000000000000..f5aa3f01b26a192b7003520cb715fe4e5285702c --- /dev/null +++ b/llvm_test_script/plugin/Testbase.py @@ -0,0 +1,123 @@ +import subprocess +import unittest2 +import os + +class CompileResult(): + def __init__(self, returncode, stdout, stderr): + self.returncode = returncode + self.out = 'stdout: ' + stdout + ';\n' + 'stderr: ' + stderr + +class TestBase(unittest2.TestCase): + + def pluginAssert(self, expr, msg=None): + """Check that the expression is true.""" + if not expr: + raise self.failureException('PluginTest: ' + msg) + + def compile(self, compiler_path, source_file_path, compile_mode, + output_file_name, output_dir=None, + plugin_name=None, plugin_so_path=None, args=""): + """ + compile_mode:'-cc1'/'-o'/'-ll'/'-bc' + """ + cmd = None + build_without_plugin = (plugin_name == None and plugin_so_path == None) + + if output_dir == None: + output_dir = os.path.realpath(os.path.dirname(source_file_path)) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + elif not os.path.isdir(output_dir): + return CompileResult(1, "", "Invalid output path.") + + output = os.path.join(output_dir, output_file_name) + + if compile_mode == '-cc1': + if build_without_plugin: + return CompileResult(1, "", "Invalid input") + cmd = '%s %s -cc1 -load %s -plugin %s %s' % \ + (compiler_path, args, plugin_so_path, \ + plugin_name, source_file_path) + elif compile_mode == '-o': + if build_without_plugin: + cmd = '%s %s -o %s %s' % \ + (compiler_path, args, output, source_file_path) + else: + cmd = ('%s %s -Xclang -load -Xclang %s ' \ + '-Xclang -add-plugin -Xclang %s -o %s %s') % \ + (compiler_path, args, plugin_so_path, + plugin_name, output, source_file_path) + elif compile_mode == '-ll': + if build_without_plugin: + cmd = '%s %s -emit-llvm -S -o %s %s' % \ + (compiler_path, args, output, source_file_path) + else: + cmd = ('%s %s -Xclang -load -Xclang %s ' \ + '-Xclang -add-plugin -Xclang %s ' \ + '-emit-llvm -S -o %s %s') % \ + (compiler_path, args, plugin_so_path, \ + plugin_name, output, source_file_path) + elif compile_mode == '-bc': + if build_without_plugin: + cmd = '%s %s -emit-llvm -c -o %s %s' % \ + (compiler_path, args, output, source_file_path) + else: + cmd = ('%s %s -Xclang -load -Xclang %s ' \ + '-Xclang -add-plugin -Xclang %s ' \ + '-emit-llvm -c -o %s %s') % \ + (compiler_path, args, plugin_so_path, \ + plugin_name, output, source_file_path) + else: + return CompileResult(1, "", "Invalid compile mode.") + + process_result = subprocess.run(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, + encoding="utf-8") + + ret = CompileResult(process_result.returncode, + process_result.stdout, + process_result.stderr) + return ret + + def match_num(self, pattern, file): + """ + count match number. + """ + cmd = 'grep -c ' + pattern + ' ' + file + ret = subprocess.run(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, + encoding="utf-8") + return int(ret.stdout) + + def match_lines(self, pattern, file): + """ + show the match lines. + """ + cmd = 'grep -n ' + pattern + ' ' + file + ret = subprocess.run(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, + encoding="utf-8") + return ret.stdout + + def simply_compile(self, compiler_path, *args): + cmd = None + if len(args) == 0: + return CompileResult(1, "", "Invalid input") + else: + cmd = '%s %s' % (compiler_path, ' '.join(args)) + process_result = subprocess.run(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, + encoding="utf-8") + + ret = CompileResult(process_result.returncode, + process_result.stdout, + process_result.stderr) + return ret diff --git a/llvm_test_script/plugin/runtest.py b/llvm_test_script/plugin/runtest.py new file mode 100644 index 0000000000000000000000000000000000000000..9e2ab576fe9f7635aa480472cf72ae845ce8ac8a --- /dev/null +++ b/llvm_test_script/plugin/runtest.py @@ -0,0 +1,91 @@ +import argparse +import platform +import unittest2 +import sys +import os + +class RunTest: + args = None + test_dir = None + test_name = None + suite = unittest2.TestSuite() + loader = unittest2.TestLoader() + failureException = Exception + + def __init__(self): + self.parse_args() + self.run_test() + + def parse_args(self): + parser = argparse.ArgumentParser(description='Plugin test.') + parser.add_argument( + '-test-path', + action='store', + default=None, + help='Test case save path and test method name.') + self.args = parser.parse_args() + + def check_args(self): + path_flag = True + if self.args.test_path == None: + path_flag = False + else: + if os.path.isdir(self.args.test_path): # test path is a dir. + self.test_dir = self.args.test_path + else: + if platform.system() == 'Linux': + self.test_dir = self.args.test_path[:self.args.test_path.rfind('/')] + self.test_name = self.args.test_path[(self.args.test_path.rfind('/')+1):] + elif platform.system() == 'Windows': + self.test_dir = self.args.test_path[:self.args.test_path.rfind('\\')] + self.test_name = self.args.test_path[(self.args.test_path.rfind('\\')+1):] + if not os.path.isfile(self.args.test_path): + parts = self.test_name.split('.') + parts_copy = parts[:] + module = None + sys.path.append(self.test_dir) + while parts_copy: + try: + module_name = '.'.join(parts_copy) + module = __import__(module_name) + break + except ImportError: + parts_copy.pop() + if not parts_copy: + # Even the top level import failed. + path_flag = False + parts = parts[len(parts_copy):] + obj = module + for part in parts: + try: + obj = getattr(obj, part) + except AttributeError as e: + # Can't traverse some part of the name. + if getattr(obj, part, None) == None: + path_flag = False + return path_flag + + def discover_cases(self): + if self.test_name == None: + self.test_name = '*.py' + discover = self.loader.discover(self.test_dir, pattern=self.test_name) + elif self.test_name.lower().endswith('.py'): + discover = self.loader.discover(self.test_dir, pattern=self.test_name) + else: + sys.path.append(self.test_dir) + discover = self.loader.loadTestsFromName(self.test_name) + + self.suite.addTest(discover) + + def run_test(self): + if self.check_args(): + self.discover_cases() + runner = unittest2.TextTestRunner(verbosity=2) + runner.run(self.suite) + else: + print('Please input cerrent test case save path.') + +main = RunTest + +if __name__=='__main__': + main() \ No newline at end of file