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.NativeHeader;
  32 import java.nicl.types.Pointer;
  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.Map;
  38 import java.util.Optional;
  39 import java.util.spi.ToolProvider;
  40 import org.testng.annotations.Test;
  41 
  42 import static org.testng.Assert.assertEquals;
  43 import static org.testng.Assert.assertNotEquals;
  44 import static org.testng.Assert.assertNotNull;
  45 import static org.testng.Assert.assertNull;
  46 import static org.testng.Assert.assertTrue;
  47 import static org.testng.Assert.assertFalse;
  48 
  49 /*
  50  * @test
  51  * @modules jdk.jextract
  52  * @build JextractToolProviderTest
  53  * @run testng/othervm -Duser.language=en JextractToolProviderTest
  54  */
  55 public class JextractToolProviderTest {
  56     private static final ToolProvider JEXTRACT_TOOL = ToolProvider.findFirst("jextract")
  57         .orElseThrow(() ->
  58             new RuntimeException("jextract tool not found")
  59         );
  60 
  61     private static String testSrcDir = System.getProperty("test.src", ".");
  62     private static String testClassesDir = System.getProperty("test.classes", ".");
  63 
  64     private static Path getFilePath(String dir, String fileName) {
  65         return Paths.get(dir, fileName).toAbsolutePath();
  66     }
  67 
  68     private static Path getInputFilePath(String fileName) {
  69         return getFilePath(testSrcDir, fileName);
  70     }
  71 
  72     private static Path getOutputFilePath(String fileName) {
  73         return getFilePath(testClassesDir, fileName);
  74     }
  75 
  76     private static int checkJextract(String expected, String... options) {
  77         StringWriter writer = new StringWriter();
  78         PrintWriter pw = new PrintWriter(writer);
  79 
  80         int result = JEXTRACT_TOOL.run(pw, pw, options);
  81         String output = writer.toString();
  82         System.err.println(output);
  83         if (expected != null) {
  84             if (!output.contains(expected)) {
  85                 throw new AssertionError("Output does not contain " + expected);
  86             }
  87         }
  88 
  89         return result;
  90     }
  91 
  92     private static void checkSuccess(String expected, String... options) {
  93         int result = checkJextract(null, options);
  94         assertEquals(result, 0, "Sucess excepted, failed: " + result);
  95     }
  96 
  97     private static void checkFailure(String expected, String... options) {
  98         int result = checkJextract(expected, options);
  99         assertNotEquals(result, 0, "Failure excepted, succeeded!");
 100     }
 101 
 102     private static void deleteFile(Path path) {
 103         try {
 104             Files.delete(path);
 105         } catch (IOException ioExp) {
 106             System.err.println(ioExp);
 107         }
 108     }
 109 
 110     private static Class<?> loadClass(String className, Path...paths) {
 111         try {
 112             URL[] urls = new URL[paths.length];
 113             for (int i = 0; i < paths.length; i++) {
 114                 urls[i] = paths[i].toUri().toURL();
 115             }
 116             URLClassLoader ucl = new URLClassLoader(urls, null);
 117             return Class.forName(className, false, ucl);
 118         } catch (RuntimeException re) {
 119             throw re;
 120         } catch (Exception e) {
 121             throw new RuntimeException(e);
 122         }
 123     }
 124 
 125     private static Field findField(Class<?> cls, String name) {
 126         try {
 127             return cls.getField(name);
 128         } catch (Exception e) {
 129             System.err.println(e);
 130             return null;
 131         }
 132     }
 133 
 134     private static Method findMethod(Class<?> cls, String name, Class<?>... argTypes) {
 135         try {
 136             return cls.getMethod(name, argTypes);
 137         } catch (Exception e) {
 138             System.err.println(e);
 139             return null;
 140         }
 141     }
 142 
 143     private static Method findStructFieldGet(Class<?> cls, String name) {
 144         return findMethod(cls, name + "$get");
 145     }
 146 
 147     private static Method findGlobalVariableGet(Class<?> cls, String name) {
 148         return findMethod(cls, name + "$get");
 149     }
 150 
 151     private static Method findFirstMethod(Class<?> cls, String name) {
 152         try {
 153             for (Method m : cls.getMethods()) {
 154                 if (name.equals(m.getName())) {
 155                     return m;
 156                 }
 157             }
 158             return null;
 159         } catch (Exception e) {
 160             System.err.println(e);
 161             return null;
 162         }
 163     }
 164 
 165     @Test
 166     public void testHelp() {
 167         checkFailure(null); // no options
 168         checkSuccess(null, "--help");
 169         checkSuccess(null, "-h");
 170         checkSuccess(null, "-?");
 171     }
 172 
 173     // error for non-existent header file
 174     @Test
 175     public void testNonExistentHeader() {
 176         checkFailure("Cannot open header file", "--dry-run",
 177             getInputFilePath("non_existent.h").toString());
 178     }
 179 
 180     @Test
 181     public void testDryRun() {
 182         // only dry-run, don't produce any output
 183         Path simpleJar = getOutputFilePath("simple.jar");
 184         deleteFile(simpleJar);
 185         checkSuccess(null, "--dry-run", getInputFilePath("simple.h").toString());
 186         try {
 187             assertFalse(Files.isRegularFile(simpleJar));
 188         } finally {
 189             deleteFile(simpleJar);
 190         }
 191     }
 192 
 193     @Test
 194     public void testOutputFileOption() {
 195         // simple output file check
 196         Path simpleJar = getOutputFilePath("simple.jar");
 197         deleteFile(simpleJar);
 198         checkSuccess(null, "-o", simpleJar.toString(),
 199             getInputFilePath("simple.h").toString());
 200         try {
 201             assertTrue(Files.isRegularFile(simpleJar));
 202         } finally {
 203             deleteFile(simpleJar);
 204         }
 205     }
 206 
 207     @Test
 208     public void testOutputClass() {
 209         Path helloJar = getOutputFilePath("hello.jar");
 210         deleteFile(helloJar);
 211         Path helloH = getInputFilePath("hello.h");
 212         checkSuccess(null, "-o", helloJar.toString(), helloH.toString());
 213         try {
 214             Class<?> cls = loadClass("hello", helloJar);
 215             // check NativeHeader annotation
 216             NativeHeader header = cls.getAnnotation(NativeHeader.class);
 217             assertNotNull(header);
 218             assertEquals(header.headerPath(), helloH.toString());

 219 
 220             // check a method for "void func()"
 221             assertNotNull(findMethod(cls, "func", Object[].class));
 222         } finally {
 223             deleteFile(helloJar);
 224         }
 225     }
 226 
 227     private void testTargetPackage(String targetPkgOption) {
 228         Path helloJar = getOutputFilePath("hello.jar");
 229         deleteFile(helloJar);
 230         Path helloH = getInputFilePath("hello.h");
 231         checkSuccess(null, targetPkgOption, "com.acme", "-o", helloJar.toString(), helloH.toString());
 232         try {
 233             Class<?> cls = loadClass("com.acme.hello", helloJar);
 234             // check NativeHeader annotation
 235             NativeHeader header = cls.getAnnotation(NativeHeader.class);
 236             assertNotNull(header);
 237             assertEquals(header.headerPath(), helloH.toString());
 238 
 239             // check a method for "void func()"
 240             assertNotNull(findMethod(cls, "func", Object[].class));
 241         } finally {
 242             deleteFile(helloJar);
 243         }
 244     }
 245 
 246     @Test
 247     public void testTargetPackageOption() {
 248         testTargetPackage("-t");
 249     }
 250 
 251     @Test
 252     public void testTargetPackageLongOption() {
 253         testTargetPackage("--target-package");
 254     }
 255 
 256     private void testPackageMapping(String pkgMapOption) {
 257         Path worldJar = getOutputFilePath("world.jar");
 258         deleteFile(worldJar);
 259         Path mytypesJar = getOutputFilePath("mytypes.jar");
 260         deleteFile(mytypesJar);
 261 
 262         Path worldH = getInputFilePath("world.h");
 263         Path include = getInputFilePath("include");
 264         // generate jar for mytypes.h
 265         checkSuccess(null, "-t", "com.acme", "-o", mytypesJar.toString(),
 266             include.resolve("mytypes.h").toString());
 267         // world.h include mytypes.h, use appropriate package for stuff from mytypes.h
 268         checkSuccess(null, "-I", include.toString(), pkgMapOption, include.toString() + "=com.acme",
 269             "-o", worldJar.toString(), worldH.toString());
 270         try {
 271             Class<?> cls = loadClass("world", worldJar, mytypesJar);
 272             Method m = findFirstMethod(cls, "distance");
 273             Class<?>[] params = m.getParameterTypes();
 274             assertEquals(params[0].getName(), "com.acme.mytypes$Point");
 275         } finally {
 276             deleteFile(worldJar);
 277             deleteFile(mytypesJar);
 278         }
 279     }
 280 
 281     @Test
 282     public void testPackageDirMappingOption() {
 283         testPackageMapping("-m");
 284     }
 285 
 286     @Test
 287     public void testPackageDirMappingLongOption() {
 288         testPackageMapping("--package-map");
 289     }
 290 
 291     @Test
 292     public void test_option_L_without_l() {
 293         Path helloJar = getOutputFilePath("hello.jar");
 294         deleteFile(helloJar);
 295         Path helloH = getInputFilePath("hello.h");
 296         Path linkDir = getInputFilePath("libs");
 297         String warning = "WARNING: -L option specified without any -l option";
 298         checkSuccess(warning, "-L", linkDir.toString(), "-o", helloJar.toString(), helloH.toString());
 299     }
 300 
 301     @Test
 302     public void test_option_rpath_without_l() {
 303         Path helloJar = getOutputFilePath("hello.jar");
 304         deleteFile(helloJar);
 305         Path helloH = getInputFilePath("hello.h");
 306         Path rpathDir = getInputFilePath("libs");
 307         String warning = "WARNING: -rpath option specified without any -l option";
 308         try {
 309             checkSuccess(warning, "-rpath", rpathDir.toString(), "-o", helloJar.toString(), helloH.toString());
 310         } finally {
 311             deleteFile(helloJar);
 312         }
 313     }
 314 
 315     @Test
 316     public void test_option_l() {
 317         Path helloJar = getOutputFilePath("hello.jar");
 318         deleteFile(helloJar);
 319         Path helloH = getInputFilePath("hello.h");
 320         checkSuccess(null, "-l", "hello", "-o", helloJar.toString(), helloH.toString());
 321         try {
 322             Class<?> cls = loadClass("hello", helloJar);
 323             // check that NativeHeader annotation captures -l value
 324             NativeHeader header = cls.getAnnotation(NativeHeader.class);
 325             assertNotNull(header);
 326             assertEquals(header.libraries().length, 1);
 327             assertEquals(header.libraries()[0], "hello");
 328             // no library paths (rpath) set
 329             assertEquals(header.libraryPaths().length, 0);
 330         } finally {
 331             deleteFile(helloJar);
 332         }
 333     }
 334 
 335     @Test
 336     public void test_option_l_and_rpath() {
 337         Path helloJar = getOutputFilePath("hello.jar");
 338         deleteFile(helloJar);
 339         Path helloH = getInputFilePath("hello.h");
 340         Path rpathDir = getInputFilePath("libs");
 341         checkSuccess(null, "-l", "hello", "-rpath", rpathDir.toString(),
 342              "-o", helloJar.toString(), helloH.toString());
 343         try {
 344             Class<?> cls = loadClass("hello", helloJar);
 345             // check that NativeHeader annotation captures -l and -rpath values
 346             NativeHeader header = cls.getAnnotation(NativeHeader.class);
 347             assertNotNull(header);
 348             assertEquals(header.libraries().length, 1);
 349             assertEquals(header.libraries()[0], "hello");
 350             assertEquals(header.libraryPaths().length, 1);
 351             assertEquals(header.libraryPaths()[0], rpathDir.toString());
 352         } finally {
 353             deleteFile(helloJar);
 354         }
 355     }
 356 
 357     @Test
 358     public void testUnionDeclaration() {
 359         Path uniondeclJar = getOutputFilePath("uniondecl.jar");
 360         deleteFile(uniondeclJar);
 361         Path uniondeclH = getInputFilePath("uniondecl.h");
 362         try {
 363             checkSuccess(null, "-o", uniondeclJar.toString(), uniondeclH.toString());
 364             Class<?> unionCls = loadClass("uniondecl", uniondeclJar);
 365             assertNotNull(unionCls);
 366             boolean found = Arrays.stream(unionCls.getClasses()).
 367                 map(Class::getSimpleName).
 368                 filter(n -> n.equals("IntOrFloat")).
 369                 findFirst().isPresent();
 370             assertTrue(found, "uniondecl.IntOrFloat not found");
 371         } finally {
 372             deleteFile(uniondeclJar);
 373         }
 374     }
 375 
 376     private void checkIntField(Class<?> cls, String name, int value) {
 377         Field field = findField(cls, name);
 378         assertNotNull(field);
 379         assertEquals(field.getType(), int.class);
 380         try {
 381             assertEquals((int)field.get(null), value);
 382         } catch (Exception exp) {
 383             System.err.println(exp);
 384             assertTrue(false, "should not reach here");
 385         }
 386     }
 387 
 388     private Class<?> findClass(Class<?>[] clz, String name) {
 389         for (Class<?> cls: clz) {
 390             if (cls.getSimpleName().equals(name)) {
 391                 return cls;
 392             }
 393         }
 394         return null;
 395     }
 396 
 397     private void testEnumValue(Class<?> enumCls, Map<String, Integer> values) {
 398         values.entrySet().stream().
 399                 forEach(e -> checkIntField(enumCls, e.getKey(), e.getValue()));
 400     }
 401 
 402     @Test
 403     public void testAnonymousEnum() {
 404         Path anonenumJar = getOutputFilePath("anonenum.jar");
 405         deleteFile(anonenumJar);
 406         Path anonenumH = getInputFilePath("anonenum.h");
 407         try {
 408             checkSuccess(null, "-o", anonenumJar.toString(), anonenumH.toString());
 409             Class<?> anonenumCls = loadClass("anonenum", anonenumJar);
 410             assertNotNull(anonenumCls);
 411             checkIntField(anonenumCls, "RED", 0xff0000);
 412             checkIntField(anonenumCls, "GREEN", 0x00ff00);
 413             checkIntField(anonenumCls, "BLUE", 0x0000ff);
 414             testEnumValue(anonenumCls, Map.of(
 415                     "Java", 0,
 416                     "C", 1,
 417                     "CPP", 2,
 418                     "Python", 3,
 419                     "Ruby", 4));
 420             testEnumValue(anonenumCls, Map.of(
 421                     "XS", 0,
 422                     "S", 1,
 423                     "M", 2,
 424                     "L", 3,
 425                     "XL", 4,
 426                     "XXL", 5));
 427             testEnumValue(anonenumCls, Map.of(
 428                     "ONE", 1,
 429                     "TWO", 2));
 430 
 431             Class<?> enumClz[] = anonenumCls.getClasses();
 432             assert(enumClz.length >= 4);
 433 
 434             Class<?> enumCls = findClass(enumClz, "codetype_t");
 435             assertNotNull(enumCls);
 436 
 437             enumCls = findClass(enumClz, "SIZE");
 438             assertNotNull(enumCls);
 439 
 440             enumCls = findClass(enumClz, "temp");
 441             assertNotNull(enumCls);
 442 
 443             enumCls = findClass(enumClz, "temp_t");
 444             assertNotNull(enumCls);
 445         } finally {
 446             deleteFile(anonenumJar);
 447         }
 448     }
 449 
 450     @Test
 451     public void testExcludeSymbols() {
 452         Path helloJar = getOutputFilePath("hello.jar");
 453         deleteFile(helloJar);
 454         Path helloH = getInputFilePath("hello.h");
 455         checkSuccess(null, "-o", helloJar.toString(), helloH.toString());
 456         try {
 457             Class<?> cls = loadClass("hello", helloJar);
 458             // check a method for "void func()"
 459             assertNotNull(findMethod(cls, "func", Object[].class));
 460             // check a method for "void junk()"
 461             assertNotNull(findMethod(cls, "junk", Object[].class));
 462         } finally {
 463             deleteFile(helloJar);
 464         }
 465 
 466         // try with --exclude-symbols" this time.
 467         checkSuccess(null, "--exclude-symbols", "junk", "-o", helloJar.toString(), helloH.toString());
 468         try {
 469             Class<?> cls = loadClass("hello", helloJar);
 470             // check a method for "void func()"
 471             assertNotNull(findMethod(cls, "func", Object[].class));
 472             // check a method for "void junk()"
 473             assertNull(findMethod(cls, "junk", Object[].class));
 474         } finally {
 475             deleteFile(helloJar);
 476         }
 477     }
 478 
 479     @Test
 480     public void testNestedStructsUnions() {
 481         Path nestedJar = getOutputFilePath("nested.jar");
 482         deleteFile(nestedJar);
 483         Path nestedH = getInputFilePath("nested.h");
 484         try {
 485             checkSuccess(null, "-o", nestedJar.toString(), nestedH.toString());
 486             Class<?> headerCls = loadClass("nested", nestedJar);
 487             assertNotNull(headerCls);
 488 
 489             Class<?> fooCls = loadClass("nested$Foo", nestedJar);
 490             assertNotNull(fooCls);
 491             // struct Foo has no getters for "x", "y" etc.
 492             assertNull(findStructFieldGet(fooCls, "x"));
 493             assertNull(findStructFieldGet(fooCls, "y"));
 494             // struct Foo has getters for bar and color
 495             assertNotNull(findStructFieldGet(fooCls, "bar"));
 496             assertNotNull(findStructFieldGet(fooCls, "color"));
 497             // make sure nested types are handled without nested namespace!
 498             assertNotNull(loadClass("nested$Bar", nestedJar));
 499             assertNotNull(loadClass("nested$Color", nestedJar));
 500 
 501             Class<?> uCls = loadClass("nested$U", nestedJar);
 502             assertNotNull(uCls);
 503             // union U has no getters for "x", "y" etc.
 504             assertNull(findStructFieldGet(uCls, "x"));
 505             assertNull(findStructFieldGet(uCls, "y"));
 506             // union U has getters for point, rgb, i
 507             assertNotNull(findStructFieldGet(uCls, "point"));
 508             assertNotNull(findStructFieldGet(uCls, "rgb"));
 509             assertNotNull(findStructFieldGet(uCls, "i"));
 510             // make sure nested types are handled without nested namespace!
 511             assertNotNull(loadClass("nested$Point", nestedJar));
 512             assertNotNull(loadClass("nested$RGB", nestedJar));
 513 
 514             Class<?> myStructCls = loadClass("nested$MyStruct", nestedJar);
 515             assertNotNull(findStructFieldGet(myStructCls, "a"));
 516             assertNotNull(findStructFieldGet(myStructCls, "b"));
 517             assertNotNull(findStructFieldGet(myStructCls, "c"));
 518             assertNotNull(findStructFieldGet(myStructCls, "d"));
 519             // 'e' is named struct element - should not be in MyStruct
 520             assertNull(findStructFieldGet(myStructCls, "e"));
 521             assertNotNull(findStructFieldGet(myStructCls, "f"));
 522             assertNotNull(findStructFieldGet(myStructCls, "g"));
 523             assertNotNull(findStructFieldGet(myStructCls, "h"));
 524             // 'i' is named struct element - should not be in MyStruct
 525             assertNull(findStructFieldGet(myStructCls, "i"));
 526             // 'j' is named struct element - should not be in MyStruct
 527             assertNull(findStructFieldGet(myStructCls, "j"));
 528             assertNotNull(findStructFieldGet(myStructCls, "k"));
 529             // "X", "Y", "Z" are enum constants -should not be in MyStruct
 530             assertNull(findStructFieldGet(myStructCls, "X"));
 531             assertNull(findStructFieldGet(myStructCls, "Y"));
 532             assertNull(findStructFieldGet(myStructCls, "Z"));
 533             // anonymous enum constants are hoisted to containing scope
 534             assertNotNull(findField(headerCls, "X"));
 535             assertNotNull(findField(headerCls, "Y"));
 536             assertNotNull(findField(headerCls, "Z"));
 537 
 538             Class<?> myUnionCls = loadClass("nested$MyUnion", nestedJar);
 539             assertNotNull(findStructFieldGet(myUnionCls, "a"));
 540             assertNotNull(findStructFieldGet(myUnionCls, "b"));
 541             assertNotNull(findStructFieldGet(myUnionCls, "c"));
 542             assertNotNull(findStructFieldGet(myUnionCls, "d"));
 543             // 'e' is named struct element - should not be in MyUnion
 544             assertNull(findStructFieldGet(myUnionCls, "e"));
 545             assertNotNull(findStructFieldGet(myUnionCls, "f"));
 546             assertNotNull(findStructFieldGet(myUnionCls, "g"));
 547             assertNotNull(findStructFieldGet(myUnionCls, "h"));
 548             // 'i' is named struct element - should not be in MyUnion
 549             assertNull(findStructFieldGet(myUnionCls, "i"));
 550             // 'j' is named struct element - should not be in MyUnion
 551             assertNull(findStructFieldGet(myUnionCls, "j"));
 552             assertNotNull(findStructFieldGet(myUnionCls, "k"));
 553             // "A", "B", "C" are enum constants -should not be in MyUnion
 554             assertNull(findStructFieldGet(myUnionCls, "A"));
 555             assertNull(findStructFieldGet(myUnionCls, "B"));
 556             assertNull(findStructFieldGet(myUnionCls, "C"));
 557             // anonymous enum constants are hoisted to containing scope
 558             assertNotNull(findField(headerCls, "A"));
 559             assertNotNull(findField(headerCls, "B"));
 560             assertNotNull(findField(headerCls, "C"));
 561         } finally {
 562             deleteFile(nestedJar);
 563         }
 564     }
 565 
 566     @Test
 567     public void testAnonymousStructTypeGlobalVar() {
 568         Path elaboratedTypeJar = getOutputFilePath("elaboratedtype.jar");
 569         deleteFile(elaboratedTypeJar);
 570         Path elaboratedTypeH = getInputFilePath("elaboratedtype.h");
 571         try {
 572             checkSuccess(null, "-o", elaboratedTypeJar.toString(), elaboratedTypeH.toString());
 573             Class<?> headerCls = loadClass("elaboratedtype", elaboratedTypeJar);
 574             assertNotNull(findGlobalVariableGet(headerCls, "point"));
 575             assertNotNull(findGlobalVariableGet(headerCls, "long_or_int"));
 576             assertNotNull(findMethod(headerCls, "func", Pointer.class));
 577         } finally {
 578             deleteFile(elaboratedTypeJar);
 579         }
 580     }
 581 }
--- EOF ---