diff --git a/allure-java-commons/src/main/java/io/qameta/allure/util/AspectUtils.java b/allure-java-commons/src/main/java/io/qameta/allure/util/AspectUtils.java index e2ae30de7..a15872be4 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/util/AspectUtils.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/util/AspectUtils.java @@ -15,7 +15,6 @@ */ package io.qameta.allure.util; -import io.qameta.allure.Param; import io.qameta.allure.model.Parameter; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.reflect.MethodSignature; @@ -26,10 +25,8 @@ import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.IntStream; -import java.util.stream.Stream; import static io.qameta.allure.util.NamingUtils.processNameTemplate; -import static io.qameta.allure.util.ResultsUtils.createParameter; /** * Utility methods used by AspectJ-based Allure integrations. @@ -85,24 +82,14 @@ public static Map getParametersMap(final JoinPoint joinPoint) { */ public static List getParameters(final MethodSignature signature, final Object... args) { final java.lang.reflect.Parameter[] params = signature.getMethod().getParameters(); + final String[] parameterNames = signature.getParameterNames(); return IntStream .range(0, args.length) .mapToObj(index -> { - final Parameter parameter = createParameter(signature.getParameterNames()[index], args[index]); final java.lang.reflect.Parameter ref = params[index]; - Stream.of(ref.getAnnotationsByType(Param.class)) - .findFirst() - .ifPresent(param -> { - Stream.of(param.value(), param.name()) - .map(String::trim) - .filter(name -> name.length() > 0) - .findFirst() - .ifPresent(parameter::setName); - - parameter.setMode(param.mode()); - parameter.setExcluded(param.excluded()); - }); - return parameter; + final String name = Optional.ofNullable(parameterNames[index]) + .orElseGet(ref::getName); + return ParameterUtils.createParameter(ref, args[index], name); }) .collect(Collectors.toList()); } diff --git a/allure-java-commons/src/main/java/io/qameta/allure/util/ParameterUtils.java b/allure-java-commons/src/main/java/io/qameta/allure/util/ParameterUtils.java index 64bcf457d..cefafbda0 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/util/ParameterUtils.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/util/ParameterUtils.java @@ -50,24 +50,51 @@ public static List createParameters(final Method method, .mapToObj(i -> { final java.lang.reflect.Parameter parameter = parameters[i]; final Object value = args[i]; - final Param annotation = parameter.getAnnotation(Param.class); - if (Objects.isNull(annotation)) { - return ResultsUtils.createParameter(parameter.getName(), value); - } - final String name = Stream.of(annotation.value(), annotation.name(), parameter.getName()) - .map(String::trim) - .filter(s -> s.length() > 0) - .findFirst() - .orElseGet(() -> "arg" + i); - - return ResultsUtils.createParameter( - name, - value, - annotation.excluded(), - annotation.mode() - ); + return createParameter(parameter, value); }) .collect(Collectors.toList()); } + /** + * Creates and returns the parameter using the Java reflection parameter name as a fallback. + * + * @param parameter the Java parameter to inspect + * @param value the value + * @return the parameter + */ + public static Parameter createParameter(final java.lang.reflect.Parameter parameter, + final Object value) { + return createParameter(parameter, value, parameter.getName()); + } + + /** + * Creates and returns the parameter. + * + * @param parameter the framework or Java parameter to inspect + * @param value the value + * @param defaultName the name to use when {@link Param} does not override it + * @return the parameter + */ + public static Parameter createParameter(final java.lang.reflect.Parameter parameter, + final Object value, + final String defaultName) { + Objects.requireNonNull(defaultName, "defaultName"); + final Param annotation = parameter.getAnnotation(Param.class); + if (Objects.isNull(annotation)) { + return ResultsUtils.createParameter(defaultName, value); + } + final String name = Stream.of(annotation.value(), annotation.name(), defaultName) + .map(String::trim) + .filter(s -> s.length() > 0) + .findFirst() + .orElse(defaultName); + + return ResultsUtils.createParameter( + name, + value, + annotation.excluded(), + annotation.mode() + ); + } + } diff --git a/allure-java-commons/src/test/java/io/qameta/allure/aspects/StepsAspectsTest.java b/allure-java-commons/src/test/java/io/qameta/allure/aspects/StepsAspectsTest.java index c8df6770e..ee52b41c0 100644 --- a/allure-java-commons/src/test/java/io/qameta/allure/aspects/StepsAspectsTest.java +++ b/allure-java-commons/src/test/java/io/qameta/allure/aspects/StepsAspectsTest.java @@ -15,6 +15,7 @@ */ package io.qameta.allure.aspects; +import io.qameta.allure.Allure; import io.qameta.allure.Issue; import io.qameta.allure.Param; import io.qameta.allure.Step; @@ -102,15 +103,17 @@ void shouldUseMethodPlaceholder() { void shouldAddParams() { final AllureResults results = runWithinTestContext(() -> stepWithParams("first", "second")); - assertThat(results.getTestResults()) - .hasSize(1) - .flatExtracting(TestResult::getSteps) - .flatExtracting(StepResult::getParameters) - .extracting(Parameter::getName, Parameter::getValue) - .containsExactlyInAnyOrder( - tuple("a", "first"), - tuple("b", "second") - ); + Allure.step( + "Assert step parameters keep AspectJ names", () -> assertThat(results.getTestResults()) + .hasSize(1) + .flatExtracting(TestResult::getSteps) + .flatExtracting(StepResult::getParameters) + .extracting(Parameter::getName, Parameter::getValue) + .containsExactlyInAnyOrder( + tuple("a", "first"), + tuple("b", "second") + ) + ); } @Test diff --git a/allure-java-commons/src/test/java/io/qameta/allure/util/ParameterUtilsTest.java b/allure-java-commons/src/test/java/io/qameta/allure/util/ParameterUtilsTest.java new file mode 100644 index 000000000..6b1e65d8e --- /dev/null +++ b/allure-java-commons/src/test/java/io/qameta/allure/util/ParameterUtilsTest.java @@ -0,0 +1,124 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed 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 io.qameta.allure.util; + +import io.qameta.allure.Param; +import io.qameta.allure.model.Parameter; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static io.qameta.allure.Allure.addAttachment; +import static io.qameta.allure.Allure.step; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.assertj.core.api.Assertions.tuple; + +class ParameterUtilsTest { + + @Test + void shouldApplyParamAnnotationToFrameworkParameter() throws NoSuchMethodException { + step("Create framework parameters and verify @Param metadata", () -> { + final Method method = getClass().getDeclaredMethod( + "methodWithParameters", + String.class, + String.class, + String.class + ); + final List parameters = Arrays.asList( + ParameterUtils.createParameter(method.getParameters()[0], "1", "first"), + ParameterUtils.createParameter(method.getParameters()[1], "2", "second"), + ParameterUtils.createParameter(method.getParameters()[2], "3", "third") + ); + + addAttachment( + "created parameters", "text/plain", parameters.stream() + .map( + parameter -> String.format( + "%s=%s excluded=%s mode=%s", + parameter.getName(), + parameter.getValue(), + parameter.getExcluded(), + parameter.getMode() + ) + ) + .collect(Collectors.joining("\n")) + ); + + assertThat(parameters) + .extracting(Parameter::getName, Parameter::getValue, Parameter::getExcluded, Parameter::getMode) + .containsExactly( + tuple("Named", "1", false, Parameter.Mode.DEFAULT), + tuple("Hidden", "2", true, Parameter.Mode.HIDDEN), + tuple("third", "3", null, null) + ); + }); + } + + @Test + void shouldUseReflectionNameWhenFrameworkNameIsNotProvided() throws NoSuchMethodException { + step("Create parameters with reflection names and verify @Param metadata", () -> { + final Method method = getClass().getDeclaredMethod( + "methodWithParameters", + String.class, + String.class, + String.class + ); + final List parameters = Arrays.asList( + ParameterUtils.createParameter(method.getParameters()[0], "1"), + ParameterUtils.createParameter(method.getParameters()[1], "2"), + ParameterUtils.createParameter(method.getParameters()[2], "3") + ); + + assertThat(parameters) + .extracting(Parameter::getName, Parameter::getValue, Parameter::getExcluded, Parameter::getMode) + .containsExactly( + tuple("Named", "1", false, Parameter.Mode.DEFAULT), + tuple("Hidden", "2", true, Parameter.Mode.HIDDEN), + tuple("plain", "3", null, null) + ); + }); + } + + @Test + void shouldRejectNullFrameworkDefaultName() throws NoSuchMethodException { + step("Verify framework default name is required", () -> { + final Method method = getClass().getDeclaredMethod( + "methodWithParameters", + String.class, + String.class, + String.class + ); + + assertThatNullPointerException() + .isThrownBy(() -> ParameterUtils.createParameter(method.getParameters()[0], "1", null)) + .withMessage("defaultName"); + }); + } + + void methodWithParameters(@Param("Named") final String named, + @Param( + name = "Hidden", + excluded = true, + mode = Parameter.Mode.HIDDEN + ) final String hidden, + final String plain) { + } + +} diff --git a/allure-testng/src/main/java/io/qameta/allure/testng/AllureTestNg.java b/allure-testng/src/main/java/io/qameta/allure/testng/AllureTestNg.java index 5b7a2e997..694028501 100644 --- a/allure-testng/src/main/java/io/qameta/allure/testng/AllureTestNg.java +++ b/allure-testng/src/main/java/io/qameta/allure/testng/AllureTestNg.java @@ -33,6 +33,7 @@ import io.qameta.allure.testng.config.AllureTestNgConfig; import io.qameta.allure.util.AnnotationUtils; import io.qameta.allure.util.ObjectUtils; +import io.qameta.allure.util.ParameterUtils; import io.qameta.allure.util.ResultsUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -836,9 +837,9 @@ private String getUniqueUuid(final IAttributes suite) { private List getParameters(final ITestContext context, final ITestNGMethod method, final Object... parameters) { - final Map result = new HashMap<>( - context.getCurrentXmlTest().getAllParameters() - ); + final Map result = new HashMap<>(); + context.getCurrentXmlTest().getAllParameters() + .forEach((name, value) -> result.put(name, createParameter(name, value))); final Object instance = method.getInstance(); if (nonNull(instance)) { Stream.of(instance.getClass().getDeclaredFields()) @@ -851,7 +852,7 @@ private List getParameters(final ITestContext context, try { field.setAccessible(true); final String value = ObjectUtils.toString(field.get(instance)); - result.put(name, value); + result.put(name, createParameter(name, value)); } catch (IllegalAccessException e) { LOGGER.debug("Could not access field value"); } @@ -869,9 +870,7 @@ private List getParameters(final ITestContext context, .map(Parameters::value) .orElse(new String[]{}); - final String[] reflectionNames = Stream.of(m.getParameters()) - .map(java.lang.reflect.Parameter::getName) - .toArray(String[]::new); + final java.lang.reflect.Parameter[] reflectionParameters = m.getParameters(); int skippedCount = 0; for (int i = 0; i < parameterTypes.length; i++) { @@ -882,20 +881,23 @@ private List getParameters(final ITestContext context, } final int indexFromAnnotation = i - skippedCount; - if (indexFromAnnotation < providedNames.length) { - result.put(providedNames[indexFromAnnotation], ObjectUtils.toString(parameters[i])); - continue; - } - - if (i < reflectionNames.length) { - result.put(reflectionNames[i], ObjectUtils.toString(parameters[i])); + final String defaultName = indexFromAnnotation < providedNames.length + ? providedNames[indexFromAnnotation] + : reflectionParameters[i].getName(); + final Parameter parameter = ParameterUtils.createParameter( + reflectionParameters[i], + parameters[i], + defaultName + ); + if (nonNull(parameter.getName())) { + result.remove(defaultName); + result.put(parameter.getName(), parameter); } } }); - return result.entrySet().stream() - .map(entry -> createParameter(entry.getKey(), entry.getValue())) + return result.values().stream() .collect(Collectors.toList()); } diff --git a/allure-testng/src/test/java/io/qameta/allure/testng/AllureTestNgTest.java b/allure-testng/src/test/java/io/qameta/allure/testng/AllureTestNgTest.java index 8a968a693..18c7cc7d0 100644 --- a/allure-testng/src/test/java/io/qameta/allure/testng/AllureTestNgTest.java +++ b/allure-testng/src/test/java/io/qameta/allure/testng/AllureTestNgTest.java @@ -15,7 +15,9 @@ */ package io.qameta.allure.testng; +import io.qameta.allure.AllureLifecycle; import io.qameta.allure.Issue; +import io.qameta.allure.Param; import io.qameta.allure.Step; import io.qameta.allure.model.Attachment; import io.qameta.allure.model.FixtureResult; @@ -31,6 +33,7 @@ import io.qameta.allure.model.WithSteps; import io.qameta.allure.test.AllureFeatures; import io.qameta.allure.test.AllureResults; +import io.qameta.allure.test.AllureResultsWriterStub; import io.qameta.allure.test.RunUtils; import io.qameta.allure.testfilter.TestPlan; import io.qameta.allure.testfilter.TestPlanV1_0; @@ -44,9 +47,16 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.testng.ITestContext; +import org.testng.ITestNGMethod; +import org.testng.ITestResult; import org.testng.TestNG; +import org.testng.annotations.Parameters; +import org.testng.internal.ConstructorOrMethod; import org.testng.xml.XmlSuite; +import org.testng.xml.XmlTest; +import java.lang.reflect.Method; import java.net.URL; import java.util.Arrays; import java.util.Collection; @@ -66,6 +76,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @SuppressWarnings("deprecation") public class AllureTestNgTest { @@ -1297,6 +1309,141 @@ public void shouldSupportFactoryOnConstructor() { ); } + @SuppressWarnings("unchecked") + @AllureFeatures.Parameters + @Issue("893") + @Test + public void shouldDisplayCustomNamesOfParameters() { + final AllureResults results = runTestNgSuites("suites/gh-893.xml"); + assertThat(results.getTestResults()) + .flatExtracting(TestResult::getParameters) + .extracting(Parameter::getName, Parameter::getValue, Parameter::getExcluded, Parameter::getMode) + .containsExactlyInAnyOrder( + tuple("First", "1", false, Parameter.Mode.DEFAULT), + tuple("Second", "1", false, Parameter.Mode.DEFAULT), + tuple("Third", "2", false, Parameter.Mode.DEFAULT), + tuple("Fourth", "5", true, Parameter.Mode.HIDDEN) + ); + } + + @SuppressWarnings("unchecked") + @AllureFeatures.Parameters + @Issue("893") + @Test + public void shouldDisplayCustomNamesWhenSkippingInjectedParameters() { + final AllureResults results = runTestNgSuites("suites/gh-893-injected-parameters.xml"); + assertThat(results.getTestResults()).hasSize(1); + final TestResult testResult = results.getTestResults().get(0); + assertThat(testResult.getParameters()) + .extracting(Parameter::getName, Parameter::getValue, Parameter::getExcluded, Parameter::getMode) + .containsExactlyInAnyOrder( + tuple("First", "first-value", false, Parameter.Mode.DEFAULT), + tuple("Second", "second-value", true, Parameter.Mode.HIDDEN), + tuple("third", "third-value", false, Parameter.Mode.MASKED), + tuple("fourth", "fourth-value", null, null) + ); + } + + @AllureFeatures.Parameters + @Issue("893") + @Test + public void shouldSkipAllNativeInjectedParameterTypesWhenResolvingNames() throws Exception { + final List parameters = resolveParametersFromMethodWithAllNativeInjectedTypes(); + + assertResolvedCustomParameters(parameters); + } + + @Step("Resolve parameters from method with all native TestNG injected types") + @SuppressWarnings({"unchecked", "PMD.AvoidAccessibilityAlteration"}) + private List resolveParametersFromMethodWithAllNativeInjectedTypes() throws Exception { + final XmlTest xmlTest = new XmlTest(new XmlSuite()); + xmlTest.addParameter("first", "first-value"); + xmlTest.addParameter("second", "second-value"); + xmlTest.addParameter("third", "third-value"); + xmlTest.addParameter("fourth", "fourth-value"); + + final Method source = getClass().getDeclaredMethod( + "methodWithAllNativeInjectedParameterTypes", + Method.class, + String.class, + ITestContext.class, + String.class, + ITestResult.class, + XmlTest.class, + Object[].class, + String.class, + String.class + ); + final ITestContext context = mock(ITestContext.class); + when(context.getCurrentXmlTest()).thenReturn(xmlTest); + + final ITestNGMethod method = mock(ITestNGMethod.class); + when(method.getInstance()).thenReturn(null); + when(method.getConstructorOrMethod()).thenReturn(new ConstructorOrMethod(source)); + + final AllureTestNg adapter = new AllureTestNg( + new AllureLifecycle(new AllureResultsWriterStub()), + new AllureTestNgTestFilter(), + AllureTestNgConfig.loadConfigProperties() + ); + final Method getParameters = AllureTestNg.class.getDeclaredMethod( + "getParameters", + ITestContext.class, + ITestNGMethod.class, + Object[].class + ); + getParameters.setAccessible(true); + + final List parameters = (List) getParameters.invoke( + adapter, + context, + method, + (Object) new Object[]{ + source, + "first-value", + context, + "second-value", + mock(ITestResult.class), + xmlTest, + new Object[]{"ignored"}, + "third-value", + "fourth-value", + } + ); + return parameters; + } + + @Step("Assert custom names and metadata after skipping injected parameters") + private void assertResolvedCustomParameters(final List parameters) { + assertThat(parameters) + .extracting(Parameter::getName, Parameter::getValue, Parameter::getExcluded, Parameter::getMode) + .containsExactlyInAnyOrder( + tuple("First", "first-value", false, Parameter.Mode.DEFAULT), + tuple("Second", "second-value", true, Parameter.Mode.HIDDEN), + tuple("third", "third-value", false, Parameter.Mode.MASKED), + tuple("fourth", "fourth-value", null, null) + ); + } + + @Parameters({"first", "second", "third", "fourth"}) + void methodWithAllNativeInjectedParameterTypes(final Method method, + @Param("First") final String first, + final ITestContext context, + @Param( + name = "Second", + excluded = true, + mode = Parameter.Mode.HIDDEN + ) final String second, + final ITestResult testResult, + final XmlTest xmlTest, + final Object[] parameters, + @Param( + name = " ", + mode = Parameter.Mode.MASKED + ) final String third, + final String fourth) { + } + @SuppressWarnings("unused") private static Stream failedFixtures() { return Stream.of( diff --git a/allure-testng/src/test/java/io/qameta/allure/testng/samples/CustomParameterNamesTest.java b/allure-testng/src/test/java/io/qameta/allure/testng/samples/CustomParameterNamesTest.java new file mode 100644 index 000000000..883539814 --- /dev/null +++ b/allure-testng/src/test/java/io/qameta/allure/testng/samples/CustomParameterNamesTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed 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 io.qameta.allure.testng.samples; + +import io.qameta.allure.Param; +import io.qameta.allure.model.Parameter; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class CustomParameterNamesTest { + + @DataProvider + public static Object[][] testDataForParamNames() { + return new Object[][]{ + {1, 1, 2, 5} + }; + } + + @Test(dataProvider = "testDataForParamNames") + public void sumTest(@Param("First") final Integer first, + @Param(name = "Second") final Integer second, + @Param(name = "Third") final Integer expectedSum, + @Param( + name = "Fourth", + excluded = true, + mode = Parameter.Mode.HIDDEN + ) final Integer unusedParam) { + } + +} diff --git a/allure-testng/src/test/java/io/qameta/allure/testng/samples/CustomParameterNamesWithInjectedParametersTest.java b/allure-testng/src/test/java/io/qameta/allure/testng/samples/CustomParameterNamesWithInjectedParametersTest.java new file mode 100644 index 000000000..e3ad41dfa --- /dev/null +++ b/allure-testng/src/test/java/io/qameta/allure/testng/samples/CustomParameterNamesWithInjectedParametersTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016-2026 Qameta Software Inc + * + * Licensed 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 io.qameta.allure.testng.samples; + +import io.qameta.allure.Param; +import io.qameta.allure.model.Parameter; +import org.testng.ITestContext; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; +import org.testng.xml.XmlTest; + +import java.lang.reflect.Method; + +public class CustomParameterNamesWithInjectedParametersTest { + + @Parameters({"first", "second", "third", "fourth"}) + @Test + public void mixedInjectedParameters(final Method method, + @Param("First") final String first, + final ITestContext context, + @Param( + name = "Second", + excluded = true, + mode = Parameter.Mode.HIDDEN + ) final String second, + final XmlTest xmlTest, + @Param( + name = " ", + mode = Parameter.Mode.MASKED + ) final String third, + final String fourth) { + } + +} diff --git a/allure-testng/src/test/java/io/qameta/allure/testng/samples/TestWithTimeout.java b/allure-testng/src/test/java/io/qameta/allure/testng/samples/TestWithTimeout.java index 506d565fb..94d96e5ae 100644 --- a/allure-testng/src/test/java/io/qameta/allure/testng/samples/TestWithTimeout.java +++ b/allure-testng/src/test/java/io/qameta/allure/testng/samples/TestWithTimeout.java @@ -25,7 +25,7 @@ */ public class TestWithTimeout { - @Test(timeOut = 100) + @Test(timeOut = 5000) public void testWithTimeout() { step("Step of the test with timeout"); } diff --git a/allure-testng/src/test/resources/suites/gh-893-injected-parameters.xml b/allure-testng/src/test/resources/suites/gh-893-injected-parameters.xml new file mode 100644 index 000000000..1e31637f0 --- /dev/null +++ b/allure-testng/src/test/resources/suites/gh-893-injected-parameters.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/allure-testng/src/test/resources/suites/gh-893.xml b/allure-testng/src/test/resources/suites/gh-893.xml new file mode 100644 index 000000000..9d4ae9ed0 --- /dev/null +++ b/allure-testng/src/test/resources/suites/gh-893.xml @@ -0,0 +1,10 @@ + + + + + + + + + +