Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
203 lines
7.4 KiB
Java
203 lines
7.4 KiB
Java
/*
|
|
* Copyright (c) 2025, 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.
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
/**
|
|
* @test
|
|
* @bug 8353835
|
|
* @summary Basic test for JFR FinalFieldMutation event
|
|
* @requires vm.hasJFR
|
|
* @modules jdk.jfr/jdk.jfr.events
|
|
* @run junit FinalFieldMutationEventTest
|
|
* @run junit/othervm --illegal-final-field-mutation=allow FinalFieldMutationEventTest
|
|
* @run junit/othervm --illegal-final-field-mutation=warn FinalFieldMutationEventTest
|
|
* @run junit/othervm --illegal-final-field-mutation=debug FinalFieldMutationEventTest
|
|
* @run junit/othervm --illegal-final-field-mutation=deny FinalFieldMutationEventTest
|
|
*/
|
|
|
|
import java.lang.annotation.Annotation;
|
|
import java.lang.invoke.MethodHandles;
|
|
import java.lang.reflect.Field;
|
|
import java.lang.reflect.Method;
|
|
import java.nio.file.Path;
|
|
import java.util.List;
|
|
import java.util.stream.Stream;
|
|
import jdk.jfr.Recording;
|
|
import jdk.jfr.consumer.RecordedClass;
|
|
import jdk.jfr.consumer.RecordedEvent;
|
|
import jdk.jfr.consumer.RecordedMethod;
|
|
import jdk.jfr.consumer.RecordingFile;
|
|
import jdk.jfr.events.FinalFieldMutationEvent;
|
|
import jdk.jfr.events.StackFilter;
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
|
|
class FinalFieldMutationEventTest {
|
|
private static final String EVENT_NAME = "jdk.FinalFieldMutation";
|
|
|
|
/**
|
|
* Test class with final field.
|
|
*/
|
|
private static class C {
|
|
final int value;
|
|
C(int value) {
|
|
this.value = value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test jdk.FinalFieldMutation is recorded when mutating a final field.
|
|
*/
|
|
@Test
|
|
void testFieldSet() throws Exception {
|
|
Field field = C.class.getDeclaredField("value");
|
|
field.setAccessible(true);
|
|
|
|
try (Recording recording = new Recording()) {
|
|
recording.enable(EVENT_NAME).withStackTrace();
|
|
|
|
boolean mutated = false;
|
|
recording.start();
|
|
try {
|
|
var obj = new C(100);
|
|
try {
|
|
field.setInt(obj, 200);
|
|
mutated = true;
|
|
} catch (IllegalAccessException e) {
|
|
// denied
|
|
}
|
|
} finally {
|
|
recording.stop();
|
|
}
|
|
|
|
// FinalFieldMutation event should be recorded if field mutated
|
|
List<RecordedEvent> events = find(recording, EVENT_NAME);
|
|
System.err.println(events);
|
|
if (mutated) {
|
|
assertEquals(1, events.size(), "1 event expected");
|
|
checkEvent(events.get(0), field, "FinalFieldMutationEventTest::testFieldSet");
|
|
} else {
|
|
assertEquals(0, events.size(), "No events expected");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test jdk.FinalFieldMutation is recorded when unreflecting a field field for mutation.
|
|
*/
|
|
@Test
|
|
void testUnreflectSetter() throws Exception {
|
|
Field field = C.class.getDeclaredField("value");
|
|
field.setAccessible(true);
|
|
|
|
try (Recording recording = new Recording()) {
|
|
recording.enable(EVENT_NAME).withStackTrace();
|
|
|
|
boolean unreflected = false;
|
|
recording.start();
|
|
try {
|
|
MethodHandles.lookup().unreflectSetter(field);
|
|
unreflected = true;
|
|
} catch (IllegalAccessException e) {
|
|
// denied
|
|
} finally {
|
|
recording.stop();
|
|
}
|
|
|
|
// FinalFieldMutation event should be recorded if field unreflected for set
|
|
List<RecordedEvent> events = find(recording, EVENT_NAME);
|
|
System.err.println(events);
|
|
if (unreflected) {
|
|
assertEquals(1, events.size(), "1 event expected");
|
|
checkEvent(events.get(0), field, "FinalFieldMutationEventTest::testUnreflectSetter");
|
|
} else {
|
|
assertEquals(0, events.size(), "No events expected");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test that a FinalFieldMutationEvent event has the declaringClass and fieldName of
|
|
* the given Field, and the expected top frame.
|
|
*/
|
|
private void checkEvent(RecordedEvent e, Field f, String expectedTopFrame) {
|
|
RecordedClass clazz = e.getClass("declaringClass");
|
|
assertNotNull(clazz);
|
|
assertEquals(f.getDeclaringClass().getName(), clazz.getName());
|
|
assertEquals(f.getName(), e.getString("fieldName"));
|
|
|
|
// check the top-frame of the stack trace
|
|
RecordedMethod m = e.getStackTrace().getFrames().getFirst().getMethod();
|
|
assertEquals(expectedTopFrame, m.getType().getName() + "::" + m.getName());
|
|
}
|
|
|
|
/**
|
|
* Tests that FinalFieldMutationEvent's stack filter value names classes/methods that
|
|
* exist. This will help detect stale values when the implementation is refactored.
|
|
*/
|
|
@Test
|
|
void testFinalFieldMutationEventStackFilter() throws Exception {
|
|
String[] filters = FinalFieldMutationEvent.class.getAnnotation(StackFilter.class).value();
|
|
for (String filter : filters) {
|
|
String[] classAndMethod = filter.split("::");
|
|
String cn = classAndMethod[0];
|
|
|
|
// throws if class not found
|
|
Class<?> clazz = Class.forName(cn);
|
|
|
|
// if the filter has a method name then check a method of that name exists
|
|
if (classAndMethod.length > 1) {
|
|
String mn = classAndMethod[1];
|
|
Method method = Stream.of(clazz.getDeclaredMethods())
|
|
.filter(m -> m.getName().equals(mn))
|
|
.findFirst()
|
|
.orElse(null);
|
|
assertNotNull(method, cn + "::" + mn + " not found");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the list of events in the given recording with the given name.
|
|
*/
|
|
private List<RecordedEvent> find(Recording recording, String name) throws Exception {
|
|
Path recordingFile = recordingFile(recording);
|
|
return RecordingFile.readAllEvents(recordingFile)
|
|
.stream()
|
|
.filter(e -> e.getEventType().getName().equals(name))
|
|
.toList();
|
|
}
|
|
|
|
/**
|
|
* Return the file path to the recording file.
|
|
*/
|
|
private Path recordingFile(Recording recording) throws Exception {
|
|
Path recordingFile = recording.getDestination();
|
|
if (recordingFile == null) {
|
|
ProcessHandle h = ProcessHandle.current();
|
|
recordingFile = Path.of("recording-" + recording.getId() + "-pid" + h.pid() + ".jfr");
|
|
recording.dump(recordingFile);
|
|
}
|
|
return recordingFile;
|
|
}
|
|
}
|