com.oracle.truffle.api.instrumentation.test.InstrumentationTest Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of truffle-instrument-test Show documentation
Show all versions of truffle-instrument-test Show documentation
Instrumentation tests including InstrumentationTestLanguage.
The newest version!
/*
* Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.oracle.truffle.api.instrumentation.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.Reader;
import java.nio.CharBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.Assert;
import org.junit.Test;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.InstrumentInfo;
import com.oracle.truffle.api.Truffle;
import com.oracle.truffle.api.TruffleLanguage;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.instrumentation.AllocationEvent;
import com.oracle.truffle.api.instrumentation.AllocationEventFilter;
import com.oracle.truffle.api.instrumentation.EventContext;
import com.oracle.truffle.api.instrumentation.ExecutionEventListener;
import com.oracle.truffle.api.instrumentation.ExecutionEventNode;
import com.oracle.truffle.api.instrumentation.ExecutionEventNodeFactory;
import com.oracle.truffle.api.instrumentation.Instrumenter;
import com.oracle.truffle.api.instrumentation.ProvidedTags;
import com.oracle.truffle.api.instrumentation.SourceSectionFilter;
import com.oracle.truffle.api.instrumentation.StandardTags;
import com.oracle.truffle.api.instrumentation.TruffleInstrument;
import com.oracle.truffle.api.instrumentation.TruffleInstrument.Registration;
import com.oracle.truffle.api.nodes.DirectCallNode;
import com.oracle.truffle.api.nodes.LanguageInfo;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.nodes.RootNode;
import com.oracle.truffle.api.instrumentation.AllocationListener;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.Instrument;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Source;
public class InstrumentationTest extends AbstractInstrumentationTest {
/*
* Test that metadata is properly propagated to Instrument handles.
*/
@Test
public void testMetadata() {
Instrument instrumentHandle1 = engine.getInstruments().get("testMetadataType1");
Assert.assertEquals("name", instrumentHandle1.getName());
Assert.assertEquals("version", instrumentHandle1.getVersion());
Assert.assertEquals("testMetadataType1", instrumentHandle1.getId());
Assert.assertFalse(isInitialized(instrumentHandle1));
Assert.assertFalse(isCreated(instrumentHandle1));
}
@Registration(name = "name", version = "version", id = "testMetadataType1")
public static class MetadataInstrument extends TruffleInstrument {
@Override
protected void onCreate(Env env) {
}
}
@Registration(name = "name", version = "version", id = "testBrokenRegistration", services = Runnable.class)
public static class BrokenRegistrationInstrument extends TruffleInstrument {
@Override
protected void onCreate(Env env) {
}
}
@Test
public void forgetsToRegisterADeclaredService() throws Exception {
Instrument handle = engine.getInstruments().get("testBrokenRegistration");
assertNotNull(handle);
Runnable r = handle.lookup(Runnable.class);
assertNull("The service isn't there", r);
if (!err.toString().contains("declares service java.lang.Runnable but doesn't register it")) {
fail(err.toString());
}
}
@Registration(name = "name", version = "version", id = "beforeUse", services = Runnable.class)
public static class BeforeUseInstrument extends TruffleInstrument implements Runnable {
private Env env;
@Override
protected void onCreate(Env anEnv) {
this.env = anEnv;
this.env.registerService(this);
}
@Override
public void run() {
LanguageInfo info = env.getLanguages().get(InstrumentationTestLanguage.ID);
SpecialService ss = env.lookup(info, SpecialService.class);
assertNotNull("Service found", ss);
assertEquals("The right extension", ss.fileExtension(), InstrumentationTestLanguage.FILENAME_EXTENSION);
assertNull("Can't query object", env.lookup(info, Object.class));
assertNull("Can't query language", env.lookup(info, TruffleLanguage.class));
}
}
@Test
public void queryInstrumentsBeforeUseAndObtainSpecialService() throws Exception {
engine = Engine.newBuilder().err(err).build();
Runnable start = null;
for (Instrument instr : engine.getInstruments().values()) {
Runnable r = instr.lookup(Runnable.class);
if (r != null) {
start = r;
start.run();
assertTrue("Now enabled: " + instr, isCreated(instr));
}
}
assertNotNull("At least one Runnable found", start);
}
@Test(expected = IllegalStateException.class) // IllegalStateException: Engine is already closed
public void queryInstrumentsAfterDisposeDoesnotEnable() throws Exception {
engine = Engine.newBuilder().err(err).build();
engine.close();
Runnable start = null;
for (Instrument instr : engine.getInstruments().values()) {
assertFalse("Instrument is disabled", isInitialized(instr));
Runnable r = instr.lookup(Runnable.class);
if (r != null) {
start = r;
start.run();
assertTrue("Now enabled: " + instr, isCreated(instr));
}
assertFalse("Instrument left disabled", isInitialized(instr));
}
assertNull("No Runnable found", start);
}
/*
* Test that metadata is properly propagated to Instrument handles.
*/
@Test
public void testDefaultId() {
Instrument descriptor1 = engine.getInstruments().get(MetadataInstrument2.class.getSimpleName());
Assert.assertEquals("", descriptor1.getName());
Assert.assertEquals("", descriptor1.getVersion());
Assert.assertEquals(MetadataInstrument2.class.getSimpleName(), descriptor1.getId());
Assert.assertFalse(isInitialized(descriptor1));
}
@Registration
public static class MetadataInstrument2 extends TruffleInstrument {
@Override
protected void onCreate(Env env) {
}
}
/*
* Test onCreate and onDispose invocations for multiple instrument instances.
*/
@Test
public void testMultipleInstruments() throws IOException {
run(""); // initialize
MultipleInstanceInstrument.onCreateCounter = 0;
MultipleInstanceInstrument.onDisposeCounter = 0;
MultipleInstanceInstrument.constructor = 0;
Instrument instrument1 = engine.getInstruments().get("testMultipleInstruments");
instrument1.lookup(Object.class); // enabled
Assert.assertEquals(1, MultipleInstanceInstrument.constructor);
Assert.assertEquals(1, MultipleInstanceInstrument.onCreateCounter);
Assert.assertEquals(0, MultipleInstanceInstrument.onDisposeCounter);
Instrument instrument2 = engine.getInstruments().get("testMultipleInstruments");
instrument2.lookup(Object.class); // the same enabled
Assert.assertEquals(1, MultipleInstanceInstrument.constructor);
Assert.assertEquals(1, MultipleInstanceInstrument.onCreateCounter);
Assert.assertEquals(0, MultipleInstanceInstrument.onDisposeCounter);
engine.close();
engine = null;
Assert.assertEquals(1, MultipleInstanceInstrument.constructor);
Assert.assertEquals(1, MultipleInstanceInstrument.onCreateCounter);
Assert.assertEquals(1, MultipleInstanceInstrument.onDisposeCounter);
}
@Registration(id = "testMultipleInstruments", services = Object.class)
public static class MultipleInstanceInstrument extends TruffleInstrument {
private static int onCreateCounter = 0;
private static int onDisposeCounter = 0;
private static int constructor = 0;
public MultipleInstanceInstrument() {
constructor++;
}
@Override
protected void onCreate(Env env) {
// Not to get error: declares service, but doesn't register it
env.registerService(new Object());
onCreateCounter++;
}
@Override
protected void onDispose(Env env) {
onDisposeCounter++;
}
}
/*
* Test exceptions from language instrumentation are not wrapped into InstrumentationExceptions.
* Test that one language cannot instrument another.
*/
@Test
public void testLanguageInstrumentationAndExceptions() throws IOException {
TestLanguageInstrumentationLanguage.installInstrumentsCounter = 0;
TestLanguageInstrumentationLanguage.createContextCounter = 0;
try {
context.eval(Source.create("test-language-instrumentation-language", "ROOT(EXPRESSION)"));
Assert.fail("expected exception");
} catch (PolyglotException ex) {
// we assert that MyLanguageException is not wrapped
assertEquals(MyLanguageException.class.getName(), ex.getMessage());
}
Assert.assertEquals(1, TestLanguageInstrumentationLanguage.installInstrumentsCounter);
Assert.assertEquals(1, TestLanguageInstrumentationLanguage.createContextCounter);
// this should run isolated from the language instrumentation.
run("STATEMENT");
}
@SuppressWarnings("serial")
private static class MyLanguageException extends RuntimeException {
}
@TruffleLanguage.Registration(id = "test-language-instrumentation-language", name = "", version = "", mimeType = "testLanguageInstrumentation")
@ProvidedTags({InstrumentationTestLanguage.ExpressionNode.class, StandardTags.StatementTag.class})
public static class TestLanguageInstrumentationLanguage extends InstrumentationTestLanguage {
static int installInstrumentsCounter = 0;
static int createContextCounter = 0;
public TestLanguageInstrumentationLanguage() {
}
private static void installInstruments(Instrumenter instrumenter) {
installInstrumentsCounter++;
instrumenter.attachListener(SourceSectionFilter.newBuilder().tagIs(InstrumentationTestLanguage.EXPRESSION).build(), new ExecutionEventListener() {
public void onReturnValue(EventContext context, VirtualFrame frame, Object result) {
}
public void onReturnExceptional(EventContext context, VirtualFrame frame, Throwable exception) {
}
public void onEnter(EventContext context, VirtualFrame frame) {
// since we are a language instrumentation we can throw exceptions
// without getting wrapped into Instrumentation exception.
throw new MyLanguageException();
}
});
instrumenter.attachListener(SourceSectionFilter.newBuilder().tagIs(InstrumentationTestLanguage.STATEMENT).build(), new ExecutionEventListener() {
public void onReturnValue(EventContext context, VirtualFrame frame, Object result) {
throw new AssertionError();
}
public void onReturnExceptional(EventContext context, VirtualFrame frame, Throwable exception) {
throw new AssertionError();
}
public void onEnter(EventContext context, VirtualFrame frame) {
throw new AssertionError();
}
});
}
@Override
protected Context createContext(com.oracle.truffle.api.TruffleLanguage.Env env) {
createContextCounter++;
Instrumenter instrumenter = env.lookup(Instrumenter.class);
Assert.assertNotNull("Instrumenter found", instrumenter);
installInstruments(instrumenter);
return super.createContext(env);
}
@Override
protected CallTarget parse(ParsingRequest request) {
return Truffle.getRuntime().createCallTarget(new RootNode(this) {
@Child private BaseNode base = parse(request.getSource());
@Override
public Object execute(VirtualFrame frame) {
return base.execute(frame);
}
});
}
@Override
protected boolean isObjectOfLanguage(Object object) {
return false;
}
}
@Test
public void testInstrumentException1() {
assureEnabled(engine.getInstruments().get("testInstrumentException1"));
Assert.assertTrue(getErr().contains("MyLanguageException"));
}
@Registration(name = "", version = "", id = "testInstrumentException1", services = Object.class)
public static class TestInstrumentException1 extends TruffleInstrument {
@Override
protected void onCreate(Env env) {
throw new MyLanguageException();
}
@Override
protected void onDispose(Env env) {
}
}
/*
* We test that instrumentation exceptions are wrapped, onReturnExceptional is invoked properly
* and not onReturnValue,
*/
@Test
public void testInstrumentException2() throws IOException {
TestInstrumentException2.returnedExceptional = 0;
TestInstrumentException2.returnedValue = 0;
assureEnabled(engine.getInstruments().get("testInstrumentException2"));
run("ROOT(EXPRESSION)");
Assert.assertTrue(getErr().contains("MyLanguageException"));
Assert.assertEquals(0, TestInstrumentException2.returnedExceptional);
Assert.assertEquals(1, TestInstrumentException2.returnedValue);
}
@Registration(name = "", version = "", id = "testInstrumentException2", services = Object.class)
public static class TestInstrumentException2 extends TruffleInstrument {
static int returnedExceptional = 0;
static int returnedValue = 0;
@Override
protected void onCreate(Env env) {
// Not to get error: declares service, but doesn't register it
env.registerService(new Object());
env.getInstrumenter().attachListener(SourceSectionFilter.newBuilder().tagIs(InstrumentationTestLanguage.EXPRESSION).build(), new ExecutionEventListener() {
public void onReturnValue(EventContext context, VirtualFrame frame, Object result) {
returnedValue++;
}
public void onReturnExceptional(EventContext context, VirtualFrame frame, Throwable exception) {
returnedExceptional++;
}
public void onEnter(EventContext context, VirtualFrame frame) {
throw new MyLanguageException();
}
});
}
@Override
protected void onDispose(Env env) {
}
}
/*
* Test that instrumentation exceptions in the onReturnExceptional are attached as suppressed
* exceptions.
*/
@Test
public void testInstrumentException3() throws IOException {
TestInstrumentException3.returnedExceptional = 0;
TestInstrumentException3.onEnter = 0;
assureEnabled(engine.getInstruments().get("testInstrumentException3"));
run("ROOT(EXPRESSION)");
Assert.assertTrue(getErr().contains("MyLanguageException"));
Assert.assertEquals(0, TestInstrumentException3.returnedExceptional);
Assert.assertEquals(1, TestInstrumentException3.onEnter);
}
@Registration(name = "", version = "", id = "testInstrumentException3", services = Object.class)
public static class TestInstrumentException3 extends TruffleInstrument {
static int returnedExceptional = 0;
static int onEnter = 0;
@Override
protected void onCreate(Env env) {
// Not to get error: declares service, but doesn't register it
env.registerService(new Object());
env.getInstrumenter().attachListener(SourceSectionFilter.newBuilder().tagIs(InstrumentationTestLanguage.EXPRESSION).build(), new ExecutionEventListener() {
public void onReturnValue(EventContext context, VirtualFrame frame, Object result) {
throw new MyLanguageException();
}
public void onReturnExceptional(EventContext context, VirtualFrame frame, Throwable exception) {
returnedExceptional++;
}
public void onEnter(EventContext context, VirtualFrame frame) {
onEnter++;
}
});
}
}
/*
* Test that event nodes are created lazily on first execution.
*/
@Test
public void testLazyProbe1() throws IOException {
TestLazyProbe1.createCalls = 0;
TestLazyProbe1.onEnter = 0;
TestLazyProbe1.onReturnValue = 0;
TestLazyProbe1.onReturnExceptional = 0;
assureEnabled(engine.getInstruments().get("testLazyProbe1"));
run("ROOT(DEFINE(foo, EXPRESSION))");
run("ROOT(DEFINE(bar, ROOT(EXPRESSION,EXPRESSION)))");
Assert.assertEquals(0, TestLazyProbe1.createCalls);
Assert.assertEquals(0, TestLazyProbe1.onEnter);
Assert.assertEquals(0, TestLazyProbe1.onReturnValue);
Assert.assertEquals(0, TestLazyProbe1.onReturnExceptional);
run("ROOT(CALL(foo))");
Assert.assertEquals(1, TestLazyProbe1.createCalls);
Assert.assertEquals(1, TestLazyProbe1.onEnter);
Assert.assertEquals(1, TestLazyProbe1.onReturnValue);
Assert.assertEquals(0, TestLazyProbe1.onReturnExceptional);
run("ROOT(CALL(bar))");
Assert.assertEquals(3, TestLazyProbe1.createCalls);
Assert.assertEquals(3, TestLazyProbe1.onEnter);
Assert.assertEquals(3, TestLazyProbe1.onReturnValue);
Assert.assertEquals(0, TestLazyProbe1.onReturnExceptional);
run("ROOT(CALL(bar))");
Assert.assertEquals(3, TestLazyProbe1.createCalls);
Assert.assertEquals(5, TestLazyProbe1.onEnter);
Assert.assertEquals(5, TestLazyProbe1.onReturnValue);
Assert.assertEquals(0, TestLazyProbe1.onReturnExceptional);
run("ROOT(CALL(foo))");
Assert.assertEquals(3, TestLazyProbe1.createCalls);
Assert.assertEquals(6, TestLazyProbe1.onEnter);
Assert.assertEquals(6, TestLazyProbe1.onReturnValue);
Assert.assertEquals(0, TestLazyProbe1.onReturnExceptional);
}
@Registration(name = "", version = "", id = "testLazyProbe1", services = Object.class)
public static class TestLazyProbe1 extends TruffleInstrument {
static int createCalls = 0;
static int onEnter = 0;
static int onReturnValue = 0;
static int onReturnExceptional = 0;
@Override
protected void onCreate(Env env) {
// Not to get error: declares service, but doesn't register it
env.registerService(new Object());
env.getInstrumenter().attachFactory(SourceSectionFilter.newBuilder().tagIs(InstrumentationTestLanguage.EXPRESSION).build(), new ExecutionEventNodeFactory() {
public ExecutionEventNode create(EventContext context) {
createCalls++;
return new ExecutionEventNode() {
@Override
public void onReturnValue(VirtualFrame frame, Object result) {
onReturnValue++;
}
@Override
public void onReturnExceptional(VirtualFrame frame, Throwable exception) {
onReturnExceptional++;
}
@Override
public void onEnter(VirtualFrame frame) {
onEnter++;
}
};
}
});
}
}
/*
* Test that parsing and executing foreign languages work.
*/
@Test
public void testEnvParse1() throws IOException {
TestEnvParse1.onExpression = 0;
TestEnvParse1.onStatement = 0;
assureEnabled(engine.getInstruments().get("testEnvParse1"));
run("STATEMENT");
Assert.assertEquals(1, TestEnvParse1.onExpression);
Assert.assertEquals(1, TestEnvParse1.onStatement);
run("STATEMENT");
Assert.assertEquals(2, TestEnvParse1.onExpression);
Assert.assertEquals(2, TestEnvParse1.onStatement);
}
@Registration(name = "", version = "", id = "testEnvParse1", services = Object.class)
public static class TestEnvParse1 extends TruffleInstrument {
static int onExpression = 0;
static int onStatement = 0;
@Override
protected void onCreate(final Env env) {
// Not to get error: declares service, but doesn't register it
env.registerService(new Object());
env.getInstrumenter().attachFactory(SourceSectionFilter.newBuilder().tagIs(InstrumentationTestLanguage.STATEMENT).build(), new ExecutionEventNodeFactory() {
public ExecutionEventNode create(EventContext context) {
final CallTarget target;
try {
target = env.parse(com.oracle.truffle.api.source.Source.newBuilder("EXPRESSION").name("unknown").mimeType(InstrumentationTestLanguage.MIME_TYPE).build());
} catch (Exception e) {
throw new AssertionError();
}
return new ExecutionEventNode() {
@Child private DirectCallNode directCall = Truffle.getRuntime().createDirectCallNode(target);
@Override
public void onEnter(VirtualFrame frame) {
onStatement++;
directCall.call(new Object[0]);
}
};
}
});
env.getInstrumenter().attachListener(SourceSectionFilter.newBuilder().tagIs(InstrumentationTestLanguage.EXPRESSION).build(), new ExecutionEventListener() {
public void onReturnValue(EventContext context, VirtualFrame frame, Object result) {
}
public void onReturnExceptional(EventContext context, VirtualFrame frame, Throwable exception) {
}
public void onEnter(EventContext context, VirtualFrame frame) {
onExpression++;
}
});
}
}
/*
* Test that parsing and executing foreign languages with context work.
*/
@Test
public void testEnvParse2() throws IOException {
TestEnvParse2.onExpression = 0;
TestEnvParse2.onStatement = 0;
assureEnabled(engine.getInstruments().get("testEnvParse2"));
run("STATEMENT");
Assert.assertEquals(1, TestEnvParse2.onExpression);
Assert.assertEquals(1, TestEnvParse2.onStatement);
run("STATEMENT");
Assert.assertEquals(2, TestEnvParse2.onExpression);
Assert.assertEquals(2, TestEnvParse2.onStatement);
}
@Registration(name = "", version = "", id = "testEnvParse2", services = Object.class)
public static class TestEnvParse2 extends TruffleInstrument {
static int onExpression = 0;
static int onStatement = 0;
@Override
protected void onCreate(final Env env) {
// Not to get error: declares service, but doesn't register it
env.registerService(new Object());
env.getInstrumenter().attachFactory(SourceSectionFilter.newBuilder().tagIs(InstrumentationTestLanguage.STATEMENT).build(), new ExecutionEventNodeFactory() {
public ExecutionEventNode create(EventContext context) {
final CallTarget target;
try {
target = context.parseInContext(com.oracle.truffle.api.source.Source.newBuilder("EXPRESSION").name("unknown").mimeType(InstrumentationTestLanguage.MIME_TYPE).build());
} catch (IOException e) {
throw new AssertionError();
}
return new ExecutionEventNode() {
@Child private DirectCallNode directCall = Truffle.getRuntime().createDirectCallNode(target);
@Override
public void onEnter(VirtualFrame frame) {
onStatement++;
directCall.call(new Object[0]);
}
};
}
});
env.getInstrumenter().attachListener(SourceSectionFilter.newBuilder().tagIs(InstrumentationTestLanguage.EXPRESSION).build(), new ExecutionEventListener() {
public void onReturnValue(EventContext context, VirtualFrame frame, Object result) {
}
public void onReturnExceptional(EventContext context, VirtualFrame frame, Throwable exception) {
}
public void onEnter(EventContext context, VirtualFrame frame) {
onExpression++;
}
});
}
}
/*
* Test instrument all with any filter. Ensure that root nodes are not tried to be instrumented.
*/
@Test
public void testInstrumentAll() throws IOException {
TestInstrumentAll1.onStatement = 0;
assureEnabled(engine.getInstruments().get("testInstrumentAll"));
run("STATEMENT");
// An implicit root node + statement
Assert.assertEquals(2, TestInstrumentAll1.onStatement);
}
@Registration(id = "testInstrumentAll", services = Object.class)
public static class TestInstrumentAll1 extends TruffleInstrument {
static int onStatement = 0;
@Override
protected void onCreate(final Env env) {
// Not to get error: declares service, but doesn't register it
env.registerService(new Object());
env.getInstrumenter().attachListener(SourceSectionFilter.ANY, new ExecutionEventListener() {
public void onEnter(EventContext context, VirtualFrame frame) {
onStatement++;
}
public void onReturnExceptional(EventContext context, VirtualFrame frame, Throwable exception) {
}
public void onReturnValue(EventContext context, VirtualFrame frame, Object result) {
}
});
}
}
/*
* Define is not instrumentable but has a source section.
*/
@Test
public void testInstrumentNonInstrumentable() throws IOException {
TestInstrumentNonInstrumentable1.onStatement = 0;
assureEnabled(engine.getInstruments().get("testInstrumentNonInstrumentable"));
run("DEFINE(foo, ROOT())");
// DEFINE is not instrumentable, only ROOT is.
Assert.assertEquals(1, TestInstrumentNonInstrumentable1.onStatement);
}
@Registration(id = "testInstrumentNonInstrumentable", services = Object.class)
public static class TestInstrumentNonInstrumentable1 extends TruffleInstrument {
static int onStatement = 0;
@Override
protected void onCreate(final Env env) {
env.getInstrumenter().attachListener(SourceSectionFilter.ANY, new ExecutionEventListener() {
public void onEnter(EventContext context, VirtualFrame frame) {
onStatement++;
}
public void onReturnExceptional(EventContext context, VirtualFrame frame, Throwable exception) {
}
public void onReturnValue(EventContext context, VirtualFrame frame, Object result) {
}
});
}
}
@Test
public void testOutputConsumer() throws IOException {
// print without instruments
String rout = run("PRINT(OUT, InitialToStdOut)");
Assert.assertEquals("InitialToStdOut", rout);
run("PRINT(ERR, InitialToStdErr)");
Assert.assertEquals("InitialToStdErr", err.toString());
err.reset();
// turn instruments on
assureEnabled(engine.getInstruments().get("testOutputConsumerArray"));
assureEnabled(engine.getInstruments().get("testOutputConsumerPiped"));
context.eval(lines("PRINT(OUT, OutputToStdOut)"));
context.eval(lines("PRINT(ERR, OutputToStdErr)"));
// test that the output goes eveywhere
Assert.assertEquals("OutputToStdOut", getOut());
Assert.assertEquals("OutputToStdOut", TestOutputConsumerArray.getOut());
Assert.assertEquals("OutputToStdErr", getErr());
Assert.assertEquals("OutputToStdErr", TestOutputConsumerArray.getErr());
CharBuffer buff = CharBuffer.allocate(100);
TestOutputConsumerPiped.fromOut.read(buff);
buff.flip();
Assert.assertEquals("OutputToStdOut", buff.toString());
buff.rewind();
TestOutputConsumerPiped.fromErr.read(buff);
buff.flip();
Assert.assertEquals("OutputToStdErr", buff.toString());
buff.rewind();
// close piped err stream and test that print still works
TestOutputConsumerPiped.fromErr.close();
context.eval(lines("PRINT(OUT, MoreOutputToStdOut)"));
context.eval(lines("PRINT(ERR, MoreOutputToStdErr)"));
Assert.assertEquals("OutputToStdOutMoreOutputToStdOut", out.toString());
Assert.assertEquals("OutputToStdOutMoreOutputToStdOut", TestOutputConsumerArray.getOut());
String errorMsg = "java.lang.Exception: Output operation write(B[II) failed for java.io.PipedOutputStream";
Assert.assertTrue(err.toString(), err.toString().startsWith("OutputToStdErr" + errorMsg));
Assert.assertTrue(err.toString(), err.toString().endsWith("MoreOutputToStdErr"));
Assert.assertEquals("OutputToStdErrMoreOutputToStdErr", TestOutputConsumerArray.getErr());
buff.limit(buff.capacity());
TestOutputConsumerPiped.fromOut.read(buff);
buff.flip();
Assert.assertEquals("MoreOutputToStdOut", buff.toString());
out.reset();
err.reset();
// the I/O error is not printed again
context.eval(lines("PRINT(ERR, EvenMoreOutputToStdErr)"));
Assert.assertEquals("EvenMoreOutputToStdErr", err.toString());
Assert.assertEquals("OutputToStdErrMoreOutputToStdErrEvenMoreOutputToStdErr", TestOutputConsumerArray.getErr());
// instruments disabled
engine.close();
engine = null;
out.reset();
err.reset();
engine = getEngine();
context.eval(lines("PRINT(OUT, FinalOutputToStdOut)"));
context.eval(lines("PRINT(ERR, FinalOutputToStdErr)"));
Assert.assertEquals("FinalOutputToStdOut", out.toString());
Assert.assertEquals("FinalOutputToStdErr", err.toString());
// nothing more printed to the disabled instrument
Assert.assertEquals("OutputToStdOutMoreOutputToStdOut", TestOutputConsumerArray.getOut());
Assert.assertEquals("OutputToStdErrMoreOutputToStdErrEvenMoreOutputToStdErr", TestOutputConsumerArray.getErr());
}
@Registration(id = "testOutputConsumerArray", services = Object.class)
public static class TestOutputConsumerArray extends TruffleInstrument {
static ByteArrayOutputStream out = new ByteArrayOutputStream();
static ByteArrayOutputStream err = new ByteArrayOutputStream();
@Override
protected void onCreate(Env env) {
env.getInstrumenter().attachOutConsumer(out);
env.getInstrumenter().attachErrConsumer(err);
// Not to get error: declares service, but doesn't register it
env.registerService(new Object());
}
static String getOut() {
return new String(out.toByteArray());
}
static String getErr() {
return new String(err.toByteArray());
}
}
@Registration(id = "testOutputConsumerPiped", services = Object.class)
public static class TestOutputConsumerPiped extends TruffleInstrument {
static PipedOutputStream out = new PipedOutputStream();
static Reader fromOut;
static PipedOutputStream err = new PipedOutputStream();
static Reader fromErr;
@Override
protected void onCreate(Env env) {
try {
fromOut = new InputStreamReader(new PipedInputStream(out));
fromErr = new InputStreamReader(new PipedInputStream(err));
} catch (IOException ex) {
throw new AssertionError(ex.getLocalizedMessage(), ex);
}
env.getInstrumenter().attachOutConsumer(out);
env.getInstrumenter().attachErrConsumer(err);
// Not to get error: declares service, but doesn't register it
env.registerService(new Object());
}
Reader fromOut() {
return fromOut;
}
Reader fromErr() {
return fromErr;
}
}
/*
* Tests for debugger or any other clients that cancel execution while halted
*/
@Test
public void testKillExceptionOnEnter() throws IOException {
assureEnabled(engine.getInstruments().get("testKillQuitException"));
TestKillQuitException.exceptionOnEnter = new MyKillException();
TestKillQuitException.exceptionOnReturnValue = null;
TestKillQuitException.returnExceptionalCount = 0;
try {
run("STATEMENT");
Assert.fail("KillException in onEnter() cancels engine execution");
} catch (PolyglotException ex) {
assertTrue(ex.getMessage(), ex.getMessage().contains(MyKillException.class.getName()));
}
Assert.assertEquals("KillException is not an execution event", 0, TestKillQuitException.returnExceptionalCount);
}
@Test
public void testKillExceptionOnReturnValue() throws IOException {
assureEnabled(engine.getInstruments().get("testKillQuitException"));
TestKillQuitException.exceptionOnEnter = null;
TestKillQuitException.exceptionOnReturnValue = new MyKillException();
TestKillQuitException.returnExceptionalCount = 0;
try {
run("STATEMENT");
Assert.fail("KillException in onReturnValue() cancels engine execution");
} catch (PolyglotException ex) {
assertTrue(ex.getMessage(), ex.getMessage().contains(MyKillException.class.getName()));
}
Assert.assertEquals("KillException is not an execution event", 0, TestKillQuitException.returnExceptionalCount);
}
@Registration(id = "testKillQuitException", services = Object.class)
public static class TestKillQuitException extends TruffleInstrument {
static Error exceptionOnEnter = null;
static Error exceptionOnReturnValue = null;
static int returnExceptionalCount = 0;
@Override
protected void onCreate(final Env env) {
env.getInstrumenter().attachListener(SourceSectionFilter.ANY, new ExecutionEventListener() {
public void onEnter(EventContext context, VirtualFrame frame) {
if (exceptionOnEnter != null) {
throw exceptionOnEnter;
}
}
public void onReturnValue(EventContext context, VirtualFrame frame, Object result) {
if (exceptionOnReturnValue != null) {
throw exceptionOnReturnValue;
}
}
public void onReturnExceptional(EventContext context, VirtualFrame frame, Throwable exception) {
returnExceptionalCount++;
}
});
}
}
/*
* Use tags that are not declarded as required.
*/
@Test
public void testUsedTagNotRequired1() throws IOException {
TestInstrumentNonInstrumentable1.onStatement = 0;
assureEnabled(engine.getInstruments().get("testUsedTagNotRequired1"));
run("ROOT()");
Assert.assertEquals(0, TestInstrumentNonInstrumentable1.onStatement);
}
@Registration(id = "testUsedTagNotRequired1", services = Object.class)
public static class TestUsedTagNotRequired1 extends TruffleInstrument {
private static class Foobar {
}
@Override
protected void onCreate(final Env env) {
try {
env.getInstrumenter().attachListener(SourceSectionFilter.newBuilder().tagIs(Foobar.class).build(), new ExecutionEventListener() {
public void onEnter(EventContext context, VirtualFrame frame) {
}
public void onReturnExceptional(EventContext context, VirtualFrame frame, Throwable exception) {
}
public void onReturnValue(EventContext context, VirtualFrame frame, Object result) {
}
});
Assert.fail();
} catch (IllegalArgumentException e) {
Assert.assertEquals(
"The attached filter SourceSectionFilter[tag is one of [foobar0]] references the " +
"following tags [foobar0] which are not declared as required by the instrument. To fix " +
"this annotate the instrument class com.oracle.truffle.api.instrumentation." +
"InstrumentationTest$TestUsedTagNotRequired1 with @RequiredTags({foobar0}).",
e.getMessage());
}
}
}
/*
* Test behavior of queryTags when used with instruments
*/
@Test
public void testQueryTags1() throws IOException {
Instrument instrument = engine.getInstruments().get("testIsNodeTaggedWith1");
Instrumenter instrumenter = instrument.lookup(Instrumenter.class);
TestIsNodeTaggedWith1.expressionNode = null;
TestIsNodeTaggedWith1.statementNode = null;
Assert.assertTrue(instrumenter.queryTags(new Node() {
}).isEmpty());
run("STATEMENT(EXPRESSION)");
assertTags(instrumenter.queryTags(TestIsNodeTaggedWith1.expressionNode), InstrumentationTestLanguage.EXPRESSION);
assertTags(instrumenter.queryTags(TestIsNodeTaggedWith1.statementNode), InstrumentationTestLanguage.STATEMENT);
try {
instrumenter.queryTags(null);
Assert.fail();
} catch (NullPointerException e) {
}
}
private static void assertTags(Set> tags, Class>... expectedTags) {
Assert.assertEquals(expectedTags.length, tags.size());
for (Class> clazz : expectedTags) {
Assert.assertTrue("Tag: " + clazz, tags.contains(clazz));
}
}
/*
* Test behavior of queryTags when used with languages
*/
@Test
public void testQueryTags2() throws IOException {
Instrument instrument = engine.getInstruments().get("testIsNodeTaggedWith1");
assureEnabled(instrument);
TestIsNodeTaggedWith1.expressionNode = null;
TestIsNodeTaggedWith1.statementNode = null;
TestIsNodeTaggedWith1Language.instrumenter = null;
Source otherLanguageSource = Source.create("testIsNodeTaggedWith1-lang", "STATEMENT(EXPRESSION)");
run(otherLanguageSource);
Instrumenter instrumenter = TestIsNodeTaggedWith1Language.instrumenter;
Node languageExpression = TestIsNodeTaggedWith1.expressionNode;
Node languageStatement = TestIsNodeTaggedWith1.statementNode;
assertTags(instrumenter.queryTags(languageExpression), InstrumentationTestLanguage.EXPRESSION);
assertTags(instrumenter.queryTags(languageStatement), InstrumentationTestLanguage.STATEMENT);
TestIsNodeTaggedWith1.expressionNode = null;
TestIsNodeTaggedWith1.statementNode = null;
run("EXPRESSION");
// fail if called with nodes from a different language
Node otherLanguageExpression = TestIsNodeTaggedWith1.expressionNode;
try {
instrumenter.queryTags(otherLanguageExpression);
Assert.fail();
} catch (IllegalArgumentException e) {
}
}
@Test
public void testInstrumentsWhenForked() throws IOException {
Instrument instrument = engine.getInstruments().get("testIsNodeTaggedWith1");
assureEnabled(instrument);
TestIsNodeTaggedWith1 service = instrument.lookup(TestIsNodeTaggedWith1.class);
assertEquals(1, service.onCreateCalls);
Source otherLanguageSource = Source.create("testIsNodeTaggedWith1-lang", "STATEMENT(EXPRESSION)");
run(otherLanguageSource);
org.graalvm.polyglot.Context forked = newContext();
assertEquals(1, service.onCreateCalls);
final Map instruments = forked.getEngine().getInstruments();
assertSame(instrument, instruments.get("testIsNodeTaggedWith1"));
assertSame(service, instruments.get("testIsNodeTaggedWith1").lookup(TestIsNodeTaggedWith1.class));
assertEquals(instruments.size(), engine.getInstruments().size());
for (String key : instruments.keySet()) {
assertSame(engine.getInstruments().get(key), instruments.get(key));
}
assertEquals(0, service.onDisposeCalls);
context.close();
assertEquals(0, service.onDisposeCalls);
forked.getEngine().close();
// test if all engines are disposed
assertEquals(1, service.onDisposeCalls);
engine = null; // avoid a second disposal in @After event
}
@TruffleLanguage.Registration(id = "testIsNodeTaggedWith1-lang", name = "", version = "", mimeType = "testIsNodeTaggedWith1")
@ProvidedTags({InstrumentationTestLanguage.ExpressionNode.class, StandardTags.StatementTag.class})
public static class TestIsNodeTaggedWith1Language extends InstrumentationTestLanguage {
static Instrumenter instrumenter;
public TestIsNodeTaggedWith1Language() {
}
@Override
protected Context createContext(com.oracle.truffle.api.TruffleLanguage.Env env) {
instrumenter = env.lookup(Instrumenter.class);
return super.createContext(env);
}
@Override
protected CallTarget parse(ParsingRequest request) {
return Truffle.getRuntime().createCallTarget(new RootNode(this) {
@Child private BaseNode base = parse(request.getSource());
@Override
public Object execute(VirtualFrame frame) {
return base.execute(frame);
}
});
}
@Override
protected boolean isObjectOfLanguage(Object object) {
return false;
}
}
@Registration(id = "testIsNodeTaggedWith1", services = {Instrumenter.class, TestIsNodeTaggedWith1.class, Object.class})
public static class TestIsNodeTaggedWith1 extends TruffleInstrument {
static Node expressionNode;
static Node statementNode;
int onCreateCalls = 0;
int onDisposeCalls = 0;
@Override
protected void onCreate(final Env env) {
onCreateCalls++;
env.registerService(this);
env.registerService(env.getInstrumenter());
env.getInstrumenter().attachFactory(SourceSectionFilter.newBuilder().tagIs(InstrumentationTestLanguage.EXPRESSION).build(), new ExecutionEventNodeFactory() {
public ExecutionEventNode create(EventContext context) {
expressionNode = context.getInstrumentedNode();
return new ExecutionEventNode() {
};
}
});
env.getInstrumenter().attachFactory(SourceSectionFilter.newBuilder().tagIs(InstrumentationTestLanguage.STATEMENT).build(), new ExecutionEventNodeFactory() {
public ExecutionEventNode create(EventContext context) {
statementNode = context.getInstrumentedNode();
return new ExecutionEventNode() {
};
}
});
}
@Override
protected void onDispose(Env env) {
onDisposeCalls++;
}
}
private static void setupEngine(Source initSource, boolean runInitAfterExec) {
InstrumentationTestLanguage.envConfig.put("initSource", initSource);
InstrumentationTestLanguage.envConfig.put("runInitAfterExec", runInitAfterExec);
}
@Test
public void testAccessInstruments() {
Instrument instrument = engine.getInstruments().get("testAccessInstruments");
TestAccessInstruments access = instrument.lookup(TestAccessInstruments.class);
InstrumentInfo info = access.env.getInstruments().get("testAccessInstruments");
assertNotNull(info);
assertEquals("testAccessInstruments", info.getId());
assertEquals("name", info.getName());
assertEquals("version", info.getVersion());
try {
access.env.lookup(info, TestAccessInstruments.class);
fail();
} catch (IllegalArgumentException e) {
// expected
}
TestAccessInstrumentsOther.initializedCount = 0;
InstrumentInfo other = access.env.getInstruments().get("testAccessInstrumentsOther");
assertNotNull(other);
assertEquals("testAccessInstrumentsOther", other.getId());
assertEquals("otherName", other.getName());
assertEquals("otherVersion", other.getVersion());
assertEquals(0, TestAccessInstrumentsOther.initializedCount);
// invalid service, should not trigger onCreate
assertNull(access.env.lookup(other, Object.class));
assertEquals(0, TestAccessInstrumentsOther.initializedCount);
// valide service, should trigger onCreate
assertNotNull(access.env.lookup(other, TestAccessInstrumentsOther.class));
assertEquals(1, TestAccessInstrumentsOther.initializedCount);
}
@Test
public void testAccessLanguages() {
Instrument instrument = engine.getInstruments().get("testAccessInstruments");
TestAccessInstruments access = instrument.lookup(TestAccessInstruments.class);
LanguageInfo info = access.env.getLanguages().get(InstrumentationTestLanguage.ID);
assertNotNull(info);
assertTrue(info.getMimeTypes().contains(InstrumentationTestLanguage.MIME_TYPE));
assertEquals("InstrumentTestLang", info.getName());
assertEquals("2.0", info.getVersion());
assertNotNull(access.env.lookup(info, SpecialService.class));
assertEquals(InstrumentationTestLanguage.FILENAME_EXTENSION, access.env.lookup(info, SpecialService.class).fileExtension());
}
@Registration(id = "testAccessInstruments", name = "name", version = "version", services = TestAccessInstruments.class)
@SuppressWarnings("hiding")
public static class TestAccessInstruments extends TruffleInstrument {
Env env;
@Override
protected void onCreate(final Env env) {
this.env = env;
env.registerService(this);
}
@Override
protected void onDispose(Env env) {
}
}
@Registration(id = "testAccessInstrumentsOther", name = "otherName", version = "otherVersion", services = TestAccessInstrumentsOther.class)
public static class TestAccessInstrumentsOther extends TruffleInstrument {
static int initializedCount = 0;
@Override
protected void onCreate(final Env env) {
env.registerService(this);
initializedCount++;
}
@Override
protected void onDispose(Env env) {
}
}
public class ReturnLanguageEnv {
public static final String KEY = "envReturner";
public TruffleLanguage.Env env;
}
@Test
public void testAccessInstrumentFromLanguage() {
ReturnLanguageEnv envReturner = new ReturnLanguageEnv();
InstrumentationTestLanguage.envConfig.put(ReturnLanguageEnv.KEY, envReturner);
context.eval(Source.create(InstrumentationTestLanguage.ID, ""));
assertNotNull(envReturner.env);
TruffleLanguage.Env env = envReturner.env;
LanguageInfo langInfo = env.getLanguages().get(InstrumentationTestLanguage.ID);
assertNotNull(langInfo);
assertTrue(langInfo.getMimeTypes().contains(InstrumentationTestLanguage.MIME_TYPE));
assertEquals("InstrumentTestLang", langInfo.getName());
assertEquals("2.0", langInfo.getVersion());
InstrumentInfo instrInfo = env.getInstruments().get("testAccessInstruments");
assertNotNull(instrInfo);
assertEquals("testAccessInstruments", instrInfo.getId());
assertEquals("name", instrInfo.getName());
assertEquals("version", instrInfo.getVersion());
assertNotNull(env.lookup(instrInfo, TestAccessInstruments.class));
assertNull(env.lookup(instrInfo, SpecialService.class));
try {
// cannot load services from current languages to avoid cycles.
env.lookup(langInfo, SpecialService.class);
fail();
} catch (Exception e1) {
// expected
}
}
@Test
public void testLanguageInitializedOrNot() throws Exception {
Source initSource = Source.create(InstrumentationTestLanguage.ID, "STATEMENT(EXPRESSION, EXPRESSION)");
setupEngine(initSource, false);
Instrument instrument = engine.getInstruments().get("testLangInitialized");
// Events during language initialization phase are included:
TestLangInitialized.initializationEvents = true;
TestLangInitialized service = instrument.lookup(TestLangInitialized.class);
run("LOOP(2, STATEMENT())");
assertEquals("[FunctionRootNode, false, StatementNode, false, ExpressionNode, false, ExpressionNode, false, FunctionRootNode, true, LoopNode, true, StatementNode, true, StatementNode, true]",
service.getEnteredNodes());
engine.close();
engine = null;
}
@Test
public void testLanguageInitializedOnly() throws Exception {
Source initSource = Source.create(InstrumentationTestLanguage.ID, "STATEMENT(EXPRESSION, EXPRESSION)");
setupEngine(initSource, false);
Instrument instrument = engine.getInstruments().get("testLangInitialized");
// Events during language initialization phase are excluded:
TestLangInitialized.initializationEvents = false;
TestLangInitialized service = instrument.lookup(TestLangInitialized.class);
run("LOOP(2, STATEMENT())");
assertEquals("[FunctionRootNode, true, LoopNode, true, StatementNode, true, StatementNode, true]", service.getEnteredNodes());
engine.close();
engine = null;
}
@Test
public void testLanguageInitializedOrNotAppend() throws Exception {
Source initSource = Source.create(InstrumentationTestLanguage.ID, "STATEMENT(EXPRESSION, EXPRESSION)");
setupEngine(initSource, true);
Instrument instrument = engine.getInstruments().get("testLangInitialized");
// Events during language initialization phase are prepended and appended:
TestLangInitialized.initializationEvents = true;
TestLangInitialized service = instrument.lookup(TestLangInitialized.class);
run("LOOP(2, STATEMENT())");
assertEquals("[FunctionRootNode, false, StatementNode, false, ExpressionNode, false, ExpressionNode, false, " +
"FunctionRootNode, true, LoopNode, true, StatementNode, true, StatementNode, true, FunctionRootNode, true, StatementNode, true, ExpressionNode, true, ExpressionNode, true]",
service.getEnteredNodes());
engine.close();
engine = null;
}
@Test
public void testLanguageInitializedOnlyAppend() throws Exception {
Source initSource = Source.create(InstrumentationTestLanguage.ID, "STATEMENT(EXPRESSION, EXPRESSION)");
setupEngine(initSource, true);
Instrument instrument = engine.getInstruments().get("testLangInitialized");
// Events during language initialization phase are excluded,
// but events from the same nodes used for initialization are appended:
TestLangInitialized.initializationEvents = false;
TestLangInitialized service = instrument.lookup(TestLangInitialized.class);
run("LOOP(2, STATEMENT())");
assertEquals("[FunctionRootNode, true, LoopNode, true, StatementNode, true, StatementNode, true, FunctionRootNode, true, StatementNode, true, ExpressionNode, true, ExpressionNode, true]",
service.getEnteredNodes());
engine.close();
engine = null;
}
@Registration(id = "testLangInitialized", services = TestLangInitialized.class)
public static class TestLangInitialized extends TruffleInstrument implements ExecutionEventListener {
static boolean initializationEvents;
private final List enteredNodes = new ArrayList<>();
@Override
protected void onCreate(Env env) {
env.registerService(this);
env.getInstrumenter().attachListener(SourceSectionFilter.ANY, this);
}
@Override
public void onEnter(EventContext context, VirtualFrame frame) {
if (!initializationEvents && !context.isLanguageContextInitialized()) {
// Skipt language context initialization if initializationEvents is false
return;
}
enteredNodes.add(context.getInstrumentedNode().getClass().getSimpleName());
enteredNodes.add(Boolean.toString(context.isLanguageContextInitialized()));
}
@Override
public void onReturnValue(EventContext context, VirtualFrame frame, Object result) {
}
@Override
public void onReturnExceptional(EventContext context, VirtualFrame frame, Throwable exception) {
}
String getEnteredNodes() {
return enteredNodes.toString();
}
}
@Test
public void testAllocation() throws Exception {
Instrument instrument = engine.getInstruments().get("testAllocation");
assureEnabled(instrument);
TestAllocation allocation = instrument.lookup(TestAllocation.class);
run("LOOP(3, VARIABLE(a, 10))");
engine.close();
engine = null;
assertEquals("[W 4 null, A 4 10, W 4 null, A 4 10, W 4 null, A 4 10]", allocation.getAllocations());
}
@Registration(id = "testAllocation", services = {TestAllocation.class, Object.class})
public static class TestAllocation extends TruffleInstrument implements AllocationListener {
private final List allocations = new ArrayList<>();
@Override
protected void onCreate(Env env) {
env.registerService(this);
LanguageInfo testLanguage = env.getLanguages().get(InstrumentationTestLanguage.ID);
env.getInstrumenter().attachAllocationListener(AllocationEventFilter.newBuilder().languages(testLanguage).build(), this);
}
String getAllocations() {
return allocations.toString();
}
@Override
@TruffleBoundary
public void onEnter(AllocationEvent event) {
allocations.add("W " + event.getNewSize() + " " + event.getValue());
}
@Override
@TruffleBoundary
public void onReturnValue(AllocationEvent event) {
allocations.add("A " + event.getNewSize() + " " + event.getValue());
}
}
private static final class MyKillException extends ThreadDeath {
static final long serialVersionUID = 1;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy