1 /*
   2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 import java.lang.reflect.Field;
  25 import java.lang.reflect.Method;
  26 import java.io.IOException;
  27 import java.io.PrintWriter;
  28 import java.io.StringWriter;
  29 import java.net.URL;
  30 import java.net.URLClassLoader;
  31 import java.nicl.metadata.Header;
  32 import java.nicl.metadata.LibraryDependencies;
  33 import java.nio.file.Path;
  34 import java.nio.file.Paths;
  35 import java.nio.file.Files;
  36 import java.util.Arrays;
  37 import java.util.Optional;
  38 import java.util.spi.ToolProvider;
  39 import org.testng.annotations.Test;
  40 
  41 import static org.testng.Assert.assertEquals;
  42 import static org.testng.Assert.assertNotEquals;
  43 import static org.testng.Assert.assertNotNull;
  44 import static org.testng.Assert.assertNull;
  45 import static org.testng.Assert.assertTrue;
  46 import static org.testng.Assert.assertFalse;
  47 
  48 /*
  49  * @test
  50  * @modules jdk.jextract
  51  * @build JextractToolProviderTest
  52  * @run testng/othervm -Duser.language=en JextractToolProviderTest
  53  */
  54 public class JextractToolProviderTest {
  55     private static final ToolProvider JEXTRACT_TOOL = ToolProvider.findFirst("jextract")
  56         .orElseThrow(() ->
  57             new RuntimeException("jextract tool not found")
  58         );
  59 
  60     private static String testSrcDir = System.getProperty("test.src", ".");
  61     private static String testClassesDir = System.getProperty("test.classes", ".");
  62 
  63     private static Path getFilePath(String dir, String fileName) {
  64         return Paths.get(dir, fileName).toAbsolutePath();
  65     }
  66 
  67     private static Path getInputFilePath(String fileName) {
  68         return getFilePath(testSrcDir, fileName);
  69     }
  70 
  71     private static Path getOutputFilePath(String fileName) {
  72         return getFilePath(testClassesDir, fileName);
  73     }
  74 
  75     private static int checkJextract(String expected, String... options) {
  76         StringWriter writer = new StringWriter();
  77         PrintWriter pw = new PrintWriter(writer);
  78 
  79         int result = JEXTRACT_TOOL.run(pw, pw, options);
  80         String output = writer.toString();
  81         System.err.println(output);
  82         if (expected != null) {
  83             if (!output.contains(expected)) {
  84                 throw new AssertionError("Output does not contain " + expected);
  85             }
  86         }
  87 
  88         return result;
  89     }
  90 
  91     private static void checkSuccess(String expected, String... options) {
  92         int result = checkJextract(null, options);
  93         assertEquals(result, 0, "Sucess excepted, failed: " + result);
  94     }
  95 
  96     private static void checkFailure(String expected, String... options) {
  97         int result = checkJextract(expected, options);
  98         assertNotEquals(result, 0, "Failure excepted, succeeded!");
  99     }
 100 
 101     private static void deleteFile(Path path) {
 102         try {
 103             Files.delete(path);
 104         } catch (IOException ioExp) {
 105             System.err.println(ioExp);
 106         }
 107     }
 108 
 109     private static Class<?> loadClass(String className, Path...paths) {
 110         try {
 111             URL[] urls = new URL[paths.length];
 112             for (int i = 0; i < paths.length; i++) {
 113                 urls[i] = paths[i].toUri().toURL();
 114             }
 115             URLClassLoader ucl = new URLClassLoader(urls, null);
 116             return Class.forName(className, false, ucl);
 117         } catch (RuntimeException re) {
 118             throw re;
 119         } catch (Exception e) {
 120             throw new RuntimeException(e);
 121         }
 122     }
 123 
 124     private static Field findField(Class<?> cls, String name) {
 125         try {
 126             return cls.getField(name);
 127         } catch (Exception e) {
 128             System.err.println(e);
 129             return null;
 130         }
 131     }
 132 
 133     private static Method findMethod(Class<?> cls, String name, Class<?>... argTypes) {
 134         try {
 135             return cls.getMethod(name, argTypes);
 136         } catch (Exception e) {
 137             System.err.println(e);
 138             return null;
 139         }
 140     }
 141 
 142     private static Method findFirstMethod(Class<?> cls, String name) {
 143         try {
 144             for (Method m : cls.getMethods()) {
 145                 if (name.equals(m.getName())) {
 146                     return m;
 147                 }
 148             }
 149             return null;
 150         } catch (Exception e) {
 151             System.err.println(e);
 152             return null;
 153         }
 154     }
 155 
 156     @Test
 157     public void testHelp() {
 158         checkFailure(null); // no options
 159         checkSuccess(null, "--help");
 160         checkSuccess(null, "-h");
 161         checkSuccess(null, "-?");
 162     }
 163 
 164     // error for non-existent header file
 165     @Test
 166     public void testNonExistentHeader() {
 167         checkFailure("Cannot open header file", "--dry-run",
 168             getInputFilePath("non_existent.h").toString());
 169     }
 170 
 171     @Test
 172     public void testDryRun() {
 173         // only dry-run, don't produce any output
 174         Path simpleJar = getOutputFilePath("simple.jar");
 175         deleteFile(simpleJar);
 176         checkSuccess(null, "--dry-run", getInputFilePath("simple.h").toString());
 177         try {
 178             assertFalse(Files.isRegularFile(simpleJar));
 179         } finally {
 180             deleteFile(simpleJar);
 181         }
 182     }
 183 
 184     @Test
 185     public void testOutputFileOption() {
 186         // simple output file check
 187         Path simpleJar = getOutputFilePath("simple.jar");
 188         deleteFile(simpleJar);
 189         checkSuccess(null, "-o", simpleJar.toString(),
 190             getInputFilePath("simple.h").toString());
 191         try {
 192             assertTrue(Files.isRegularFile(simpleJar));
 193         } finally {
 194             deleteFile(simpleJar);
 195         }
 196     }
 197 
 198     @Test
 199     public void testOutputClass() {
 200         Path helloJar = getOutputFilePath("hello.jar");
 201         deleteFile(helloJar);
 202         Path helloH = getInputFilePath("hello.h");
 203         checkSuccess(null, "-o", helloJar.toString(), helloH.toString());
 204         try {
 205             Class<?> cls = loadClass("hello", helloJar);
 206             // check header annotation
 207             Header header = cls.getAnnotation(Header.class);
 208             assertNotNull(header);
 209             assertEquals(header.path(), helloH.toString());
 210 
 211             // check a method for "void func()"
 212             assertNotNull(findMethod(cls, "func", Object[].class));
 213         } finally {
 214             deleteFile(helloJar);
 215         }
 216     }
 217 
 218     private void testTargetPackage(String targetPkgOption) {
 219         Path helloJar = getOutputFilePath("hello.jar");
 220         deleteFile(helloJar);
 221         Path helloH = getInputFilePath("hello.h");
 222         checkSuccess(null, targetPkgOption, "com.acme", "-o", helloJar.toString(), helloH.toString());
 223         try {
 224             Class<?> cls = loadClass("com.acme.hello", helloJar);
 225             // check header annotation
 226             Header header = cls.getAnnotation(Header.class);
 227             assertNotNull(header);
 228             assertEquals(header.path(), helloH.toString());
 229 
 230             // check a method for "void func()"
 231             assertNotNull(findMethod(cls, "func", Object[].class));
 232         } finally {
 233             deleteFile(helloJar);
 234         }
 235     }
 236 
 237     @Test
 238     public void testTargetPackageOption() {
 239         testTargetPackage("-t");
 240     }
 241 
 242     @Test
 243     public void testTargetPackageLongOption() {
 244         testTargetPackage("--target-package");
 245     }
 246 
 247     private void testPackageMapping(String pkgMapOption) {
 248         Path worldJar = getOutputFilePath("world.jar");
 249         deleteFile(worldJar);
 250         Path mytypesJar = getOutputFilePath("mytypes.jar");
 251         deleteFile(mytypesJar);
 252 
 253         Path worldH = getInputFilePath("world.h");
 254         Path include = getInputFilePath("include");
 255         // generate jar for mytypes.h
 256         checkSuccess(null, "-t", "com.acme", "-o", mytypesJar.toString(),
 257             include.resolve("mytypes.h").toString());
 258         // world.h include mytypes.h, use appropriate package for stuff from mytypes.h
 259         checkSuccess(null, "-I", include.toString(), pkgMapOption, include.toString() + "=com.acme",
 260             "-o", worldJar.toString(), worldH.toString());
 261         try {
 262             Class<?> cls = loadClass("world", worldJar, mytypesJar);
 263             Method m = findFirstMethod(cls, "distance");
 264             Class<?>[] params = m.getParameterTypes();
 265             assertEquals(params[0].getName(), "com.acme.mytypes$Point");
 266         } finally {
 267             deleteFile(worldJar);
 268             deleteFile(mytypesJar);
 269         }
 270     }
 271 
 272     @Test
 273     public void testPackageDirMappingOption() {
 274         testPackageMapping("-m");
 275     }
 276 
 277     @Test
 278     public void testPackageDirMappingLongOption() {
 279         testPackageMapping("--package-map");
 280     }
 281 
 282     @Test
 283     public void test_option_L_without_l() {
 284         Path helloJar = getOutputFilePath("hello.jar");
 285         deleteFile(helloJar);
 286         Path helloH = getInputFilePath("hello.h");
 287         Path linkDir = getInputFilePath("libs");
 288         String warning = "WARNING: -L option specified without any -l option";
 289         checkSuccess(warning, "-L", linkDir.toString(), "-o", helloJar.toString(), helloH.toString());
 290     }
 291 
 292     @Test
 293     public void test_option_rpath_without_l() {
 294         Path helloJar = getOutputFilePath("hello.jar");
 295         deleteFile(helloJar);
 296         Path helloH = getInputFilePath("hello.h");
 297         Path rpathDir = getInputFilePath("libs");
 298         String warning = "WARNING: -rpath option specified without any -l option";
 299         try {
 300             checkSuccess(warning, "-rpath", rpathDir.toString(), "-o", helloJar.toString(), helloH.toString());
 301         } finally {
 302             deleteFile(helloJar);
 303         }
 304     }
 305 
 306     @Test
 307     public void test_option_l() {
 308         Path helloJar = getOutputFilePath("hello.jar");
 309         deleteFile(helloJar);
 310         Path helloH = getInputFilePath("hello.h");
 311         checkSuccess(null, "-l", "hello", "-o", helloJar.toString(), helloH.toString());
 312         try {
 313             Class<?> cls = loadClass("hello", helloJar);
 314             // check LibraryDependencies annotation capture -l value
 315             LibraryDependencies libDeps = cls.getAnnotation(LibraryDependencies.class);
 316             assertNotNull(libDeps);
 317             assertEquals(libDeps.names().length, 1);
 318             assertEquals(libDeps.names()[0], "hello");
 319             // no library paths (rpath) set
 320             assertEquals(libDeps.paths().length, 0);
 321         } finally {
 322             deleteFile(helloJar);
 323         }
 324     }
 325 
 326     @Test
 327     public void test_option_l_and_rpath() {
 328         Path helloJar = getOutputFilePath("hello.jar");
 329         deleteFile(helloJar);
 330         Path helloH = getInputFilePath("hello.h");
 331         Path rpathDir = getInputFilePath("libs");
 332         checkSuccess(null, "-l", "hello", "-rpath", rpathDir.toString(),
 333              "-o", helloJar.toString(), helloH.toString());
 334         try {
 335             Class<?> cls = loadClass("hello", helloJar);
 336             // check LibraryDependencies annotation captures -l and -rpath values
 337             LibraryDependencies libDeps = cls.getAnnotation(LibraryDependencies.class);
 338             assertNotNull(libDeps);
 339             assertEquals(libDeps.names().length, 1);
 340             assertEquals(libDeps.names()[0], "hello");
 341             assertEquals(libDeps.paths().length, 1);
 342             assertEquals(libDeps.paths()[0], rpathDir.toString());
 343         } finally {
 344             deleteFile(helloJar);
 345         }
 346     }
 347 
 348     @Test
 349     public void testUnionDeclaration() {
 350         Path uniondeclJar = getOutputFilePath("uniondecl.jar");
 351         deleteFile(uniondeclJar);
 352         Path uniondeclH = getInputFilePath("uniondecl.h");
 353         try {
 354             checkSuccess(null, "-o", uniondeclJar.toString(), uniondeclH.toString());
 355             Class<?> unionCls = loadClass("uniondecl", uniondeclJar);
 356             assertNotNull(unionCls);
 357             boolean found = Arrays.stream(unionCls.getClasses()).
 358                 map(Class::getSimpleName).
 359                 filter(n -> n.equals("IntOrFloat")).
 360                 findFirst().isPresent();
 361             assertTrue(found, "uniondecl.IntOrFloat not found");
 362         } finally {
 363             deleteFile(uniondeclJar);
 364         }
 365     }
 366 
 367     private void checkIntField(Class<?> cls, String name, int value) {
 368         Field field = findField(cls, name);
 369         assertNotNull(field);
 370         assertEquals(field.getType(), int.class);
 371         try {
 372             assertEquals((int)field.get(null), value);
 373         } catch (Exception exp) {
 374             System.err.println(exp);
 375             assertTrue(false, "should not reach here");
 376         }
 377     }
 378 
 379     @Test
 380     public void testAnonymousEnum() {
 381         Path anonenumJar = getOutputFilePath("anonenum.jar");
 382         deleteFile(anonenumJar);
 383         Path anonenumH = getInputFilePath("anonenum.h");
 384         try {
 385             checkSuccess(null, "-o", anonenumJar.toString(), anonenumH.toString());
 386             Class<?> anonenumCls = loadClass("anonenum", anonenumJar);
 387             assertNotNull(anonenumCls);
 388             // the nested type for anonymous enum has name starting with "enum__anonymous_at"
 389             // followed by full path name of header file + line + column numbers. Any non-ident
 390             // char replaced by "_". But we test only the start pattern here.
 391             Optional<Class<?>> optEnumCls = Arrays.stream(anonenumCls.getClasses()).
 392                 filter(c -> c.getSimpleName().startsWith("enum__anonymous_at")).
 393                 findFirst();
 394             assertTrue(optEnumCls.isPresent());
 395             Class<?> enumCls = optEnumCls.get();
 396             checkIntField(enumCls, "RED", 0xff0000);
 397             checkIntField(enumCls, "GREEN", 0x00ff00);
 398             checkIntField(enumCls, "BLUE", 0x0000ff);
 399         } finally {
 400             deleteFile(anonenumJar);
 401         }
 402     }
 403 
 404     @Test
 405     public void testExcludeSymbols() {
 406         Path helloJar = getOutputFilePath("hello.jar");
 407         deleteFile(helloJar);
 408         Path helloH = getInputFilePath("hello.h");
 409         checkSuccess(null, "-o", helloJar.toString(), helloH.toString());
 410         try {
 411             Class<?> cls = loadClass("hello", helloJar);
 412             // check a method for "void func()"
 413             assertNotNull(findMethod(cls, "func", Object[].class));
 414             // check a method for "void junk()"
 415             assertNotNull(findMethod(cls, "junk", Object[].class));
 416         } finally {
 417             deleteFile(helloJar);
 418         }
 419 
 420         // try with --exclude-symbols" this time.
 421         checkSuccess(null, "--exclude-symbols", "junk", "-o", helloJar.toString(), helloH.toString());
 422         try {
 423             Class<?> cls = loadClass("hello", helloJar);
 424             // check a method for "void func()"
 425             assertNotNull(findMethod(cls, "func", Object[].class));
 426             // check a method for "void junk()"
 427             assertNull(findMethod(cls, "junk", Object[].class));
 428         } finally {
 429             deleteFile(helloJar);
 430         }
 431     }
 432 
 433     @Test
 434     public void testNestedStructsUnions() {
 435         Path nestedJar = getOutputFilePath("nested.jar");
 436         deleteFile(nestedJar);
 437         Path nestedH = getInputFilePath("nested.h");
 438         try {
 439             checkSuccess(null, "-o", nestedJar.toString(), nestedH.toString());
 440             assertNotNull(loadClass("nested", nestedJar));
 441             Class<?> fooCls = loadClass("nested$Foo", nestedJar);
 442             assertNotNull(fooCls);
 443             // struct Foo has no getters for "x", "y" etc.
 444             assertNull(findMethod(fooCls, "x$get"));
 445             assertNull(findMethod(fooCls, "y$get"));
 446             // struct Foo has getters for bar and color
 447             assertNotNull(findMethod(fooCls, "bar$get"));
 448             assertNotNull(findMethod(fooCls, "color$get"));
 449             // make sure nested types are handled without nested namespace!
 450             assertNotNull(loadClass("nested$Bar", nestedJar));
 451             assertNotNull(loadClass("nested$Color", nestedJar));
 452 
 453             Class<?> uCls = loadClass("nested$U", nestedJar);
 454             assertNotNull(uCls);
 455             // union U has no getters for "x", "y" etc.
 456             assertNull(findMethod(uCls, "x$get"));
 457             assertNull(findMethod(uCls, "y$get"));
 458             // union U has getters for point, rgb, i
 459             assertNotNull(findMethod(uCls, "point$get"));
 460             assertNotNull(findMethod(uCls, "rgb$get"));
 461             assertNotNull(findMethod(uCls, "i$get"));
 462             // make sure nested types are handled without nested namespace!
 463             assertNotNull(loadClass("nested$Point", nestedJar));
 464             assertNotNull(loadClass("nested$RGB", nestedJar));
 465         } finally {
 466             deleteFile(nestedJar);
 467         }
 468     }
 469 }