From e1fad965e73a2a1dc6c84606e16c504505cc4d6b Mon Sep 17 00:00:00 2001 From: Robbie Gemmell Date: Wed, 13 Nov 2024 16:38:07 +0000 Subject: [PATCH] ARTEMIS-5110: add tests for ID validations and suggestions done by log annotation processor Also improve an error message to report the LogBundle class like others do. --- .../processor/LogAnnotationProcessor.java | 25 ++-- .../tests/pom.xml | 7 ++ .../processor/LogAnnotationProcessorTest.java | 110 ++++++++++++++++++ .../cases/LAPTCase1_InvalidIDForRegex.java | 30 +++++ .../cases/LAPTCase2_InvalidIDReused.java | 33 ++++++ .../cases/LAPTCase3_InvalidIDRetired.java | 38 ++++++ .../LAPTCase4_InvalidIDRetiredWithGap.java | 37 ++++++ .../cases/LAPTCase5_InvalidRetiredID.java | 33 ++++++ .../cases/LAPTCase6_UnsortedRetiredID.java | 33 ++++++ artemis-pom/pom.xml | 7 ++ pom.xml | 1 + 11 files changed, 343 insertions(+), 11 deletions(-) create mode 100644 artemis-log-annotation-processor/tests/src/test/java/org/apache/activemq/artemis/logs/annotation/processor/LogAnnotationProcessorTest.java create mode 100644 artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase1_InvalidIDForRegex.java create mode 100644 artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase2_InvalidIDReused.java create mode 100644 artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase3_InvalidIDRetired.java create mode 100644 artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase4_InvalidIDRetiredWithGap.java create mode 100644 artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase5_InvalidRetiredID.java create mode 100644 artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase6_UnsortedRetiredID.java diff --git a/artemis-log-annotation-processor/src/main/java/org/apache/activemq/artemis/logs/annotation/processor/LogAnnotationProcessor.java b/artemis-log-annotation-processor/src/main/java/org/apache/activemq/artemis/logs/annotation/processor/LogAnnotationProcessor.java index 0b1b838b7c..70b48aaf1f 100644 --- a/artemis-log-annotation-processor/src/main/java/org/apache/activemq/artemis/logs/annotation/processor/LogAnnotationProcessor.java +++ b/artemis-log-annotation-processor/src/main/java/org/apache/activemq/artemis/logs/annotation/processor/LogAnnotationProcessor.java @@ -154,7 +154,7 @@ public class LogAnnotationProcessor extends AbstractProcessor { int generatedPaths = 0; if (messageAnnotation != null) { - validateRegexID(bundleAnnotation, messageAnnotation.id()); + validateRegexID(bundleAnnotation.regexID(), executableMember, messageAnnotation.id()); generatedPaths++; if (DEBUG) { debug("... annotated with " + messageAnnotation); @@ -163,7 +163,7 @@ public class LogAnnotationProcessor extends AbstractProcessor { } if (logAnnotation != null) { - validateRegexID(bundleAnnotation, logAnnotation.id()); + validateRegexID(bundleAnnotation.regexID(), executableMember, logAnnotation.id()); generatedPaths++; if (DEBUG) { debug("... annotated with " + logAnnotation); @@ -205,17 +205,19 @@ public class LogAnnotationProcessor extends AbstractProcessor { return true; } - private static void validateRegexID(LogBundle bundleAnnotation, long id) { - if (!isAllowedIDValue(bundleAnnotation, id)) { - throw new IllegalArgumentException("Code " + id + " does not match regular expression \"" + bundleAnnotation.regexID() + "\" specified on the LogBundle"); + private static void validateRegexID(String regexID, ExecutableElement executableMember, long id) { + if (!isAllowedIDValue(regexID, id)) { + String enclosingClass = executableMember.getEnclosingElement().toString(); + + throw new IllegalArgumentException(enclosingClass + ": Code " + id + " does not match regular expression specified on the LogBundle: " + regexID); } } - private static boolean isAllowedIDValue(LogBundle bundleAnnotation, long id) { - if (bundleAnnotation.regexID() != null && !bundleAnnotation.regexID().isEmpty()) { + private static boolean isAllowedIDValue(String regexID, long id) { + if (regexID != null && !regexID.isEmpty()) { String toStringID = Long.toString(id); - return toStringID.matches(bundleAnnotation.regexID()); + return toStringID.matches(regexID); } return true; @@ -535,7 +537,7 @@ public class LogAnnotationProcessor extends AbstractProcessor { nextId++; } - if (isAllowedIDValue(bundleAnnotation, nextId)) { + if (isAllowedIDValue(bundleAnnotation.regexID(), nextId)) { failure.append("Consider trying ID ").append(nextId).append(" which is the next unused value."); } else { failure.append("There are no new IDs available within the given ID regex: " + bundleAnnotation.regexID()); @@ -575,9 +577,10 @@ public class LogAnnotationProcessor extends AbstractProcessor { return; } + String regexID = bundleAnnotation.regexID(); for (int id : retiredIDs) { - if (!isAllowedIDValue(bundleAnnotation, id)) { - throw new IllegalArgumentException(annotatedType + ": The retiredIDs elements must each match the configured regexID. The ID " + id + " does not match: " + bundleAnnotation.regexID()); + if (!isAllowedIDValue(regexID, id)) { + throw new IllegalArgumentException(annotatedType + ": The retiredIDs elements must each match the configured regexID. The ID " + id + " does not match: " + regexID); } } diff --git a/artemis-log-annotation-processor/tests/pom.xml b/artemis-log-annotation-processor/tests/pom.xml index 505cc183d6..9511d2fd36 100644 --- a/artemis-log-annotation-processor/tests/pom.xml +++ b/artemis-log-annotation-processor/tests/pom.xml @@ -70,5 +70,12 @@ junit-jupiter-engine test + + + + com.karuslabs + elementary + test + diff --git a/artemis-log-annotation-processor/tests/src/test/java/org/apache/activemq/artemis/logs/annotation/processor/LogAnnotationProcessorTest.java b/artemis-log-annotation-processor/tests/src/test/java/org/apache/activemq/artemis/logs/annotation/processor/LogAnnotationProcessorTest.java new file mode 100644 index 0000000000..81e631125b --- /dev/null +++ b/artemis-log-annotation-processor/tests/src/test/java/org/apache/activemq/artemis/logs/annotation/processor/LogAnnotationProcessorTest.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.activemq.artemis.logs.annotation.processor; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.invoke.MethodHandles; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.karuslabs.elementary.Finder; +import com.karuslabs.elementary.Results; +import com.karuslabs.elementary.junit.JavacExtension; +import com.karuslabs.elementary.junit.annotations.Options; +import com.karuslabs.elementary.junit.annotations.Processors; +import com.karuslabs.elementary.junit.annotations.Resource; + +@ExtendWith(JavacExtension.class) +@Options("-Werror") +@Processors(LogAnnotationProcessor.class) +public class LogAnnotationProcessorTest { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + @Test + @Resource("org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase1_InvalidIDForRegex.java") + public void testIDInvalidForGivenRegex(Results results) { + final String expectedMessage = "org.apache.activemq.artemis.logs.annotation.processor.cases.LAPTCase1_InvalidIDForRegex: " + + "Code 100 does not match regular expression specified on the LogBundle: [0-9]{1}"; + + doCheckFailureErrorMessageTestImpl(results, expectedMessage); + } + + @Test + @Resource("org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase2_InvalidIDReused.java") + public void testIDInvalidReused(Results results) { + final String expectedMessage = "org.apache.activemq.artemis.logs.annotation.processor.cases.LAPTCase2_InvalidIDReused: " + + "ID 3 with message 'reusedID' was previously used already, to define message 'initialIDuse'. " + + "Consider trying ID 5 which is the next unused value."; + + doCheckFailureErrorMessageTestImpl(results, expectedMessage); + } + + @Test + @Resource("org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase3_InvalidIDRetired.java") + public void testIDInvalidRetired(Results results) { + final String expectedMessage = "org.apache.activemq.artemis.logs.annotation.processor.cases.LAPTCase3_InvalidIDRetired: " + + "ID 2 was previously retired, another ID must be used. " + + "Consider trying ID 7 which is the next unused value."; + + doCheckFailureErrorMessageTestImpl(results, expectedMessage); + } + + @Test + @Resource("org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase4_InvalidIDRetiredWithGap.java") + public void testIDInvalidRetiredWithGap(Results results) { + final String expectedMessage = "org.apache.activemq.artemis.logs.annotation.processor.cases.LAPTCase4_InvalidIDRetiredWithGap: " + + "ID 2 was previously retired, another ID must be used. " + + "Consider trying ID 4 which is the next unused value."; + + doCheckFailureErrorMessageTestImpl(results, expectedMessage); + } + + @Test + @Resource("org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase5_InvalidRetiredID.java") + public void testInvalidRetiredID(Results results) { + final String expectedMessage = "org.apache.activemq.artemis.logs.annotation.processor.cases.LAPTCase5_InvalidRetiredID: " + + "The retiredIDs elements must each match the configured regexID. " + + "The ID 10 does not match: [0-9]{1}"; + + doCheckFailureErrorMessageTestImpl(results, expectedMessage); + } + + @Test + @Resource("org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase6_UnsortedRetiredID.java") + public void testUnsortedRetiredIDs(Results results) { + final String expectedMessage = "org.apache.activemq.artemis.logs.annotation.processor.cases.LAPTCase6_UnsortedRetiredID: " + + "The retiredIDs value must be sorted. Try using: {2, 4, 5}"; + + doCheckFailureErrorMessageTestImpl(results, expectedMessage); + } + + private void doCheckFailureErrorMessageTestImpl(Results results, String expectedMessage) { + Finder errors = results.find().errors(); + List errorMessages = errors.messages(); + + logger.trace("Error result messages: {}", errorMessages); + + assertEquals(1, errors.count(), () -> "Expected 1 error result. Got : " + errorMessages); + assertEquals(expectedMessage, errorMessages.get(0), "Did not get expected error message."); + } +} diff --git a/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase1_InvalidIDForRegex.java b/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase1_InvalidIDForRegex.java new file mode 100644 index 0000000000..6c5a9a918e --- /dev/null +++ b/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase1_InvalidIDForRegex.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.activemq.artemis.logs.annotation.processor.cases; + +import org.apache.activemq.artemis.logs.annotation.LogBundle; +import org.apache.activemq.artemis.logs.annotation.Message; + +// Test class used by LogAnnotationProcessorTest to check failure modes. + +@LogBundle(projectCode = "LAPTCase1", regexID="[0-9]{1}") +public interface LAPTCase1_InvalidIDForRegex { + + // Regex allows IDs 0-9, use a higher value that fails checks. + @Message(id = 100, value = "InvalidIDForRegex") + String invalidIDForRegex(); +} diff --git a/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase2_InvalidIDReused.java b/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase2_InvalidIDReused.java new file mode 100644 index 0000000000..a87eb1ff79 --- /dev/null +++ b/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase2_InvalidIDReused.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.activemq.artemis.logs.annotation.processor.cases; + +import org.apache.activemq.artemis.logs.annotation.LogBundle; +import org.apache.activemq.artemis.logs.annotation.Message; + +// Test class used by LogAnnotationProcessorTest to check failure modes. + +@LogBundle(projectCode = "LAPTCase2", regexID="[0-9]{1}", retiredIDs = { 1, 2, 4 }) +public interface LAPTCase2_InvalidIDReused { + + @Message(id = 3, value = "initialIDuse") + String initialIDuse(); + + // Used ID 3 again, not allowed. Error will suggest ID 5 (rather than 4, since it is also retired). + @Message(id = 3, value = "reusedID") + String reusedID(); +} diff --git a/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase3_InvalidIDRetired.java b/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase3_InvalidIDRetired.java new file mode 100644 index 0000000000..15b7f3b786 --- /dev/null +++ b/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase3_InvalidIDRetired.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.activemq.artemis.logs.annotation.processor.cases; + +import org.apache.activemq.artemis.logs.annotation.LogBundle; + +import org.apache.activemq.artemis.logs.annotation.Message; + +// Test class used by LogAnnotationProcessorTest to check failure modes. + +@LogBundle(projectCode = "LAPTCase3", regexID="[0-9]{1}", retiredIDs = { 2, 5, 6 }) +public interface LAPTCase3_InvalidIDRetired { + + @Message(id = 1, value = "ignoreMe") + String ignoreMe(); + + // Used ID 2, not allowed due to being marked retired. Error will suggest ID 7 + // (4 is highest 'active' ID, and 5 and 6 are also retired). + @Message(id = 2, value = "retiredID") + String retiredID(); + + @Message(id = 4, value = "ignoreMeAlso") + String ignoreMeAlso(); +} diff --git a/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase4_InvalidIDRetiredWithGap.java b/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase4_InvalidIDRetiredWithGap.java new file mode 100644 index 0000000000..8fe8ad77f5 --- /dev/null +++ b/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase4_InvalidIDRetiredWithGap.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.activemq.artemis.logs.annotation.processor.cases; + +import org.apache.activemq.artemis.logs.annotation.LogBundle; +import org.apache.activemq.artemis.logs.annotation.Message; + +// Test class used by LogAnnotationProcessorTest to check failure modes. + +@LogBundle(projectCode = "LAPTCase4", regexID="[0-9]{1}", retiredIDs = { 2, 9 }) +public interface LAPTCase4_InvalidIDRetiredWithGap { + + @Message(id = 1, value = "ignoreMe") + String ignoreMe(); + + // Used ID 2, not allowed due to being marked retired. Error will suggest ID 4 + // (since although 9 is retired there is an 'active' gap, 4 is next highest free 'active' not-retired ID)). + @Message(id = 2, value = "retiredID") + String retiredID(); + + @Message(id = 3, value = "ignoreMeAlso") + String ignoreMeAlso(); +} diff --git a/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase5_InvalidRetiredID.java b/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase5_InvalidRetiredID.java new file mode 100644 index 0000000000..eae3a0a8ac --- /dev/null +++ b/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase5_InvalidRetiredID.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.activemq.artemis.logs.annotation.processor.cases; + +import org.apache.activemq.artemis.logs.annotation.LogBundle; +import org.apache.activemq.artemis.logs.annotation.Message; + +// Test class used by LogAnnotationProcessorTest to check failure modes. + +// Regex allows IDs 0-9, use higher value for a retiredIDs entry to check it fails. +@LogBundle(projectCode = "LAPTCase5", regexID="[0-9]{1}", retiredIDs = { 2, 4, 10}) +public interface LAPTCase5_InvalidRetiredID { + + @Message(id = 1, value = "ignoreMe") + String ignoreMe(); + + @Message(id = 3, value = "ignoreMeAlso") + String ignoreMeAlso(); +} diff --git a/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase6_UnsortedRetiredID.java b/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase6_UnsortedRetiredID.java new file mode 100644 index 0000000000..be7fedd573 --- /dev/null +++ b/artemis-log-annotation-processor/tests/src/test/resources/org/apache/activemq/artemis/logs/annotation/processor/cases/LAPTCase6_UnsortedRetiredID.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.activemq.artemis.logs.annotation.processor.cases; + +import org.apache.activemq.artemis.logs.annotation.LogBundle; +import org.apache.activemq.artemis.logs.annotation.Message; + +// Test class used by LogAnnotationProcessorTest to check failure modes. + +// The retiredIDs must be sorted, provide some that are not. The error will give sorted version. +@LogBundle(projectCode = "LAPTCase6", regexID="[0-9]{1}", retiredIDs = { 2, 5, 4}) +public interface LAPTCase6_UnsortedRetiredID { + + @Message(id = 1, value = "ignoreMe") + String ignoreMe(); + + @Message(id = 3, value = "ignoreMeAlso") + String ignoreMeAlso(); +} diff --git a/artemis-pom/pom.xml b/artemis-pom/pom.xml index 05423ff1e6..df9003d6cc 100644 --- a/artemis-pom/pom.xml +++ b/artemis-pom/pom.xml @@ -578,6 +578,13 @@ test + + com.karuslabs + elementary + ${elementary.version} + test + + io.micrometer diff --git a/pom.xml b/pom.xml index a52fa9591b..bcf5e98efd 100644 --- a/pom.xml +++ b/pom.xml @@ -183,6 +183,7 @@ 2.1.0 + 3.0.0 4.0.24 3.4.1 5.15.0