Skip to content
4 changes: 3 additions & 1 deletion allure-java-commons/src/main/java/io/qameta/allure/Link.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package io.qameta.allure;

import io.qameta.allure.util.ResultsUtils;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
Expand Down Expand Up @@ -66,5 +68,5 @@
*
* @return the link type.
*/
String type() default "custom";
String type() default ResultsUtils.CUSTOM_LINK_TYPE;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2019 Qameta Software OÜ
*
* 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;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static io.qameta.allure.util.ResultsUtils.CUSTOM_LINK_TYPE;

/**
* Marker annotation. Annotations marked by this annotation will be discovered
* by Allure and added to test results as a link.
*
* @see Link
* @see TmsLink
* @see Issue
*/
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface LinkAnnotation {

/**
* The value of link. In not specified will take value from <code>value()</code>
* method of target annotation.
*
* @return the value of the link to add.
*/
String value() default "";

/**
* This type is used for create an icon for link. Also there is few reserved types such as issue and tms.
*
* @return the link type.
*/
String type() default CUSTOM_LINK_TYPE;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.qameta.allure.util;

import io.qameta.allure.LabelAnnotation;
import io.qameta.allure.LinkAnnotation;
import io.qameta.allure.model.Label;
import io.qameta.allure.model.Link;
import org.slf4j.Logger;
Expand All @@ -26,17 +27,18 @@
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Arrays.asList;

/**
* Collection of utils used by Allure integration to extract meta information from
* test cases via reflection.
Expand Down Expand Up @@ -64,6 +66,7 @@ public static List<Link> getLinks(final AnnotatedElement annotatedElement) {
result.addAll(extractLinks(annotatedElement, io.qameta.allure.Link.class, ResultsUtils::createLink));
result.addAll(extractLinks(annotatedElement, io.qameta.allure.Issue.class, ResultsUtils::createLink));
result.addAll(extractLinks(annotatedElement, io.qameta.allure.TmsLink.class, ResultsUtils::createLink));
result.addAll(extractCustomLinks(asList(annotatedElement.getDeclaredAnnotations())));
return result;
}

Expand All @@ -74,7 +77,7 @@ public static List<Link> getLinks(final AnnotatedElement annotatedElement) {
* @return discovered links.
*/
public static List<Link> getLinks(final Annotation... annotations) {
return getLinks(Arrays.asList(annotations));
return getLinks(asList(annotations));
}

/**
Expand All @@ -88,6 +91,7 @@ public static List<Link> getLinks(final Collection<Annotation> annotations) {
result.addAll(extractLinks(annotations, io.qameta.allure.Link.class, ResultsUtils::createLink));
result.addAll(extractLinks(annotations, io.qameta.allure.Issue.class, ResultsUtils::createLink));
result.addAll(extractLinks(annotations, io.qameta.allure.TmsLink.class, ResultsUtils::createLink));
result.addAll(extractCustomLinks(annotations));
return result;
}

Expand All @@ -109,7 +113,7 @@ public static Set<Label> getLabels(final AnnotatedElement annotatedElement) {
* @return discovered labels.
*/
public static Set<Label> getLabels(final Annotation... annotations) {
return getLabels(Arrays.asList(annotations));
return getLabels(asList(annotations));
}

/**
Expand All @@ -129,6 +133,7 @@ public static Set<Label> getLabels(final Collection<Annotation> annotations) {
private static <T extends Annotation> Set<Link> extractLinks(final AnnotatedElement element,
final Class<T> annotationType,
final Function<T, Link> mapper) {

return Stream.of(element.getAnnotationsByType(annotationType))
.map(mapper)
.collect(Collectors.toSet());
Expand All @@ -146,6 +151,32 @@ private static <T extends Annotation> Set<Link> extractLinks(final Collection<An
.collect(Collectors.toSet());
}

private static Collection<? extends Link> extractCustomLinks(final Collection<Annotation> annotations) {
return annotations.stream()
.flatMap(AnnotationUtils::extractRepeatable)
.filter(annotation -> annotation.annotationType().isAnnotationPresent(LinkAnnotation.class))
.flatMap(annotation -> AnnotationUtils.toLink(annotation).stream())
.collect(Collectors.toSet());
}

private static Set<Link> toLink(final Annotation annotation) {
final LinkAnnotation linkAnnotation = annotation.annotationType().getAnnotation(LinkAnnotation.class);

try {
final Method method = annotation.annotationType().getMethod(VALUE_METHOD_NAME);
final Object object = method.invoke(annotation);
return objectToStringStream(object)
.map(value -> ResultsUtils.createLink("", value, "", linkAnnotation.type()))
.collect(Collectors.toSet());
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
LOGGER.error(
"Invalid annotation {}: marker annotations should contains value() method",
annotation
);
}
return Collections.emptySet();
}

private static Set<Label> getMarks(final Annotation annotation) {
return Stream.of(annotation.annotationType().getAnnotationsByType(LabelAnnotation.class))
.map(marker -> getLabel(annotation, marker))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public final class ResultsUtils {

public static final String ISSUE_LINK_TYPE = "issue";
public static final String TMS_LINK_TYPE = "tms";
public static final String CUSTOM_LINK_TYPE = "custom";

public static final String ALLURE_ID_LABEL_NAME = "AS_ID";
public static final String SUITE_LABEL_NAME = "suite";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import io.qameta.allure.Issues;
import io.qameta.allure.LabelAnnotation;
import io.qameta.allure.Link;
import io.qameta.allure.LinkAnnotation;
import io.qameta.allure.Links;
import io.qameta.allure.Story;
import io.qameta.allure.TmsLink;
Expand Down Expand Up @@ -192,6 +193,18 @@ void shouldExtractLinksFromAnnotationList() {
);
}

@ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE)
@SystemProperty(name = "allure.link.custom.pattern", value = "https://example.org/custom/{}")
@SystemProperty(name = "allure.link.tms.pattern", value = "https://tms.com/custom/{}")
@Test
void shouldExtractCustomLinks() {
assertThat(getLinks(WithCustomLink.class.getDeclaredAnnotations()))
.extracting(io.qameta.allure.model.Link::getUrl)
.containsOnly("https://example.org/custom/LINK-2",
"https://example.org/custom/LINK-1",
"https://tms.com/custom/ISSUE-1");
}

@Epic("e1")
@Feature("f1")
@Story("s1")
Expand Down Expand Up @@ -277,4 +290,22 @@ class WithLinks {
public @interface CustomMultiLabel {
}

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@LinkAnnotation
public @interface CustomLink {
String value() default "";
}

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@LinkAnnotation(type = "tms")
public @interface CustomIssue {
String value();
}

@CustomLink("LINK-2")
@Link("LINK-1")
@CustomIssue("ISSUE-1")
class WithCustomLink { }
}