1 /*
  2  * Copyright (c) 2018, 2025, 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 package jdk.jfr.event.metadata;
 24 
 25 import java.io.IOException;
 26 import java.lang.reflect.Field;
 27 import java.nio.file.Files;
 28 import java.nio.file.Path;
 29 import java.nio.file.Paths;
 30 import java.util.Arrays;
 31 import java.util.HashSet;
 32 import java.util.List;
 33 import java.util.Set;
 34 import java.util.stream.Collectors;
 35 import java.util.stream.Stream;
 36 
 37 import jdk.jfr.EventType;
 38 import jdk.jfr.Experimental;
 39 import jdk.jfr.FlightRecorder;
 40 import jdk.test.lib.Utils;
 41 import jdk.test.lib.jfr.EventNames;
 42 
 43 /**
 44  * @test Check for JFR events not covered by tests
 45  * @requires vm.flagless
 46  * @requires vm.hasJFR
 47  * @library /test/lib /test/jdk
 48  * @run main jdk.jfr.event.metadata.TestLookForUntestedEvents
 49  */
 50 public class TestLookForUntestedEvents {
 51     private static final Path jfrTestRoot = Paths.get(Utils.TEST_SRC).getParent().getParent();
 52     private static final String MSG_SEPARATOR = "==========================";
 53     private static Set<String> jfrEventTypes = new HashSet<>();
 54 
 55     private static final Set<String> hardToTestEvents = new HashSet<>(
 56         Arrays.asList(
 57             "DataLoss", "IntFlag", "ReservedStackActivation", "NativeLibraryUnload",
 58             "DoubleFlag", "UnsignedLongFlagChanged", "IntFlagChanged",
 59             "UnsignedIntFlag", "UnsignedIntFlagChanged", "DoubleFlagChanged")
 60     );
 61 
 62     // GC uses specific framework to test the events, instead of using event names literally.
 63     // GC tests were inspected, as well as runtime output of GC tests.
 64     // The following events below are know to be covered based on that inspection.
 65     private static final Set<String> coveredGcEvents = new HashSet<>(
 66         Arrays.asList(
 67             "MetaspaceGCThreshold", "MetaspaceAllocationFailure", "MetaspaceOOM",
 68             "MetaspaceChunkFreeListSummary", "G1HeapSummary", "ParallelOldGarbageCollection",
 69             "OldGarbageCollection", "G1GarbageCollection", "GCPhasePause",
 70             "GCPhasePauseLevel1", "GCPhasePauseLevel2", "GCPhasePauseLevel3",
 71             "GCPhasePauseLevel4")
 72     );
 73 
 74     // Container events are tested in hotspot/jtreg/containers/docker/TestJFREvents.java
 75     private static final Set<String> coveredContainerEvents = new HashSet<>(
 76         Arrays.asList(
 77             "ContainerConfiguration", "ContainerCPUUsage", "ContainerCPUThrottling",
 78             "ContainerMemoryUsage", "ContainerIOUsage")
 79     );
 80     // These events are tested in test/jdk/java/lang/Thread/virtual/JfrEvents.java
 81     private static final Set<String> coveredVirtualThreadEvents = Set.of(
 82         "VirtualThreadPinned", "VirtualThreadSubmitFailed");
 83 
 84     // This event is tested in test/jdk/java/lang/reflect/Field/mutateFinals/FinalFieldMutationEventTest.java
 85     private static final Set<String> coveredFinalFieldMutationEvents = Set.of(
 86         "FinalFieldMutationEvent");
 87 
 88     // This is a "known failure list" for this test.
 89     // NOTE: if the event is not covered, a bug should be open, and bug number
 90     // noted in the comments for this set.
 91     private static final Set<String> knownNotCoveredEvents = new HashSet<>(
 92     );
 93 
 94     // Experimental events
 95     private static final Set<String> experimentalEvents = Set.of(
 96         "Flush", "SyncOnValueBasedClass", "CPUTimeSample", "CPUTimeSamplesLost");
 97 
 98     // Subset of the experimental events that should have tests
 99     private static final Set<String> experimentalButTestedEvents = Set.of("CPUTimeSample");
100 
101     public static void main(String[] args) throws Exception {
102         for (EventType type : FlightRecorder.getFlightRecorder().getEventTypes()) {
103             if (type.getAnnotation(Experimental.class) == null) {
104                 jfrEventTypes.add(type.getName().replace("jdk.", ""));
105             }
106         }
107 
108         checkEventNamesClass();
109         lookForEventsNotCoveredByTests();
110     }
111 
112     // Look thru JFR tests to make sure JFR events are referenced in the tests
113     private static void lookForEventsNotCoveredByTests() throws Exception {
114         List<Path> paths = Files.walk(jfrTestRoot)
115             .filter(Files::isRegularFile)
116             .filter(path -> isJavaFile(path))
117             .collect(Collectors.toList());
118 
119         Set<String> eventsNotCoveredByTest = new HashSet<>(jfrEventTypes);
120         Set<String> checkedEvents = new HashSet<>(jfrEventTypes);
121         checkedEvents.addAll(experimentalButTestedEvents);
122         for (String event : checkedEvents) {
123             for (Path p : paths) {
124                 if (findStringInFile(p, event)) {
125                     eventsNotCoveredByTest.remove(event);
126                     break;
127                 }
128             }
129         }
130 
131         // Account for hard-to-test, experimental and GC tested events
132         eventsNotCoveredByTest.removeAll(hardToTestEvents);
133         eventsNotCoveredByTest.removeAll(coveredGcEvents);
134         eventsNotCoveredByTest.removeAll(coveredVirtualThreadEvents);
135         eventsNotCoveredByTest.removeAll(coveredFinalFieldMutationEvents);
136         eventsNotCoveredByTest.removeAll(coveredContainerEvents);
137         eventsNotCoveredByTest.removeAll(knownNotCoveredEvents);
138 
139         if (!eventsNotCoveredByTest.isEmpty()) {
140             print(MSG_SEPARATOR + " Events not covered by test");
141             for (String event: eventsNotCoveredByTest) {
142                 print(event);
143             }
144             print(MSG_SEPARATOR);
145             throw new RuntimeException("Found JFR events not covered by tests");
146         }
147     }
148 
149     // Make sure all the JFR events are accounted for in jdk.test.lib.jfr.EventNames
150     private static void checkEventNamesClass() throws Exception {
151         // jdk.test.lib.jfr.EventNames
152         Set<String> eventsFromEventNamesClass = new HashSet<>();
153         for (Field f : EventNames.class.getFields()) {
154             String name = f.getName();
155             if (!name.equals("PREFIX")) {
156                 String eventName = (String) f.get(null);
157                 eventName = eventName.replace(EventNames.PREFIX, "");
158                 eventsFromEventNamesClass.add(eventName);
159             }
160         }
161 
162         // remove experimental events from eventsFromEventNamesClass since jfrEventTypes
163         // excludes experimental events
164         eventsFromEventNamesClass.removeAll(experimentalEvents);
165 
166         if (!jfrEventTypes.equals(eventsFromEventNamesClass)) {
167             String exceptionMsg = "Events declared in jdk.test.lib.jfr.EventNames differ " +
168                          "from events returned by FlightRecorder.getEventTypes()";
169             print(MSG_SEPARATOR);
170             print(exceptionMsg);
171             print("");
172             printSetDiff(jfrEventTypes, eventsFromEventNamesClass,
173                         "jfrEventTypes", "eventsFromEventNamesClass");
174             print("");
175 
176             print("This could be because:");
177             print("1) You forgot to write a unit test. Please do so in test/jdk/jdk/jfr/event/");
178             print("2) You wrote a unit test, but you didn't reference the event in");
179             print("   test/lib/jdk/test/lib/jfr/EventNames.java. ");
180             print("3) It is not feasible to test the event, not even a sanity test. ");
181             print("   Add the event name to test/lib/jdk/test/lib/jfr/EventNames.java ");
182             print("   and a short comment why it can't be tested");
183             print("4) The event is experimental. Please add 'experimental=\"true\"' to <Event> ");
184             print("   element in metadata.xml if it is a native event, or @Experimental if it is a ");
185             print("   Java event. The event will now not show up in JMC");
186             System.out.println(MSG_SEPARATOR);
187             throw new RuntimeException(exceptionMsg);
188         }
189     }
190 
191     // ================ Helper methods
192     private static boolean isJavaFile(Path p) {
193         String fileName = p.getFileName().toString();
194         int i = fileName.lastIndexOf('.');
195         if ( (i < 0) || (i > fileName.length()) ) {
196             return false;
197         }
198         return "java".equals(fileName.substring(i+1));
199     }
200 
201     private static boolean findStringInFile(Path p, String searchTerm) throws IOException {
202         long c = 0;
203         try (Stream<String> stream = Files.lines(p)) {
204             c = stream
205                 .filter(line -> line.contains(searchTerm))
206                 .count();
207         }
208         return (c != 0);
209     }
210 
211     private static void printSetDiff(Set<String> a, Set<String> b,
212         String setAName, String setBName) {
213         if (a.size() > b.size()) {
214             a.removeAll(b);
215             System.out.printf("Set %s has more elements than set %s:", setAName, setBName);
216             System.out.println();
217             printSet(a);
218         } else {
219             b.removeAll(a);
220             System.out.printf("Set %s has more elements than set %s:", setBName, setAName);
221             System.out.println();
222             printSet(b);
223         }
224     }
225 
226     private static void printSet(Set<String> set) {
227         for (String e : set) {
228             System.out.println(e);
229         }
230     }
231 
232     private static void print(String s) {
233         System.out.println(s);
234     }
235 }