diff --git a/test/hotspot/jtreg/testlibrary/jittester/conf/exclude.methods.lst b/test/hotspot/jtreg/testlibrary/jittester/conf/exclude.methods.lst index befd0ed66db..f8db44b1463 100644 --- a/test/hotspot/jtreg/testlibrary/jittester/conf/exclude.methods.lst +++ b/test/hotspot/jtreg/testlibrary/jittester/conf/exclude.methods.lst @@ -31,3 +31,6 @@ java/lang/System::nanoTime() java/lang/annotation/IncompleteAnnotationException::IncompleteAnnotationException(Ljava/lang/Class;Ljava/lang/String;) java/util/AbstractSet::toString() java/util/HashSet::toString() + +#Unstable methods +*::hashCode diff --git a/test/hotspot/jtreg/testlibrary/jittester/src/jdk/test/lib/jittester/MethodTemplate.java b/test/hotspot/jtreg/testlibrary/jittester/src/jdk/test/lib/jittester/MethodTemplate.java new file mode 100644 index 00000000000..26f5369f720 --- /dev/null +++ b/test/hotspot/jtreg/testlibrary/jittester/src/jdk/test/lib/jittester/MethodTemplate.java @@ -0,0 +1,256 @@ +/* + * 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. + */ + +package testlibrary.jittester.src.jdk.test.lib.jittester; + +import java.io.IOException; +import java.io.StringReader; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jdk.test.lib.Asserts; + +import static java.util.function.Predicate.not; + + +/** + * A wrapper for string method templates, similar to the CompileCommand patterns. + */ +public final class MethodTemplate { + + /** + * String that can have wildcard symbols on its ends, allowing it to match a family of strings. + * For example, "abc*" matches "abc123", and so on. + */ + public static class WildcardString { + private final String pattern; + private final boolean frontWildcarded; + private final boolean tailWildcarded; + + /** + * Creates a WildcardString from given string. + * @param pattern string pattern, like "some*" + */ + public WildcardString(String pattern) { + // check for the leading '*' + frontWildcarded = pattern.charAt(0) == '*'; + pattern = frontWildcarded ? pattern.substring(1) : pattern; + + // check for the trailing '*' + tailWildcarded = pattern.length() > 0 && pattern.charAt(pattern.length() - 1) == '*'; + pattern = tailWildcarded ? pattern.substring(0, pattern.length() - 1) : pattern; + + this.pattern = pattern; + } + + /** + * Returns true it this WildcardString matches given other string. + * @param other the string that this WildcardString should be matched against + * @return true in case of a match. + */ + public boolean matches(String other) { + boolean result = pattern.equals(other); + result |= frontWildcarded ? other.endsWith(pattern) : result; + result |= tailWildcarded ? other.startsWith(pattern) : result; + result |= tailWildcarded && frontWildcarded ? other.contains(pattern) : result; + return result; + } + } + + private static final Pattern METHOD_PATTERN = Pattern.compile(generateMethodPattern()); + + private final WildcardString klassName; + private final WildcardString methodName; + private final Optional>> signature; + + private MethodTemplate(String klassName, String methodName, Optional>> signature) { + this.klassName = new WildcardString(klassName); + this.methodName = new WildcardString(methodName); + this.signature = signature; + } + + private static String generateMethodPattern() { + // Sample valid template(s): java/lang/String::indexOf(Ljava/lang/String;I) + // java/lang/::*(Ljava/lang/String;I) + // *String::indexOf(*) + // java/lang/*::indexOf + + String primitiveType = "[ZBSCIJFD]"; // Simply a letter, like 'I' + String referenceType = "L[\\w/$]+;"; // Like 'Ljava/lang/String;' + String primOrRefType = + "\\[?" + primitiveType + // Bracket is optional: '[Z', or 'Z' + "|" + + "\\[?" + referenceType; // Bracket is optional: '[LSomeObject;' or 'LSomeObject;' + String argTypesOrWildcard = "(" + // Method argument(s) Ljava/lang/String;Z... + "(" + primOrRefType + ")*" + + ")|\\*"; // .. or a wildcard: + + return + "(?[\\w/$]*\\*?)" + // Class name, like 'java/lang/String' + "::" + // Simply '::' + "(?\\*?[\\w$]+\\*?)" + // method name, 'indexOf'' + "(\\((?" + // Method argument(s) in brackets: + argTypesOrWildcard + // (Ljava/lang/String;Z) or '*' or nothing + ")\\))?"; + } + + /** + * Returns true iff none of the given MethodTemplates matches the given Executable. + * + * @param templates the collection of templates to check + * @param method the executable to match the colletions templates + * @return true if none of the given templates matches the method, false otherwise + */ + public static boolean noneMatches(Collection templates, Executable method) { + for (MethodTemplate template : templates) { + if (template.matches(method)) { + return false; + } + } + return true; + } + + /** + * Returns true if this MethodTemplate matches the given Executable. + * + * @param other the Executable to try to match to + * @return whether the other matches this MethodTemplate + */ + public boolean matches(Executable other) { + boolean result = klassName.matches(other.getDeclaringClass().getName()); + + result &= (other instanceof Constructor) + ? result + : methodName.matches(other.getName()); + + return result && + signature.map(Arrays.asList(other.getParameterTypes())::equals) + .orElse(true); + } + + /** + * Parses the given string and returs a MethodTemplate. + * + * @param methodStr the string to parse + * @return created MethodTemplate + */ + public static MethodTemplate parse(String methodStr) { + Matcher matcher = METHOD_PATTERN.matcher(methodStr); + String msg = String.format("Format of the methods exclude input file is incorrect," + + " methodStr \"%s\" has wrong format", methodStr); + Asserts.assertTrue(matcher.matches(), msg); + + String klassName = matcher.group("klassName").replaceAll("/", "\\."); + String methodName = matcher.group("methodName"); + Optional>> signature = Optional.ofNullable(matcher.group("argTypes")) + .filter(not("*"::equals)) + .map(MethodTemplate::parseSignature); + return new MethodTemplate(klassName, methodName, signature); + } + + private static List> parseSignature(String signature) { + List> sigClasses = new ArrayList<>(); + char typeChar; + boolean isArray; + String klassName; + StringBuilder sb; + StringBuilder arrayDim; + try (StringReader str = new StringReader(signature)) { + int symbol = str.read(); + while (symbol != -1) { + typeChar = (char) symbol; + arrayDim = new StringBuilder(); + Class primArrayClass = null; + if (typeChar == '[') { + isArray = true; + arrayDim.append('['); + symbol = str.read(); + while (symbol == '[') { + arrayDim.append('['); + symbol = str.read(); + } + typeChar = (char) symbol; + if (typeChar != 'L') { + primArrayClass = Class.forName(arrayDim.toString() + typeChar); + } + } else { + isArray = false; + } + switch (typeChar) { + case 'Z': + sigClasses.add(isArray ? primArrayClass : boolean.class); + break; + case 'I': + sigClasses.add(isArray ? primArrayClass : int.class); + break; + case 'J': + sigClasses.add(isArray ? primArrayClass : long.class); + break; + case 'F': + sigClasses.add(isArray ? primArrayClass : float.class); + break; + case 'D': + sigClasses.add(isArray ? primArrayClass : double.class); + break; + case 'B': + sigClasses.add(isArray ? primArrayClass : byte.class); + break; + case 'S': + sigClasses.add(isArray ? primArrayClass : short.class); + break; + case 'C': + sigClasses.add(isArray ? primArrayClass : char.class); + break; + case 'L': + sb = new StringBuilder(); + symbol = str.read(); + while (symbol != ';') { + sb.append((char) symbol); + symbol = str.read(); + } + klassName = sb.toString().replaceAll("/", "\\."); + if (isArray) { + klassName = arrayDim.toString() + "L" + klassName + ";"; + } + Class klass = Class.forName(klassName); + sigClasses.add(klass); + break; + default: + throw new Error("Unknown type " + typeChar); + } + symbol = str.read(); + } + } catch (IOException | ClassNotFoundException ex) { + throw new Error("Unexpected exception while parsing exclude methods file", ex); + } + return sigClasses; + } + +} diff --git a/test/hotspot/jtreg/testlibrary/jittester/src/jdk/test/lib/jittester/TypesParser.java b/test/hotspot/jtreg/testlibrary/jittester/src/jdk/test/lib/jittester/TypesParser.java index bfe5a3224b6..ba956692af7 100644 --- a/test/hotspot/jtreg/testlibrary/jittester/src/jdk/test/lib/jittester/TypesParser.java +++ b/test/hotspot/jtreg/testlibrary/jittester/src/jdk/test/lib/jittester/TypesParser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 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 @@ -23,9 +23,7 @@ package jdk.test.lib.jittester; -import java.io.File; import java.io.IOException; -import java.io.StringReader; import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -35,22 +33,31 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; import java.util.List; -import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + import jdk.test.lib.Asserts; import jdk.test.lib.jittester.functions.FunctionInfo; import jdk.test.lib.jittester.types.TypeArray; import jdk.test.lib.jittester.types.TypeKlass; +import static java.util.function.Predicate.not; + /** * Class used for parsing included classes file and excluded methods file */ public class TypesParser { + private List methodsToExclude; + private static final HashMap, Type> TYPE_CACHE = new HashMap<>(); + private static String trimComment(String source) { + int commentStart = source.indexOf('#'); + return commentStart == -1 ? source : source.substring(0, commentStart); + } + /** * Parses included classes file and excluded methods file to TypeList and SymbolTable. * This routine takes all classes named in the classes file and puts them to the TypeList, @@ -62,27 +69,21 @@ public class TypesParser { public static void parseTypesAndMethods(String klassesFileName, String exMethodsFileName) { Asserts.assertNotNull(klassesFileName, "Classes input file name is null"); Asserts.assertFalse(klassesFileName.isEmpty(), "Classes input file name is empty"); - List> klasses = parseKlasses(klassesFileName); - Set methodsToExclude; - if (exMethodsFileName != null && !exMethodsFileName.isEmpty()) { - methodsToExclude = parseMethods(exMethodsFileName); - } else { - methodsToExclude = new HashSet<>(); - } - klasses.stream().forEach(klass -> { - TypeKlass typeKlass = (TypeKlass) getType(klass); - if (TypeList.isReferenceType(typeKlass)) { - return; - } - TypeList.add(typeKlass); - Set methods = new HashSet<>(); - methods.addAll(Arrays.asList(klass.getMethods())); - methods.addAll(Arrays.asList(klass.getConstructors())); - methods.removeAll(methodsToExclude); - methods.stream().forEach(method -> { - if (method.isSynthetic()) { - return; - } + TypesParser theParser = new TypesParser(); + theParser.initMethodsToExclude(exMethodsFileName); + parseKlasses(klassesFileName) + .stream() + .filter(klass -> !TypeList.isReferenceType(getTypeKlass(klass))) + .forEach(theParser::processKlass); + } + + private void processKlass(Class klass) { + TypeKlass typeKlass = getTypeKlass(klass); + TypeList.add(typeKlass); + Stream.concat(Arrays.stream(klass.getMethods()), Arrays.stream(klass.getConstructors())) + .filter(not(Executable::isSynthetic)) + .filter(method -> MethodTemplate.noneMatches(methodsToExclude, method)) + .forEach(method -> { String name = method.getName(); boolean isConstructor = false; Type returnType; @@ -106,10 +107,8 @@ public static void parseTypesAndMethods(String klassesFileName, String exMethods paramList.add(new VariableInfo("arg" + argNum, typeKlass, paramType, VariableInfo.LOCAL | VariableInfo.INITIALIZED)); } - typeKlass.addSymbol(new FunctionInfo(name, typeKlass, returnType, 1, flags, - paramList)); + typeKlass.addSymbol(new FunctionInfo(name, typeKlass, returnType, 1, flags, paramList)); }); - }); } private static Type getType(Class klass) { @@ -155,6 +154,10 @@ private static Type getType(Class klass) { return type; } + private static TypeKlass getTypeKlass(Class klass) { + return (TypeKlass) getType(klass); + } + private static int getArrayClassDimension(Class klass) { if (!klass.isArray()) { return 0; @@ -234,133 +237,24 @@ private static List> parseKlasses(String klassesFileName) { return klassesList; } - private static Set parseMethods(String methodsFileName) { - Asserts.assertNotNull(methodsFileName, "Methods exclude input file name is null"); - Asserts.assertFalse(methodsFileName.isEmpty(), "Methods exclude input file name is empty"); - LinkedList methodNamesList = new LinkedList<>(); - Path klassesFilePath = Paths.get(methodsFileName); - try { - Files.lines(klassesFilePath).forEach(line -> { - line = line.trim(); - if (line.isEmpty()) { - return; - } - String msg = String.format("Format of the methods exclude input file \"%s\" is incorrect," - + " line \"%s\" has wrong format", methodsFileName, line); - Asserts.assertTrue(line.matches("\\w[\\w/$]*::[\\w$]+\\((\\[?[ZBSCIJFD]|\\[?L[\\w/$]+;)*\\)"), msg); - methodNamesList.add(line.substring(0, line.length() - 1)); - }); - } catch (IOException ex) { - throw new Error("Error reading exclude method file", ex); - } - Set methodsList = new HashSet<>(); - methodNamesList.forEach(methodName -> { - String[] klassAndNameAndSig = methodName.split("::"); - String klassName = klassAndNameAndSig[0].replaceAll("/", "\\."); - String[] nameAndSig = klassAndNameAndSig[1].split("[\\(\\)]"); - String name = nameAndSig[0]; - String signature = ""; - if (nameAndSig.length > 1) { - signature = nameAndSig[1]; - } - Class klass = null; - List> signatureTypes = null; + private void initMethodsToExclude(String methodsFileName) { + if (methodsFileName != null && !methodsFileName.isEmpty()) { + Path methodsFilePath = Paths.get(methodsFileName); try { - klass = Class.forName(klassName); - signatureTypes = parseSignature(signature); - } catch (ClassNotFoundException ex) { - throw new Error("Unexpected exception while parsing exclude methods file", ex); - } - try { - Executable method; - if (name.equals(klass.getSimpleName())) { - method = klass.getConstructor(signatureTypes.toArray(new Class[0])); - } else { - method = klass.getMethod(name, signatureTypes.toArray(new Class[0])); - } - methodsList.add(method); - } catch (NoSuchMethodException | SecurityException ex) { - throw new Error("Unexpected exception while parsing exclude methods file", ex); - } - }); - return methodsList; - } + methodsToExclude = Files.lines(methodsFilePath) + // Cleaning nonimportant parts + .map(TypesParser::trimComment) + .map(String::trim) + .filter(not(String::isEmpty)) - private static List> parseSignature(String signature) throws ClassNotFoundException { - LinkedList> sigClasses = new LinkedList<>(); - char typeChar; - boolean isArray; - String klassName; - StringBuilder sb; - StringBuilder arrayDim; - try (StringReader str = new StringReader(signature)) { - int symbol = str.read(); - while (symbol != -1){ - typeChar = (char) symbol; - arrayDim = new StringBuilder(); - Class primArrayClass = null; - if (typeChar == '[') { - isArray = true; - arrayDim.append('['); - symbol = str.read(); - while (symbol == '['){ - arrayDim.append('['); - symbol = str.read(); - } - typeChar = (char) symbol; - if (typeChar != 'L') { - primArrayClass = Class.forName(arrayDim.toString() + typeChar); - } - } else { - isArray = false; - } - switch (typeChar) { - case 'Z': - sigClasses.add(isArray ? primArrayClass : boolean.class); - break; - case 'I': - sigClasses.add(isArray ? primArrayClass : int.class); - break; - case 'J': - sigClasses.add(isArray ? primArrayClass : long.class); - break; - case 'F': - sigClasses.add(isArray ? primArrayClass : float.class); - break; - case 'D': - sigClasses.add(isArray ? primArrayClass : double.class); - break; - case 'B': - sigClasses.add(isArray ? primArrayClass : byte.class); - break; - case 'S': - sigClasses.add(isArray ? primArrayClass : short.class); - break; - case 'C': - sigClasses.add(isArray ? primArrayClass : char.class); - break; - case 'L': - sb = new StringBuilder(); - symbol = str.read(); - while (symbol != ';') { - sb.append((char) symbol); - symbol = str.read(); - } - klassName = sb.toString().replaceAll("/", "\\."); - if (isArray) { - klassName = arrayDim.toString() + "L" + klassName + ";"; - } - Class klass = Class.forName(klassName); - sigClasses.add(klass); - break; - default: - throw new Error("Unknown type " + typeChar); - } - symbol = str.read(); + // Actual parsing + .map(MethodTemplate::parse) + .collect(Collectors.toList()); + } catch (IOException ex) { + throw new Error("Error reading exclude method file", ex); } - } catch (IOException ex) { - throw new Error("Unexpected exception while parsing exclude methods file", ex); + } else { + methodsToExclude = new ArrayList<>(); } - return sigClasses; } } diff --git a/test/lib-test/jdk/test/lib/jittester/MethodTemplateTest.java b/test/lib-test/jdk/test/lib/jittester/MethodTemplateTest.java new file mode 100644 index 00000000000..3cee24b4684 --- /dev/null +++ b/test/lib-test/jdk/test/lib/jittester/MethodTemplateTest.java @@ -0,0 +1,128 @@ +/* + * 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. + */ + +package jdk.test.lib.jittester; + +import java.lang.reflect.Executable; + +import org.testng.annotations.Test; +import static org.testng.Assert.*; + +/* + * @test + * @summary Unit tests for JITTester string method templates + * + * @library /test/lib + * /test/hotspot/jtreg/testlibrary/jittester/src + * + * @run testng jdk.test.lib.jittester.MethodTemplateTest + */ +public class MethodTemplateTest { + + @Test + public void testMatchingPatterns() throws NoSuchMethodException { + Tester.forMethod(System.class, "getenv", String.class) + .assertMatches("java/lang/System::getenv(Ljava/lang/String;)") + .assertMatches("*::getenv(Ljava/lang/String;)") + .assertMatches("java/lang/*::getenv(Ljava/lang/String;)") + .assertMatches("java/lang/System::*env*(Ljava/lang/String;)") + .assertMatches("java/lang/System::getenv") + .assertMatches("java/lang/System::getenv(*)"); + + Tester.forCtor(RuntimeException.class, Throwable.class) + .assertMatches("java/lang/RuntimeException::RuntimeException(Ljava/lang/Throwable;)"); + + Tester.forMethod(String.class, "regionMatches", int.class, String.class, int.class, int.class) + .assertMatches("java/lang/String::regionMatches(ILjava/lang/String;II)"); + } + + @Test + public void testNonMatchingPatterns() throws NoSuchMethodException { + Tester.forMethod(String.class, "regionMatches", int.class, String.class, int.class, int.class) + .assertDoesNotMatch("java/lang/String::regionMatches(IIILjava/lang/String;)"); + + Tester.forMethod(String.class, "endsWith", String.class) + .assertDoesNotMatch("java/lang/String::startsWith(Ljava/lang/String;)"); + } + + @Test + public void testWildcardStrings() { + assertTrue(new MethodTemplate.WildcardString("Torment") + .matches("Torment")); + + assertTrue(new MethodTemplate.WildcardString("Torm*") + .matches("Torment")); + + assertTrue(new MethodTemplate.WildcardString("*ent") + .matches("Torment")); + + assertTrue(new MethodTemplate.WildcardString("*") + .matches("Something")); + + assertTrue(new MethodTemplate.WildcardString("**") + .matches("Something")); + + assertTrue(new MethodTemplate.WildcardString("*Middle*") + .matches("OnlyMiddleMatches")); + + assertFalse(new MethodTemplate.WildcardString("Wrong") + .matches("Correct")); + assertFalse(new MethodTemplate.WildcardString("Joy") + .matches("Joyfull")); + assertFalse(new MethodTemplate.WildcardString("*Torm*") + .matches("Sorrow")); + } + + static final class Tester { + private final Executable executable; + + private Tester(Executable executable) { + this.executable = executable; + } + + public Tester assertMatches(String stringTemplate) { + MethodTemplate template = MethodTemplate.parse(stringTemplate); + assertTrue(template.matches(executable), + "Method '" + executable + "' does not match template '" + stringTemplate + "'"); + return this; + } + + public Tester assertDoesNotMatch(String stringTemplate) { + MethodTemplate template = MethodTemplate.parse(stringTemplate); + assertFalse(template.matches(executable), + "Method '" + executable + "' erroneously matches template '" + stringTemplate + "'"); + return this; + } + + public static Tester forMethod(Class klass, String name, Class... arguments) + throws NoSuchMethodException { + return new Tester(klass.getDeclaredMethod(name, arguments)); + } + + public static Tester forCtor(Class klass, Class... arguments) + throws NoSuchMethodException { + return new Tester(klass.getConstructor(arguments)); + } + } + +}