1 /*
   2  * Copyright (c) 2015, 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.foreign.annotations.*;
  25 import java.nio.file.Files;
  26 import java.io.ByteArrayInputStream;
  27 import java.io.ByteArrayOutputStream;
  28 import java.io.File;
  29 import java.io.IOException;
  30 import java.io.PrintWriter;
  31 import java.lang.annotation.Annotation;
  32 import java.lang.reflect.Method;
  33 import java.nio.file.Path;
  34 import java.nio.file.Paths;
  35 import java.util.Arrays;
  36 import java.util.HashMap;
  37 import java.util.HashSet;
  38 import java.util.List;
  39 import java.util.Map;
  40 import java.util.Set;
  41 import java.util.jar.JarEntry;
  42 import java.util.jar.JarInputStream;
  43 import java.util.jar.JarOutputStream;
  44 import java.util.stream.Collectors;
  45 import javax.tools.JavaCompiler;
  46 import javax.tools.StandardJavaFileManager;
  47 import javax.tools.ToolProvider;
  48 
  49 import com.sun.tools.jextract.Filters;
  50 import com.sun.tools.jextract.JextractTool;
  51 import com.sun.tools.jextract.JarWriter;
  52 import com.sun.tools.jextract.Log;
  53 import com.sun.tools.jextract.Options;
  54 import com.sun.tools.jextract.Writer;
  55 import jdk.internal.org.objectweb.asm.ClassReader;
  56 import jdk.internal.org.objectweb.asm.tree.ClassNode;
  57 import jdk.internal.org.objectweb.asm.util.ASMifier;
  58 import jdk.internal.org.objectweb.asm.util.CheckClassAdapter;
  59 import jdk.internal.org.objectweb.asm.util.Textifier;
  60 import jdk.internal.org.objectweb.asm.util.TraceClassVisitor;
  61 import static org.testng.Assert.assertEquals;
  62 import static org.testng.Assert.assertNotNull;
  63 import static org.testng.Assert.assertTrue;
  64 import static org.testng.Assert.fail;
  65 import org.testng.annotations.BeforeClass;
  66 import org.testng.annotations.DataProvider;
  67 import org.testng.annotations.Factory;
  68 import org.testng.annotations.Test;
  69 import com.sun.tools.jextract.Context;
  70 
  71 /*
  72  * @test
  73  * @summary Main test runner created all cases
  74  * @modules java.base/jdk.internal.org.objectweb.asm
  75  * @modules java.base/jdk.internal.org.objectweb.asm.tree
  76  * @modules java.base/jdk.internal.org.objectweb.asm.util
  77  * @modules jdk.jextract/com.sun.tools.jextract
  78  * @build InMemoryFileManager
  79  * @run testng Runner
  80  */
  81 public class Runner {
  82     private final Path[] javaSrcFiles;
  83     private final Context ctx;
  84 
  85     private InMemoryFileManager<StandardJavaFileManager> mfm;
  86     private ClassLoader expectedCL;
  87     private Map<String, byte[]> actualClz;
  88     private ClassLoader actualCL;
  89     private Writer writer;
  90     private Object[][] clz_data;
  91 
  92     public Runner(Path nativeSrc, String pkg, Path[] javaSrcFiles) {
  93         this.javaSrcFiles = javaSrcFiles;
  94         this.ctx = createContext(nativeSrc, pkg);
  95     }
  96 
  97     private static Context createContext(Path nativeSrc, String pkg) {
  98         if (!Files.isReadable(nativeSrc)) {
  99             throw new IllegalArgumentException("Cannot read the file: " + nativeSrc);
 100         }
 101         nativeSrc = nativeSrc.toAbsolutePath();
 102         Options.Builder options = Options.builder();
 103         options.setTargetPackage(pkg);
 104         return new Context(List.of(nativeSrc.toAbsolutePath()),
 105                 options.build(), Log.createDefault(), Filters.createDefault());
 106     }
 107 
 108     private Writer extract() throws IOException {
 109         return new JextractTool(ctx).processHeaders();
 110     }
 111 
 112     private InMemoryFileManager<StandardJavaFileManager> compileJavaCode() {
 113         JavaCompiler cl = ToolProvider.getSystemJavaCompiler();
 114         StandardJavaFileManager sfm = cl.getStandardFileManager(null, null, null);
 115         InMemoryFileManager<StandardJavaFileManager> fm = new InMemoryFileManager<>(sfm);
 116         JavaCompiler.CompilationTask task = cl.getTask(null, fm, null, null,
 117                 null, sfm.getJavaFileObjects(javaSrcFiles));
 118         task.call();
 119         return fm;
 120     }
 121 
 122     @Test
 123     public void testJarManifest() throws IOException {
 124         // Get the jar
 125         ByteArrayOutputStream bos = new ByteArrayOutputStream();
 126         new JarWriter(ctx, writer).writeJarFile(new JarOutputStream(bos), new String[0]);
 127 
 128         System.out.println("Jar built, verifying...");
 129         JarInputStream jis = new JarInputStream(new ByteArrayInputStream(bos.toByteArray()));
 130 
 131         // List all classes in the jar
 132         Set<String> files = new HashSet<>();
 133         for (JarEntry e = jis.getNextJarEntry(); e != null; e = jis.getNextJarEntry()) {
 134             if (e.isDirectory()) {
 135                 continue;
 136             }
 137             String name = e.getName();
 138             if (! name.endsWith(".properties")) {
 139                 if (! name.endsWith(".class")) {
 140                     // Should not have file not class files
 141                     System.err.println("Warning: unexpected file " + name);
 142                 }
 143                 name = name.substring(0, name.length() - 6);
 144                 files.add(normalize(name));
 145             }
 146         }
 147 
 148         assertEquals(files, mfm.listClasses());
 149     }
 150 
 151     private void verifyNativeLocation(NativeLocation actual, NativeLocation expected) {
 152         // Only check the filename, not full path
 153         assertNotNull(actual);
 154         assertNotNull(expected);
 155         assertEquals(Paths.get(actual.file()).getFileName(),
 156                      Paths.get(expected.file()).getFileName());
 157         assertEquals(actual.line(), expected.line());
 158         assertEquals(actual.column(), expected.column());
 159     }
 160 
 161     private void verifyMethodAnnotation(Method actual, Method expected) {
 162         Annotation[] ea = expected.getAnnotations();
 163 
 164         for (Annotation a: ea) {
 165             if (a instanceof NativeLocation) {
 166                 verifyNativeLocation(actual.getAnnotation(NativeLocation.class), (NativeLocation) a);
 167             } else {
 168                 @SuppressWarnings("unchecked")
 169                 Annotation e = actual.getAnnotation((Class)a.getClass().getInterfaces()[0]);
 170                 assertNotNull(e);
 171                 assertEquals(a, e);
 172             }
 173         }
 174     }
 175 
 176     @Test(dataProvider = "classes")
 177     public void testMethods(Class<?> actual, Class<?> expected) {
 178         Method[] am = actual.getMethods();
 179         Method[] em = expected.getMethods();
 180         HashMap<String, Method> ams = new HashMap<>();
 181         for (Method m: am) {
 182             ams.put(m.toGenericString(), m);
 183         }
 184         HashMap<String, Method> ems = new HashMap<>();
 185         for (Method m: em) {
 186             String sig = m.toGenericString();
 187             Method ma = ams.remove(sig);
 188             if (ma == null) {
 189                 System.out.println("Missing " + sig);
 190                 ems.put(m.toGenericString(), m);
 191             } else {
 192                 assertEquals(ma.isVarArgs(), m.isVarArgs());
 193                 try {
 194                     verifyMethodAnnotation(ma, m);
 195                 } catch (Throwable t) {
 196                     fail("Failed method " + sig, t);
 197                 }
 198             }
 199         }
 200         ams.keySet().forEach((sig) -> {
 201             System.out.println("Got: " + sig);
 202         });
 203         assertTrue(ams.isEmpty());
 204         assertTrue(ems.isEmpty());
 205     }
 206 
 207     @Test(dataProvider = "classes")
 208     public void testAnnotations(Class<?> actual, Class<?> expected) {
 209         Annotation[] aa = actual.getAnnotations();
 210         Annotation[] ea = expected.getAnnotations();
 211 
 212         if (actual.getName().contains("$")) {
 213             assertTrue(actual.getName().contains("$"));
 214             assertTrue(expected.isMemberClass());
 215             assertTrue(actual.isMemberClass());
 216             if (actual.isAnnotationPresent(NativeStruct.class)) {
 217                 NativeStruct ant = actual.getAnnotation(NativeStruct.class);
 218                 assertNotNull(ant);
 219                 assertEquals(ant, expected.getAnnotation(NativeStruct.class));
 220             } else if (actual.isAnnotationPresent(NativeFunction.class)) {
 221                 NativeFunction cb = actual.getAnnotation(NativeFunction.class);
 222                 assertNotNull(cb);
 223                 assertEquals(cb, expected.getAnnotation(NativeFunction.class));
 224             }
 225 
 226             if (expected.isAnnotationPresent(NativeLocation.class)) {
 227                 NativeLocation loc = actual.getAnnotation(NativeLocation.class);
 228                 assertNotNull(loc);
 229                 verifyNativeLocation(loc, expected.getAnnotation(NativeLocation.class));
 230             }
 231         } else {
 232             NativeHeader ah = actual.getAnnotation(NativeHeader.class);
 233             assertNotNull(ah);
 234             NativeHeader eh = expected.getAnnotation(NativeHeader.class);
 235             assertNotNull(eh);
 236 
 237             assertEquals(Paths.get(ah.path()).getFileName(),
 238                     Paths.get(eh.path()).getFileName());
 239         }
 240 
 241     }
 242 
 243     private void AsmCheckClass(String name) {
 244         ClassReader cr = new ClassReader(actualClz.get(name));
 245         PrintWriter pw = new PrintWriter(System.out);
 246         TraceClassVisitor tcv = new TraceClassVisitor(
 247                 new CheckClassAdapter(new ClassNode()),
 248                 new Textifier(), pw);
 249         try {
 250             cr.accept(tcv, ClassReader.SKIP_FRAMES);
 251         } catch (Throwable e) {
 252             tcv.p.print(pw);
 253             e.printStackTrace(pw);
 254         }
 255         pw.flush();
 256     }
 257 
 258     private void AsmifyClass(String name) throws IOException {
 259         ClassReader cr = new ClassReader(mfm.getClassData(name));
 260         PrintWriter pw = new PrintWriter(System.out);
 261         TraceClassVisitor tcv = new TraceClassVisitor(
 262                 new CheckClassAdapter(new ClassNode()),
 263                 new ASMifier(), pw);
 264         try {
 265             cr.accept(tcv, ClassReader.SKIP_FRAMES);
 266         } catch (Throwable e) {
 267             tcv.p.print(pw);
 268             e.printStackTrace(pw);
 269         }
 270         pw.flush();
 271     }
 272 
 273     @DataProvider(name = "classes")
 274     public Object[][] loadClasses() throws ClassNotFoundException, IOException {
 275         // compile();
 276         if (clz_data == null) {
 277             String[] clz_names = mfm.listClasses().toArray(new String[0]);
 278             final int len = clz_names.length;
 279             clz_data = new Object[len][2];
 280             for (int i = 0; i < len; i++) {
 281                 String name = clz_names[i];
 282                 System.err.println("Load class " + name);
 283                 // AsmCheckClass(name);
 284                 // AsmifyClass(name);
 285                 clz_data[i][0] = actualCL.loadClass(name);
 286                 clz_data[i][1] = expectedCL.loadClass(name);
 287             }
 288         }
 289         return clz_data;
 290     }
 291 
 292     @BeforeClass
 293     public void compile() throws IOException {
 294         System.out.println("Compiling...");
 295         mfm = compileJavaCode();
 296         writer = extract();
 297         actualClz = writer.results();
 298         expectedCL = mfm.getTheClassLoader();
 299         actualCL = new ClassLoader() {
 300             @Override
 301             protected Class<?> findClass(String name) throws ClassNotFoundException {
 302                 byte[] byteCode = actualClz.get(canonicalize(name));
 303                 if (byteCode == null) throw new ClassNotFoundException(name);
 304                 return defineClass(name, byteCode, 0, byteCode.length);
 305             }
 306         };
 307         System.out.println("Done compile, ready for test");
 308         assertEquals(actualClz.keySet().stream()
 309                         .map(Runner::normalize)
 310                         .collect(Collectors.toSet()),
 311                 mfm.listClasses());
 312         System.out.println("Compile result validated.");
 313     }
 314 
 315     private static String normalize(String classname) {
 316         return classname.replace('/', '.');
 317     }
 318 
 319     private static String canonicalize(String classname) {
 320         return classname.replace('.', '/');
 321     }
 322 
 323     private static Path[] paths(String testDir, String[] files, boolean platformDependent) {
 324         boolean isWindows = System.getProperty("os.name").startsWith("Windows");
 325         return Arrays.stream(files)
 326                 .map(f -> Paths.get(testDir, "compare", isWindows && platformDependent ? "windows" : "", f))
 327                 .toArray(Path[]::new);
 328     }
 329 
 330     @Factory(dataProvider = "cases")
 331     public static Object[] run(String nativeSrc, String pkgName, String[] javaSrcFiles, boolean platformDependent) {
 332         String testDir = System.getProperty("test.src", ".");
 333         System.out.println("Use test case files in " + testDir);
 334         return new Object[] {
 335             new Runner(Paths.get(testDir, nativeSrc), pkgName, paths(testDir, javaSrcFiles, platformDependent))
 336         };
 337     }
 338 
 339     @DataProvider(name = "cases")
 340     public static Object[][] cases() {
 341         return new Object[][] {
 342             { "simple.h", "com.acme", new String[] { "simple.java" }, true},
 343             { "recursive.h", "com.acme", new String[] { "recursive.java" }, false},
 344             { "TypedefAnonStruct.h", "com.acme", new String[] { "TypedefAnonStruct.java" }, false},
 345             { "pad.h", "com.acme", new String[] { "pad.java" }, false},
 346             { "bitfields.h", "com.acme", new String[] { "bitfields.java" }, true},
 347             { "globalFuncPointer.h", "com.acme", new String[] { "globalFuncPointer.java" }, false}
 348         };
 349     }
 350 }