javatest.py (8367B)
1 #! /usr/bin/env python 2 # encoding: utf-8 3 # Federico Pellegrin, 2019 (fedepell) 4 5 """ 6 Provides Java Unit test support using :py:class:`waflib.Tools.waf_unit_test.utest` 7 task via the **javatest** feature. 8 9 This gives the possibility to run unit test and have them integrated into the 10 standard waf unit test environment. It has been tested with TestNG and JUnit 11 but should be easily expandable to other frameworks given the flexibility of 12 ut_str provided by the standard waf unit test environment. 13 14 The extra takes care also of managing non-java dependencies (ie. C/C++ libraries 15 using JNI or Python modules via JEP) and setting up the environment needed to run 16 them. 17 18 Example usage: 19 20 def options(opt): 21 opt.load('java waf_unit_test javatest') 22 23 def configure(conf): 24 conf.load('java javatest') 25 26 def build(bld): 27 28 [ ... mainprog is built here ... ] 29 30 bld(features = 'javac javatest', 31 srcdir = 'test/', 32 outdir = 'test', 33 sourcepath = ['test'], 34 classpath = [ 'src' ], 35 basedir = 'test', 36 use = ['JAVATEST', 'mainprog'], # mainprog is the program being tested in src/ 37 ut_str = 'java -cp ${CLASSPATH} ${JTRUNNER} ${SRC}', 38 jtest_source = bld.path.ant_glob('test/*.xml'), 39 ) 40 41 42 At command line the CLASSPATH where to find the testing environment and the 43 test runner (default TestNG) that will then be seen in the environment as 44 CLASSPATH_JAVATEST (then used for use) and JTRUNNER and can be used for 45 dependencies and ut_str generation. 46 47 Example configure for TestNG: 48 waf configure --jtpath=/tmp/testng-6.12.jar:/tmp/jcommander-1.71.jar --jtrunner=org.testng.TestNG 49 or as default runner is TestNG: 50 waf configure --jtpath=/tmp/testng-6.12.jar:/tmp/jcommander-1.71.jar 51 52 Example configure for JUnit: 53 waf configure --jtpath=/tmp/junit.jar --jtrunner=org.junit.runner.JUnitCore 54 55 The runner class presence on the system is checked for at configuration stage. 56 57 """ 58 59 import os 60 from waflib import Task, TaskGen, Options, Errors, Utils, Logs 61 from waflib.Tools import ccroot 62 63 JAR_RE = '**/*' 64 65 def _process_use_rec(self, name): 66 """ 67 Recursively process ``use`` for task generator with name ``name``.. 68 Used by javatest_process_use. 69 """ 70 if name in self.javatest_use_not or name in self.javatest_use_seen: 71 return 72 try: 73 tg = self.bld.get_tgen_by_name(name) 74 except Errors.WafError: 75 self.javatest_use_not.add(name) 76 return 77 78 self.javatest_use_seen.append(name) 79 tg.post() 80 81 for n in self.to_list(getattr(tg, 'use', [])): 82 _process_use_rec(self, n) 83 84 @TaskGen.feature('javatest') 85 @TaskGen.after_method('process_source', 'apply_link', 'use_javac_files') 86 def javatest_process_use(self): 87 """ 88 Process the ``use`` attribute which contains a list of task generator names and store 89 paths that later is used to populate the unit test runtime environment. 90 """ 91 self.javatest_use_not = set() 92 self.javatest_use_seen = [] 93 self.javatest_libpaths = [] # strings or Nodes 94 self.javatest_pypaths = [] # strings or Nodes 95 self.javatest_dep_nodes = [] 96 97 names = self.to_list(getattr(self, 'use', [])) 98 for name in names: 99 _process_use_rec(self, name) 100 101 def extend_unique(lst, varlst): 102 ext = [] 103 for x in varlst: 104 if x not in lst: 105 ext.append(x) 106 lst.extend(ext) 107 108 # Collect type specific info needed to construct a valid runtime environment 109 # for the test. 110 for name in self.javatest_use_seen: 111 tg = self.bld.get_tgen_by_name(name) 112 113 # Python-Java embedding crosstools such as JEP 114 if 'py' in tg.features: 115 # Python dependencies are added to PYTHONPATH 116 pypath = getattr(tg, 'install_from', tg.path) 117 118 if 'buildcopy' in tg.features: 119 # Since buildcopy is used we assume that PYTHONPATH in build should be used, 120 # not source 121 extend_unique(self.javatest_pypaths, [pypath.get_bld().abspath()]) 122 123 # Add buildcopy output nodes to dependencies 124 extend_unique(self.javatest_dep_nodes, [o for task in getattr(tg, 'tasks', []) for o in getattr(task, 'outputs', [])]) 125 else: 126 # If buildcopy is not used, depend on sources instead 127 extend_unique(self.javatest_dep_nodes, tg.source) 128 extend_unique(self.javatest_pypaths, [pypath.abspath()]) 129 130 131 if getattr(tg, 'link_task', None): 132 # For tasks with a link_task (C, C++, D et.c.) include their library paths: 133 if not isinstance(tg.link_task, ccroot.stlink_task): 134 extend_unique(self.javatest_dep_nodes, tg.link_task.outputs) 135 extend_unique(self.javatest_libpaths, tg.link_task.env.LIBPATH) 136 137 if 'pyext' in tg.features: 138 # If the taskgen is extending Python we also want to add the interpreter libpath. 139 extend_unique(self.javatest_libpaths, tg.link_task.env.LIBPATH_PYEXT) 140 else: 141 # Only add to libpath if the link task is not a Python extension 142 extend_unique(self.javatest_libpaths, [tg.link_task.outputs[0].parent.abspath()]) 143 144 if 'javac' in tg.features or 'jar' in tg.features: 145 if hasattr(tg, 'jar_task'): 146 # For Java JAR tasks depend on generated JAR 147 extend_unique(self.javatest_dep_nodes, tg.jar_task.outputs) 148 else: 149 # For Java non-JAR ones we need to glob generated files (Java output files are not predictable) 150 if hasattr(tg, 'outdir'): 151 base_node = tg.outdir 152 else: 153 base_node = tg.path.get_bld() 154 155 self.javatest_dep_nodes.extend([dx for dx in base_node.ant_glob(JAR_RE, remove=False, quiet=True)]) 156 157 158 159 @TaskGen.feature('javatest') 160 @TaskGen.after_method('apply_java', 'use_javac_files', 'set_classpath', 'javatest_process_use') 161 def make_javatest(self): 162 """ 163 Creates a ``utest`` task with a populated environment for Java Unit test execution 164 165 """ 166 tsk = self.create_task('utest') 167 tsk.set_run_after(self.javac_task) 168 169 # Dependencies from recursive use analysis 170 tsk.dep_nodes.extend(self.javatest_dep_nodes) 171 172 # Put test input files as waf_unit_test relies on that for some prints and log generation 173 # If jtest_source is there, this is specially useful for passing XML for TestNG 174 # that contain test specification, use that as inputs, otherwise test sources 175 if getattr(self, 'jtest_source', None): 176 tsk.inputs = self.to_nodes(self.jtest_source) 177 else: 178 if self.javac_task.srcdir[0].exists(): 179 tsk.inputs = self.javac_task.srcdir[0].ant_glob('**/*.java', remove=False) 180 181 if getattr(self, 'ut_str', None): 182 self.ut_run, lst = Task.compile_fun(self.ut_str, shell=getattr(self, 'ut_shell', False)) 183 tsk.vars = lst + tsk.vars 184 185 if getattr(self, 'ut_cwd', None): 186 if isinstance(self.ut_cwd, str): 187 # we want a Node instance 188 if os.path.isabs(self.ut_cwd): 189 self.ut_cwd = self.bld.root.make_node(self.ut_cwd) 190 else: 191 self.ut_cwd = self.path.make_node(self.ut_cwd) 192 else: 193 self.ut_cwd = self.bld.bldnode 194 195 # Get parent CLASSPATH and add output dir of test, we run from wscript dir 196 # We have to change it from list to the standard java -cp format (: separated) 197 tsk.env.CLASSPATH = ':'.join(self.env.CLASSPATH) + ':' + self.outdir.abspath() 198 199 if not self.ut_cwd.exists(): 200 self.ut_cwd.mkdir() 201 202 if not hasattr(self, 'ut_env'): 203 self.ut_env = dict(os.environ) 204 def add_paths(var, lst): 205 # Add list of paths to a variable, lst can contain strings or nodes 206 lst = [ str(n) for n in lst ] 207 Logs.debug("ut: %s: Adding paths %s=%s", self, var, lst) 208 self.ut_env[var] = os.pathsep.join(lst) + os.pathsep + self.ut_env.get(var, '') 209 210 add_paths('PYTHONPATH', self.javatest_pypaths) 211 212 if Utils.is_win32: 213 add_paths('PATH', self.javatest_libpaths) 214 elif Utils.unversioned_sys_platform() == 'darwin': 215 add_paths('DYLD_LIBRARY_PATH', self.javatest_libpaths) 216 add_paths('LD_LIBRARY_PATH', self.javatest_libpaths) 217 else: 218 add_paths('LD_LIBRARY_PATH', self.javatest_libpaths) 219 220 def configure(ctx): 221 cp = ctx.env.CLASSPATH or '.' 222 if getattr(Options.options, 'jtpath', None): 223 ctx.env.CLASSPATH_JAVATEST = getattr(Options.options, 'jtpath').split(':') 224 cp += ':' + getattr(Options.options, 'jtpath') 225 226 if getattr(Options.options, 'jtrunner', None): 227 ctx.env.JTRUNNER = getattr(Options.options, 'jtrunner') 228 229 if ctx.check_java_class(ctx.env.JTRUNNER, with_classpath=cp): 230 ctx.fatal('Could not run test class %r' % ctx.env.JTRUNNER) 231 232 def options(opt): 233 opt.add_option('--jtpath', action='store', default='', dest='jtpath', 234 help='Path to jar(s) needed for javatest execution, colon separated, if not in the system CLASSPATH') 235 opt.add_option('--jtrunner', action='store', default='org.testng.TestNG', dest='jtrunner', 236 help='Class to run javatest test [default: org.testng.TestNG]') 237