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