2018-09-28 18:39:58 +02:00

342 lines
12 KiB
Java

/*
* 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 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 wastFile;
private File nodeScript;
private File spiderMonkeyScript;
private File nodeWastScript;
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 );
}
}
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 );
}
textCompiled = wasm.compileToText();
try {
create();
wastFile = newFile( "test.wast" );
try( FileOutputStream stream = new FileOutputStream( wastFile ) ) {
stream.write( textCompiled.getBytes( StandardCharsets.UTF_8 ) );
}
wasmFile = newFile( "test.wasm" );
wasm.compileToBinary( wasmFile );
nodeScript = createScript( "nodetest.js" );
spiderMonkeyScript = createScript( "SpiderMonkeyTest.js" );
nodeWastScript = createScript( "WastTest.js" );
ProcessBuilder processBuilder = IS_WINDOWS ? new ProcessBuilder( "cmd", "/C", "npm", "install", "wabt@nightly" ) : new ProcessBuilder( "npm", "install", "wabt@nightly" );
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.wast}", wastFile.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 );
} 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() );
}
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();
break;
case NodeJS:
processBuilder = nodeJsCommand( nodeScript );
break;
case NodeWast:
processBuilder = nodeJsCommand( nodeWastScript );
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( 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() throws IOException {
return new ProcessBuilder( spiderMonkey.getCommand(), spiderMonkeyScript.getAbsolutePath() );
}
/**
* 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 = System.getProperty( "node.dir" );
if( command == null ) {
command = "node";
} else {
if( IS_WINDOWS ) {
command += "/node";
} else {
command += "/bin/node";
}
}
return new ProcessBuilder( command, "--experimental-wasm-se", "--experimental-wasm-sat-f2i-conversions", script.getAbsolutePath() );
}
/**
* 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 <T extends Throwable> void throwException( Throwable exception ) throws T {
throw (T)exception;
}
}