/* * Copyright 2017 - 2018 Volker Berlin (i-net software) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.inetsoftware.jwebassembly; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.ProcessBuilder.Redirect; import java.lang.reflect.Method; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Scanner; import javax.annotation.Nonnull; import org.junit.rules.TemporaryFolder; /** * A Junit Rule that compile the given classes and compare the results from node and Java. * * @author Volker Berlin */ public class WasmRule extends TemporaryFolder { private static final boolean IS_WINDOWS = System.getProperty( "os.name" ).toLowerCase().indexOf( "win" ) >= 0; private static final SpiderMonkey spiderMonkey = new SpiderMonkey(); private final Class[] classes; private File wasmFile; private File watFile; private File nodeScript; private File spiderMonkeyScript; private File nodeWatScript; private File spiderMonkeyWatScript; private boolean failed; private String textCompiled; /** * Compile the given classes to a Wasm and save it to a file. * * @param classes * list of classes to compile */ public WasmRule( Class... classes ) { if( classes == null || classes.length == 0 ) { throw new IllegalArgumentException( "You need to set minimum one test class" ); } this.classes = classes; } /** * {@inheritDoc} */ @Override protected void before() throws Throwable { compile(); } /** * {@inheritDoc} */ @Override protected void after() { super.after(); if( failed ) { System.out.println( textCompiled ); } } /** * Compile the classes of the test. */ public void compile() throws IOException, WasmException { JWebAssembly wasm = new JWebAssembly(); for( Class clazz : classes ) { URL url = clazz.getResource( '/' + clazz.getName().replace( '.', '/' ) + ".class" ); wasm.addFile( url ); } wasm.setProperty( JWebAssembly.DEBUG_NAMES, "true" ); textCompiled = wasm.compileToText(); try { create(); watFile = newFile( "test.wat" ); try( FileOutputStream stream = new FileOutputStream( watFile ) ) { stream.write( textCompiled.getBytes( StandardCharsets.UTF_8 ) ); } wasmFile = newFile( "test.wasm" ); wasm.compileToBinary( wasmFile ); nodeScript = createScript( "nodetest.js" ); spiderMonkeyScript = createScript( "SpiderMonkeyTest.js" ); nodeWatScript = createScript( "WatTest.js" ); spiderMonkeyWatScript = createScript( "SpiderMonkeyWatTest.js" ); //create dummy files to prevent error messages FileOutputStream jsonPackage = new FileOutputStream( new File( getRoot(), "package.json" ) ); jsonPackage.write( "{\"name\":\"test\", \"description\": \"description\", \"license\": \"Apache-2.0\", \"repository\": {}}".getBytes() ); jsonPackage.close(); jsonPackage = new FileOutputStream( new File( getRoot(), "package-lock.json" ) ); jsonPackage.write( "{\"lockfileVersion\": 1}".getBytes() ); jsonPackage.close(); ProcessBuilder processBuilder = new ProcessBuilder( "npm", "install", "wabt@nightly" ); if( IS_WINDOWS ) { processBuilder.command().add( 0, "cmd" ); processBuilder.command().add( 1, "/C" ); } processBuilder.directory( getRoot() ); processBuilder.redirectOutput( Redirect.INHERIT ); processBuilder.redirectError( Redirect.INHERIT ); System.out.println( String.join( " ", processBuilder.command() ) ); Process process = processBuilder.start(); int exitCode = process.waitFor(); if( exitCode != 0 ) { fail( readStream( process.getErrorStream() ) ); } } catch( Throwable ex ) { System.out.println( textCompiled ); throwException( ex ); } } /** * Load a script resource, patch it and save it * * @param name * the resource name * @return The saved file name * @throws IOException * if any IO error occur */ private File createScript( String name ) throws IOException { File file = newFile( name ); URL scriptUrl = getClass().getResource( name ); String expected = readStream( scriptUrl.openStream() ); expected = expected.replace( "{test.wasm}", wasmFile.getName() ); expected = expected.replace( "{test.wat}", watFile.getName() ); try (FileOutputStream scriptStream = new FileOutputStream( file )) { scriptStream.write( expected.getBytes( StandardCharsets.UTF_8 ) ); } return file; } /** * Run a test single test. It run the method in Java and call it via node in the WenAssembly. If the result are * different it fire an error. * * @param script * The script engine * @param methodName * the method name of the test. * @param params * the parameters for the method */ public void test( ScriptEngine script, String methodName, Object... params ) { Object expected; try { Class[] types = new Class[params.length]; for( int i = 0; i < types.length; i++ ) { Class type = params[i].getClass(); switch( type.getName() ) { case "java.lang.Byte": type = byte.class; break; case "java.lang.Short": type = short.class; break; case "java.lang.Character": type = char.class; break; case "java.lang.Integer": type = int.class; break; case "java.lang.Long": type = long.class; break; case "java.lang.Float": type = float.class; break; case "java.lang.Double": type = double.class; break; } types[i] = type; } Method method = null; for( int i = 0; i < classes.length; i++ ) { try { Class clazz = classes[i]; method = clazz.getDeclaredMethod( methodName, types ); break; } catch( NoSuchMethodException ex ) { if( i == classes.length - 1 ) { throw ex; } } } method.setAccessible( true ); expected = method.invoke( null, params ); if( expected instanceof Character ) { // WASM does not support char that it is number expected = new Integer( ((Character)expected).charValue() ); } if( expected instanceof Boolean ) { // WASM does not support boolean that it is number expected = new Integer( ((Boolean)expected) ? 1 : 0 ); } for( int i = 0; i < params.length; i++ ) { if( params[i] instanceof Character ) { params[i] = new Integer( ((Character)params[i]).charValue() ); } } String actual = evalWasm( script, methodName, params ); assertEquals( String.valueOf( expected ), actual ); } catch( Throwable ex ) { failed = true; throwException( ex ); return; } } /** * Evaluate the wasm exported function. * * @param script * The script engine * @param methodName * the method name of the test. * @param params * the parameters for the method * @return the output of the script */ public String evalWasm( ScriptEngine script, String methodName, Object... params ) { ProcessBuilder processBuilder = null; try { switch( script ) { case SpiderMonkey: processBuilder = spiderMonkeyCommand( spiderMonkeyScript ); break; case SpiderMonkeyWat: processBuilder = spiderMonkeyCommand( spiderMonkeyWatScript ); break; case NodeJS: processBuilder = nodeJsCommand( nodeScript ); break; case NodeWat: processBuilder = nodeJsCommand( nodeWatScript ); break; default: throw new IllegalStateException( script.toString() ); } processBuilder.command().add( methodName ); for( int i = 0; i < params.length; i++ ) { processBuilder.command().add( String.valueOf( params[i] ) ); } processBuilder.directory( getRoot() ); Process process = processBuilder.start(); int exitCode = process.waitFor(); String result = readStream( process.getInputStream() ).trim(); if( exitCode != 0 ) { String errorMessage = readStream( process.getErrorStream() ); assertEquals( result + '\n' + errorMessage, 0, exitCode ); } return result; } catch( Throwable ex ) { failed = true; ex.printStackTrace(); if( processBuilder != null ) { String executable = processBuilder.command().get( 0 ); System.err.println( executable ); File exec = new File(executable); System.err.println( exec.exists() ); exec = exec.getParentFile(); if( exec != null ) { System.err.println( Arrays.toString( exec.list() ) ); } } throwException( ex ); return null; } } /** * Create a ProcessBuilder for spider monkey script shell. * * @return the value from the script * @throws IOException * if the download failed */ private ProcessBuilder spiderMonkeyCommand( File script ) throws IOException { return new ProcessBuilder( spiderMonkey.getCommand(), "--wasm-gc", script.getAbsolutePath() ); } /** * The executable of the node command. * * @return the node executable */ @Nonnull private static String nodeExecuable() { String command = System.getProperty( "node.dir" ); if( command == null ) { command = "node"; } else { if( IS_WINDOWS ) { command += "/node"; } else { command += "/bin/node"; } } return command; } /** * Create a ProcessBuilder for node.js * * @param script * the path to the script that should be executed * @return the value from the script */ private static ProcessBuilder nodeJsCommand( File script ) { String command = nodeExecuable(); // details see with command: node --v8-options ProcessBuilder processBuilder = new ProcessBuilder( command, "--experimental-wasm-mv", "--experimental-wasm-se", "--experimental-wasm-sat-f2i-conversions", "--experimental-wasm-eh", "--experimental-wasm-anyref", script.getName() ); if( IS_WINDOWS ) { processBuilder.command().add( 0, "cmd" ); processBuilder.command().add( 1, "/C" ); } return processBuilder; } /** * Reads a stream into a String. * * @param input * the InputStream * @return the string */ public static String readStream( InputStream input ) { try (Scanner scanner = new Scanner( input ).useDelimiter( "\\A" )) { return scanner.hasNext() ? scanner.next() : ""; } } /** * Throw any exception independent of signatures * * @param exception * the exception * @throws T * a generic helper */ public static void throwException( Throwable exception ) throws T { throw (T)exception; } }