diff --git a/allure-java-commons-test/build.gradle b/allure-java-commons-test/build.gradle index 6926b596e..030ef1e32 100644 --- a/allure-java-commons-test/build.gradle +++ b/allure-java-commons-test/build.gradle @@ -10,7 +10,8 @@ configurations { } dependencies { - compile project(':allure-java-commons') + compile('commons-io:commons-io') + compile(project(':allure-java-commons')) } jar { diff --git a/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureResultsWriterStub.java b/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureResultsWriterStub.java index 2742af4c6..7e99193cc 100644 --- a/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureResultsWriterStub.java +++ b/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureResultsWriterStub.java @@ -3,18 +3,24 @@ import io.qameta.allure.AllureResultsWriter; import io.qameta.allure.model.TestResult; import io.qameta.allure.model.TestResultContainer; +import org.apache.commons.io.IOUtils; +import java.io.IOException; import java.io.InputStream; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; /** * @author Egor Borisov ehborisov@gmail.com */ +@SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes") public class AllureResultsWriterStub implements AllureResultsWriter { private final List testResults = new CopyOnWriteArrayList<>(); private final List testContainers = new CopyOnWriteArrayList<>(); + private final Map attachments = new ConcurrentHashMap<>(); public void write(final TestResult testResult) { testResults.add(testResult); @@ -25,7 +31,12 @@ public void write(final TestResultContainer testResultContainer) { } public void write(final String source, final InputStream attachment) { - //not implemented + try { + final byte[] bytes = IOUtils.toByteArray(attachment); + attachments.put(source, bytes); + } catch (IOException e) { + throw new RuntimeException("Could not read attachment content " + source, e); + } } public List getTestResults() { @@ -35,4 +46,8 @@ public List getTestResults() { public List getTestContainers() { return testContainers; } + + public Map getAttachments() { + return attachments; + } } diff --git a/allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java b/allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java index 5f85e27d4..1554d6d70 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/AllureLifecycle.java @@ -1,6 +1,7 @@ package io.qameta.allure; import io.qameta.allure.internal.AllureStorage; +import io.qameta.allure.internal.AllureThreadContext; import io.qameta.allure.listener.ContainerLifecycleListener; import io.qameta.allure.listener.FixtureLifecycleListener; import io.qameta.allure.listener.LifecycleNotifier; @@ -13,6 +14,7 @@ import io.qameta.allure.model.TestResult; import io.qameta.allure.model.TestResultContainer; import io.qameta.allure.model.WithAttachments; +import io.qameta.allure.model.WithSteps; import io.qameta.allure.util.PropertiesUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,7 +34,7 @@ /** * The class contains Allure context and methods to change it. */ -@SuppressWarnings("PMD.TooManyMethods") +@SuppressWarnings({"PMD.TooManyMethods", "unused"}) public class AllureLifecycle { private static final Logger LOGGER = LoggerFactory.getLogger(AllureLifecycle.class); @@ -41,14 +43,25 @@ public class AllureLifecycle { private final AllureStorage storage; + private final AllureThreadContext threadContext; + private final LifecycleNotifier notifier; + /** + * Creates a new lifecycle with default results writer. Shortcut + * for {@link #AllureLifecycle(AllureResultsWriter)} + */ public AllureLifecycle() { this(getDefaultWriter()); } + /** + * Creates a new lifecycle instance with specified {@link AllureResultsWriter}. + * + * @param writer the results writer. + */ public AllureLifecycle(final AllureResultsWriter writer) { - final ClassLoader classLoader = getClass().getClassLoader(); + final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); this.notifier = new LifecycleNotifier( load(ContainerLifecycleListener.class, classLoader), load(TestLifecycleListener.class, classLoader), @@ -57,97 +70,231 @@ public AllureLifecycle(final AllureResultsWriter writer) { ); this.writer = writer; this.storage = new AllureStorage(); - } - - public void startTestContainer(final String parentUuid, final TestResultContainer container) { - updateTestContainer(parentUuid, found -> found.getChildren().add(container.getUuid())); + this.threadContext = new AllureThreadContext(); + } + + /** + * Starts test container with specified parent container. + * + * @param containerUuid the uuid of parent container. + * @param container the container. + */ + public void startTestContainer(final String containerUuid, final TestResultContainer container) { + storage.getContainer(containerUuid).ifPresent(parent -> { + synchronized (storage) { + parent.getChildren().add(container.getUuid()); + } + }); startTestContainer(container); } + /** + * Starts test container. + * + * @param container the container. + */ public void startTestContainer(final TestResultContainer container) { notifier.beforeContainerStart(container); container.setStart(System.currentTimeMillis()); - storage.addContainer(container); + storage.put(container.getUuid(), container); notifier.afterContainerStart(container); } + /** + * Updates test container. + * + * @param uuid the uuid of container. + * @param update the update function. + */ public void updateTestContainer(final String uuid, final Consumer update) { - storage.getContainer(uuid).ifPresent(container -> { - notifier.beforeContainerUpdate(container); - update.accept(container); - notifier.afterContainerUpdate(container); - }); - } - + final Optional found = storage.getContainer(uuid); + if (!found.isPresent()) { + LOGGER.error("Could not update test container: container with uuid {} not found", uuid); + return; + } + final TestResultContainer container = found.get(); + notifier.beforeContainerUpdate(container); + update.accept(container); + notifier.afterContainerUpdate(container); + } + + /** + * Stops test container by given uuid. + * + * @param uuid the uuid of container. + */ public void stopTestContainer(final String uuid) { - storage.getContainer(uuid).ifPresent(container -> { - notifier.beforeContainerStop(container); - container.setStop(System.currentTimeMillis()); - notifier.afterContainerUpdate(container); - }); - } - + final Optional found = storage.getContainer(uuid); + if (!found.isPresent()) { + LOGGER.error("Could not stop test container: container with uuid {} not found", uuid); + return; + } + final TestResultContainer container = found.get(); + notifier.beforeContainerStop(container); + container.setStop(System.currentTimeMillis()); + notifier.afterContainerUpdate(container); + } + + /** + * Writes test container with given uuid. + * + * @param uuid the uuid of container. + */ public void writeTestContainer(final String uuid) { - storage.removeContainer(uuid).ifPresent(container -> { - notifier.beforeContainerWrite(container); - writer.write(container); - notifier.afterContainerWrite(container); + final Optional found = storage.removeContainer(uuid); + if (!found.isPresent()) { + LOGGER.error("Could not write test container: container with uuid {} not found", uuid); + return; + } + final TestResultContainer container = found.get(); + notifier.beforeContainerWrite(container); + writer.write(container); + notifier.afterContainerWrite(container); + } + + /** + * Start a new prepare fixture with given parent. + * + * @param containerUuid the uuid of parent container. + * @param uuid the fixture uuid. + * @param result the fixture. + */ + public void startPrepareFixture(final String containerUuid, final String uuid, final FixtureResult result) { + storage.getContainer(containerUuid).ifPresent(container -> { + synchronized (storage) { + container.getBefores().add(result); + } }); - } - - public void startPrepareFixture(final String parentUuid, final String uuid, final FixtureResult result) { notifier.beforeFixtureStart(result); - updateTestContainer(parentUuid, container -> container.getBefores().add(result)); startFixture(uuid, result); notifier.afterFixtureStart(result); } - public void startTearDownFixture(final String parentUuid, final String uuid, final FixtureResult result) { + /** + * Start a new tear down fixture with given parent. + * + * @param containerUuid the uuid of parent container. + * @param uuid the fixture uuid. + * @param result the fixture. + */ + public void startTearDownFixture(final String containerUuid, final String uuid, final FixtureResult result) { + storage.getContainer(containerUuid).ifPresent(container -> { + synchronized (storage) { + container.getAfters().add(result); + } + }); + notifier.beforeFixtureStart(result); - updateTestContainer(parentUuid, container -> container.getAfters().add(result)); startFixture(uuid, result); notifier.afterFixtureStart(result); } + /** + * Start a new fixture with given uuid. + * + * @param uuid the uuid of fixture. + * @param result the test fixture. + */ private void startFixture(final String uuid, final FixtureResult result) { - storage.addFixture(uuid, result); + storage.put(uuid, result); result.setStage(Stage.RUNNING); result.setStart(System.currentTimeMillis()); - storage.clearStepContext(); - storage.startStep(uuid); + threadContext.clear(); + threadContext.start(uuid); } + /** + * Updates current running fixture. Shortcut for {@link #updateFixture(String, Consumer)}. + * + * @param update the update function. + */ public void updateFixture(final Consumer update) { - updateFixture(storage.getRootStep(), update); - } - + final Optional root = threadContext.getRoot(); + if (!root.isPresent()) { + LOGGER.error("Could not update test fixture: no test fixture running"); + return; + } + final String uuid = root.get(); + updateFixture(uuid, update); + } + + /** + * Updates fixture by given uuid. + * + * @param uuid the uuid of fixture. + * @param update the update function. + */ public void updateFixture(final String uuid, final Consumer update) { - storage.getFixture(uuid).ifPresent(fixture -> { - notifier.beforeFixtureUpdate(fixture); - update.accept(fixture); - notifier.afterFixtureUpdate(fixture); - }); - } - + final Optional found = storage.getFixture(uuid); + if (!found.isPresent()) { + LOGGER.error("Could not update test fixture: test fixture with uuid {} not found", uuid); + return; + } + final FixtureResult fixture = found.get(); + + notifier.beforeFixtureUpdate(fixture); + update.accept(fixture); + notifier.afterFixtureUpdate(fixture); + } + + /** + * Stops fixture by given uuid. + * + * @param uuid the uuid of fixture. + */ public void stopFixture(final String uuid) { - storage.removeFixture(uuid).ifPresent(fixture -> { - notifier.beforeFixtureStop(fixture); - storage.clearStepContext(); - fixture.setStage(Stage.FINISHED); - fixture.setStop(System.currentTimeMillis()); - notifier.afterFixtureStop(fixture); - }); - } - + final Optional found = storage.removeFixture(uuid); + if (!found.isPresent()) { + LOGGER.error("Could not stop test fixture: test fixture with uuid {} not found", uuid); + return; + } + final FixtureResult fixture = found.get(); + + notifier.beforeFixtureStop(fixture); + fixture.setStage(Stage.FINISHED); + fixture.setStop(System.currentTimeMillis()); + threadContext.clear(); + notifier.afterFixtureStop(fixture); + } + + /** + * Returns uuid of current running test case if any. + * + * @return the uuid of current running test case. + */ public Optional getCurrentTestCase() { - return Optional.ofNullable(storage.getRootStep()); - } - - public void scheduleTestCase(final String parentUuid, final TestResult result) { - updateTestContainer(parentUuid, container -> container.getChildren().add(result.getUuid())); + return threadContext.getRoot(); + } + + /** + * Returns uuid of current running test case or step if any. + * + * @return the uuid of current running test case or step. + */ + public Optional getCurrentTestCaseOrStep() { + return threadContext.getCurrent(); + } + + /** + * Schedules test case with given parent. + * + * @param containerUuid the uuid of container. + * @param result the test case to schedule. + */ + public void scheduleTestCase(final String containerUuid, final TestResult result) { + storage.getContainer(containerUuid).ifPresent(container -> { + synchronized (storage) { + container.getChildren().add(result.getUuid()); + } + }); scheduleTestCase(result); } + /** + * Schedule given test case. + * + * @param result the test case to schedule. + */ public void scheduleTestCase(final TestResult result) { notifier.beforeTestSchedule(result); result.setStage(Stage.SCHEDULED); @@ -155,118 +302,288 @@ public void scheduleTestCase(final TestResult result) { notifier.afterTestSchedule(result); } + /** + * Starts test case with given uuid. In order to start test case it should be scheduled at first. + * + * @param uuid the uuid of test case to start. + */ public void startTestCase(final String uuid) { - storage.getTestResult(uuid).ifPresent(testResult -> { - notifier.beforeTestStart(testResult); - testResult - .setStage(Stage.RUNNING) - .setStart(System.currentTimeMillis()); - storage.clearStepContext(); - storage.startStep(uuid); - notifier.afterTestStart(testResult); - }); - } - + threadContext.clear(); + final Optional found = storage.getTestResult(uuid); + if (!found.isPresent()) { + LOGGER.error("Could not start test case: test case with uuid {} is not scheduled", uuid); + return; + } + final TestResult testResult = found.get(); + + notifier.beforeTestStart(testResult); + testResult + .setStage(Stage.RUNNING) + .setStart(System.currentTimeMillis()); + threadContext.start(uuid); + notifier.afterTestStart(testResult); + } + + /** + * Shortcut for {@link #updateTestCase(String, Consumer)} for current running test case uuid. + * + * @param update the update function. + */ public void updateTestCase(final Consumer update) { - final String uuid = storage.getRootStep(); + final Optional root = threadContext.getRoot(); + if (!root.isPresent()) { + LOGGER.error("Could not update test case: no test case running"); + return; + } + + final String uuid = root.get(); updateTestCase(uuid, update); } + /** + * Updates test case by given uuid. + * + * @param uuid the uuid of test case to update. + * @param update the update function. + */ public void updateTestCase(final String uuid, final Consumer update) { - storage.getTestResult(uuid).ifPresent(testResult -> { - notifier.beforeTestUpdate(testResult); - update.accept(testResult); - notifier.afterTestUpdate(testResult); - }); - } - + final Optional found = storage.getTestResult(uuid); + if (!found.isPresent()) { + LOGGER.error("Could not update test case: test case with uuid {} not found", uuid); + return; + } + final TestResult testResult = found.get(); + + notifier.beforeTestUpdate(testResult); + update.accept(testResult); + notifier.afterTestUpdate(testResult); + } + + /** + * Stops test case by given uuid. Test case marked as {@link Stage#FINISHED} and also + * stop timestamp is calculated. Result would be stored in memory until + * {@link #writeTestCase(String)} method is called. Also stopped test case could be + * updated by {@link #updateTestCase(String, Consumer)} method. + * + * @param uuid the uuid of test case to stop. + */ public void stopTestCase(final String uuid) { - storage.getTestResult(uuid).ifPresent(testResult -> { - notifier.beforeTestStop(testResult); - testResult - .setStage(Stage.FINISHED) - .setStop(System.currentTimeMillis()); - storage.clearStepContext(); - notifier.afterTestStop(testResult); - }); - } - + final Optional found = storage.getTestResult(uuid); + if (!found.isPresent()) { + LOGGER.error("Could not stop test case: test case with uuid {} not found", uuid); + return; + } + final TestResult testResult = found.get(); + + notifier.beforeTestStop(testResult); + testResult + .setStage(Stage.FINISHED) + .setStop(System.currentTimeMillis()); + threadContext.clear(); + notifier.afterTestStop(testResult); + } + + /** + * Writes test case with given uuid using configured {@link AllureResultsWriter}. + * + * @param uuid the uuid of test case to write. + */ public void writeTestCase(final String uuid) { - storage.removeTestResult(uuid).ifPresent(testResult -> { - notifier.beforeTestWrite(testResult); - writer.write(testResult); - notifier.afterTestWrite(testResult); - }); - } - + final Optional found = storage.removeTestResult(uuid); + if (!found.isPresent()) { + LOGGER.error("Could not write test case: test case with uuid {} not found", uuid); + return; + } + + final TestResult testResult = found.get(); + notifier.beforeTestWrite(testResult); + writer.write(testResult); + notifier.afterTestWrite(testResult); + } + + /** + * Start a new step as child step of current running test case or step. Shortcut + * for {@link #startStep(String, String, StepResult)}. + * + * @param uuid the uuid of step. + * @param result the step. + */ public void startStep(final String uuid, final StepResult result) { - storage.getCurrentStep().ifPresent(parentUuid -> startStep(parentUuid, uuid, result)); - } - + final Optional current = threadContext.getCurrent(); + if (!current.isPresent()) { + LOGGER.error("Could not start step: no test case running"); + return; + } + final String parentUuid = current.get(); + startStep(parentUuid, uuid, result); + } + + /** + * Start a new step as child of specified parent. + * + * @param parentUuid the uuid of parent test case or step. + * @param uuid the uuid of step. + * @param result the step. + */ public void startStep(final String parentUuid, final String uuid, final StepResult result) { notifier.beforeStepStart(result); + result.setStage(Stage.RUNNING); result.setStart(System.currentTimeMillis()); - storage.startStep(uuid); - storage.addStep(parentUuid, uuid, result); + + threadContext.start(uuid); + + storage.put(uuid, result); + storage.get(parentUuid, WithSteps.class).ifPresent(parentStep -> { + synchronized (storage) { + parentStep.getSteps().add(result); + } + }); + notifier.afterStepStart(result); } + /** + * Updates current step. Shortcut for {@link #updateStep(String, Consumer)}. + * + * @param update the update function. + */ public void updateStep(final Consumer update) { - storage.getCurrentStep().ifPresent(uuid -> updateStep(uuid, update)); - } - + final Optional current = threadContext.getCurrent(); + if (!current.isPresent()) { + LOGGER.error("Could not update step: no step running"); + return; + } + final String uuid = current.get(); + updateStep(uuid, update); + } + + /** + * Updates step by specified uuid. + * + * @param uuid the uuid of step. + * @param update the update function. + */ public void updateStep(final String uuid, final Consumer update) { - storage.getStep(uuid).ifPresent(step -> { - notifier.beforeStepUpdate(step); - update.accept(step); - notifier.afterStepUpdate(step); - }); - } + final Optional found = storage.getStep(uuid); + if (!found.isPresent()) { + LOGGER.error("Could not update step: step with uuid {} not found", uuid); + return; + } - public void stopStep() { - storage.getCurrentStep().ifPresent(this::stopStep); - } + final StepResult step = found.get(); - public void stopStep(final String uuid) { - storage.removeStep(uuid).ifPresent(step -> { - notifier.beforeStepStop(step); - step.setStage(Stage.FINISHED); - step.setStop(System.currentTimeMillis()); - storage.stopStep(); - notifier.afterStepStop(step); - }); + notifier.beforeStepUpdate(step); + update.accept(step); + notifier.afterStepUpdate(step); } + /** + * Stops current running step. Shortcut for {@link #stopStep(String)}. + */ + public void stopStep() { + final String root = threadContext.getRoot().orElse(null); + final Optional current = threadContext.getCurrent() + .filter(uuid -> !Objects.equals(uuid, root)); + if (!current.isPresent()) { + LOGGER.error("Could not stop step: no step running"); + return; + } + final String uuid = current.get(); + stopStep(uuid); + } + + /** + * Stops step by given uuid. + * + * @param uuid the uuid of step to stop. + */ + public void stopStep(final String uuid) { + final Optional found = storage.removeStep(uuid); + if (!found.isPresent()) { + LOGGER.error("Could not stop step: step with uuid {} not found", uuid); + return; + } + + final StepResult step = found.get(); + + notifier.beforeStepStop(step); + step.setStage(Stage.FINISHED); + step.setStop(System.currentTimeMillis()); + threadContext.stop(); + notifier.afterStepStop(step); + } + + /** + * Adds attachment into current test or step if any exists. Shortcut + * for {@link #addAttachment(String, String, String, InputStream)} + * + * @param name the name of attachment + * @param type the content type of attachment + * @param fileExtension the attachment file extension + * @param body attachment content + */ public void addAttachment(final String name, final String type, final String fileExtension, final byte[] body) { addAttachment(name, type, fileExtension, new ByteArrayInputStream(body)); } + /** + * Adds attachment to current running test or step. + * + * @param name the name of attachment + * @param type the content type of attachment + * @param fileExtension the attachment file extension + * @param stream attachment content + */ public void addAttachment(final String name, final String type, final String fileExtension, final InputStream stream) { writeAttachment(prepareAttachment(name, type, fileExtension), stream); } + /** + * Adds attachment to current running test or step, and returns source. In order + * to store attachment content use {@link #writeAttachment(String, InputStream)} method. + * + * @param name the name of attachment + * @param type the content type of attachment + * @param fileExtension the attachment file extension + * @return the source of added attachment + */ @SuppressWarnings({"PMD.NullAssignment", "PMD.UseObjectForClearerAPI"}) public String prepareAttachment(final String name, final String type, final String fileExtension) { - final Optional currentStep = storage.getCurrentStep(); - currentStep.ifPresent(uuid -> LOGGER.debug("Adding attachment to item with uuid {}", uuid)); final String extension = Optional.ofNullable(fileExtension) .filter(ext -> !ext.isEmpty()) .map(ext -> ext.charAt(0) == '.' ? ext : "." + ext) .orElse(""); final String source = UUID.randomUUID().toString() + ATTACHMENT_FILE_SUFFIX + extension; + + final Optional current = threadContext.getCurrent(); + if (!current.isPresent()) { + LOGGER.error("Could not add attachment: no test is running"); + //backward compatibility: return source even if no attachment is going to be written. + return source; + } final Attachment attachment = new Attachment() .setName(isEmpty(name) ? null : name) .setType(isEmpty(type) ? null : type) .setSource(source); - currentStep.flatMap(uuid -> storage.get(uuid, WithAttachments.class)) - .ifPresent(withAttachments -> withAttachments.getAttachments().add(attachment)); + final String uuid = current.get(); + storage.get(uuid, WithAttachments.class).ifPresent(withAttachments -> { + synchronized (storage) { + withAttachments.getAttachments().add(attachment); + } + }); return attachment.getSource(); } + /** + * Writes attachment with specified source. + * + * @param attachmentSource the source of attachment. + * @param stream the attachment content. + */ public void writeAttachment(final String attachmentSource, final InputStream stream) { writer.write(attachmentSource, stream); } diff --git a/allure-java-commons/src/main/java/io/qameta/allure/internal/AllureStorage.java b/allure-java-commons/src/main/java/io/qameta/allure/internal/AllureStorage.java index 8edf370d2..a0d2e280a 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/internal/AllureStorage.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/internal/AllureStorage.java @@ -4,13 +4,13 @@ import io.qameta.allure.model.StepResult; import io.qameta.allure.model.TestResult; import io.qameta.allure.model.TestResultContainer; -import io.qameta.allure.model.WithSteps; -import java.util.LinkedList; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; /** * Internal Allure data storage. @@ -21,50 +21,12 @@ public class AllureStorage { private final Map storage = new ConcurrentHashMap<>(); - @SuppressWarnings("checkstyle:LineLength") - private final ThreadLocal> currentStepContext = new InheritableThreadLocal>() { - @Override - public LinkedList initialValue() { - return new LinkedList<>(); - } - }; - - @SuppressWarnings("PMD.NullAssignment") - public Optional getCurrentStep() { - final LinkedList uids = currentStepContext.get(); - return uids.isEmpty() - ? Optional.empty() - : Optional.of(uids.getFirst()); - } - - @SuppressWarnings("PMD.NullAssignment") - public String getRootStep() { - final LinkedList uids = currentStepContext.get(); - return uids.isEmpty() - ? null - : uids.getLast(); - } - - public void startStep(final String uuid) { - currentStepContext.get().push(uuid); - } - - public void stopStep() { - currentStepContext.get().pop(); - } - - public void clearStepContext() { - currentStepContext.remove(); - } + private final ReadWriteLock lock = new ReentrantReadWriteLock(); public Optional getContainer(final String uuid) { return get(uuid, TestResultContainer.class); } - public void addContainer(final TestResultContainer container) { - put(container.getUuid(), container); - } - public Optional removeContainer(final String uuid) { return remove(uuid, TestResultContainer.class); } @@ -85,10 +47,6 @@ public Optional getFixture(final String uuid) { return get(uuid, FixtureResult.class); } - public void addFixture(final String uuid, final FixtureResult fixtureResult) { - put(uuid, fixtureResult); - } - public Optional removeFixture(final String uuid) { return remove(uuid, FixtureResult.class); } @@ -97,37 +55,43 @@ public Optional getStep(final String uuid) { return get(uuid, StepResult.class); } - public void addStep(final String parentUuid, final String uuid, final StepResult step) { - put(uuid, step); - get(parentUuid, WithSteps.class).ifPresent(parentStep -> parentStep.getSteps().add(step)); - } - public Optional removeStep(final String uuid) { return remove(uuid, StepResult.class); } - public T put(final String uuid, final T item) { - Objects.requireNonNull(uuid, "Can't put item to storage: uuid can't be null"); - storage.put(uuid, item); - return item; - } - public Optional get(final String uuid, final Class clazz) { - Objects.requireNonNull(uuid, "Can't get item from storage: uuid can't be null"); - return Optional.ofNullable(storage.get(uuid)) - .map(item -> cast(item, clazz)); + lock.readLock().lock(); + try { + Objects.requireNonNull(uuid, "Can't get item from storage: uuid can't be null"); + return Optional.ofNullable(storage.get(uuid)) + .filter(clazz::isInstance) + .map(clazz::cast); + } finally { + lock.readLock().unlock(); + } } - public Optional remove(final String uuid, final Class clazz) { - Objects.requireNonNull(uuid, "Can't remove item from storage: uuid can't be null"); - return Optional.ofNullable(storage.remove(uuid)) - .map(item -> cast(item, clazz)); + public T put(final String uuid, final T item) { + lock.writeLock().lock(); + try { + Objects.requireNonNull(uuid, "Can't put item to storage: uuid can't be null"); + storage.put(uuid, item); + return item; + } finally { + lock.writeLock().unlock(); + } } - public T cast(final Object obj, final Class clazz) { - if (clazz.isInstance(obj)) { - return clazz.cast(obj); + public Optional remove(final String uuid, final Class clazz) { + lock.writeLock().lock(); + try { + Objects.requireNonNull(uuid, "Can't remove item from storage: uuid can't be null"); + return Optional.ofNullable(storage.remove(uuid)) + .filter(clazz::isInstance) + .map(clazz::cast); + } finally { + lock.writeLock().unlock(); } - throw new IllegalStateException("Can not cast " + obj + " to " + clazz); } + } diff --git a/allure-java-commons/src/main/java/io/qameta/allure/internal/AllureThreadContext.java b/allure-java-commons/src/main/java/io/qameta/allure/internal/AllureThreadContext.java new file mode 100644 index 000000000..a2d6cf9b4 --- /dev/null +++ b/allure-java-commons/src/main/java/io/qameta/allure/internal/AllureThreadContext.java @@ -0,0 +1,80 @@ +package io.qameta.allure.internal; + +import java.util.LinkedList; +import java.util.Objects; +import java.util.Optional; + +/** + * Storage that stores information about not finished tests and steps. + * + * @author charlie (Dmitry Baev). + */ +public class AllureThreadContext { + + private final Context context = new Context(); + + /** + * Returns last (most recent) uuid. + */ + public Optional getCurrent() { + final LinkedList uuids = context.get(); + return uuids.isEmpty() + ? Optional.empty() + : Optional.of(uuids.getFirst()); + } + + /** + * Returns first (oldest) uuid. + */ + public Optional getRoot() { + final LinkedList uuids = context.get(); + return uuids.isEmpty() + ? Optional.empty() + : Optional.of(uuids.getLast()); + } + + /** + * Adds new uuid. + */ + public void start(final String uuid) { + Objects.requireNonNull(uuid, "step uuid"); + context.get().push(uuid); + } + + /** + * Removes latest added uuid. Ignores empty context. + * + * @return removed uuid. + */ + public Optional stop() { + final LinkedList uuids = context.get(); + if (!uuids.isEmpty()) { + return Optional.of(uuids.pop()); + } + return Optional.empty(); + } + + /** + * Removes all the data stored for current thread. + */ + public void clear() { + context.remove(); + } + + /** + * Thread local context that stores information about not finished tests and steps. + */ + private static class Context extends InheritableThreadLocal> { + + @Override + public LinkedList initialValue() { + return new LinkedList<>(); + } + + @Override + protected LinkedList childValue(final LinkedList parentStepContext) { + return new LinkedList<>(parentStepContext); + } + + } +} diff --git a/allure-java-commons/src/main/java/io/qameta/allure/listener/ContainerLifecycleListener.java b/allure-java-commons/src/main/java/io/qameta/allure/listener/ContainerLifecycleListener.java index 2c8ac3983..d40688b23 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/listener/ContainerLifecycleListener.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/listener/ContainerLifecycleListener.java @@ -7,7 +7,7 @@ * * @since 2.0 */ -public interface ContainerLifecycleListener { +public interface ContainerLifecycleListener extends LifecycleListener { default void beforeContainerStart(TestResultContainer container) { //do nothing diff --git a/allure-java-commons/src/main/java/io/qameta/allure/listener/FixtureLifecycleListener.java b/allure-java-commons/src/main/java/io/qameta/allure/listener/FixtureLifecycleListener.java index f8444378e..3e8c2226f 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/listener/FixtureLifecycleListener.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/listener/FixtureLifecycleListener.java @@ -7,7 +7,7 @@ * * @since 2.0 */ -public interface FixtureLifecycleListener { +public interface FixtureLifecycleListener extends LifecycleListener { default void beforeFixtureStart(FixtureResult result) { //do nothing diff --git a/allure-java-commons/src/main/java/io/qameta/allure/listener/LifecycleListener.java b/allure-java-commons/src/main/java/io/qameta/allure/listener/LifecycleListener.java new file mode 100644 index 000000000..e944c8879 --- /dev/null +++ b/allure-java-commons/src/main/java/io/qameta/allure/listener/LifecycleListener.java @@ -0,0 +1,9 @@ +package io.qameta.allure.listener; + +/** + * Marker interface for lifecycle listeners. + * + * @author charlie (Dmitry Baev). + */ +public interface LifecycleListener { +} diff --git a/allure-java-commons/src/main/java/io/qameta/allure/listener/LifecycleNotifier.java b/allure-java-commons/src/main/java/io/qameta/allure/listener/LifecycleNotifier.java index 8ef01592a..0a57110bd 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/listener/LifecycleNotifier.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/listener/LifecycleNotifier.java @@ -4,8 +4,11 @@ import io.qameta.allure.model.StepResult; import io.qameta.allure.model.TestResult; import io.qameta.allure.model.TestResultContainer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.List; +import java.util.function.BiConsumer; /** * @since 2.0 @@ -14,6 +17,8 @@ public class LifecycleNotifier implements ContainerLifecycleListener, TestLifecycleListener, FixtureLifecycleListener, StepLifecycleListener { + private static final Logger LOGGER = LoggerFactory.getLogger(LifecycleNotifier.class); + private final List containerListeners; private final List testListeners; @@ -35,151 +40,164 @@ public LifecycleNotifier(final List containerListene @Override public void beforeTestSchedule(final TestResult result) { - testListeners.forEach(listener -> listener.beforeTestSchedule(result)); + runSafely(testListeners, TestLifecycleListener::beforeTestSchedule, result); } @Override public void afterTestSchedule(final TestResult result) { - testListeners.forEach(listener -> listener.afterTestSchedule(result)); + runSafely(testListeners, TestLifecycleListener::afterTestSchedule, result); } @Override public void beforeTestUpdate(final TestResult result) { - testListeners.forEach(listener -> listener.beforeTestUpdate(result)); + runSafely(testListeners, TestLifecycleListener::beforeTestUpdate, result); } @Override public void afterTestUpdate(final TestResult result) { - testListeners.forEach(listener -> listener.afterTestUpdate(result)); + runSafely(testListeners, TestLifecycleListener::afterTestUpdate, result); } @Override public void beforeTestStart(final TestResult result) { - testListeners.forEach(listener -> listener.beforeTestStart(result)); + runSafely(testListeners, TestLifecycleListener::beforeTestStart, result); } @Override public void afterTestStart(final TestResult result) { - testListeners.forEach(listener -> listener.afterTestStart(result)); + runSafely(testListeners, TestLifecycleListener::afterTestStart, result); } @Override public void beforeTestStop(final TestResult result) { - testListeners.forEach(listener -> listener.beforeTestStop(result)); + runSafely(testListeners, TestLifecycleListener::beforeTestStop, result); } @Override public void afterTestStop(final TestResult result) { - testListeners.forEach(listener -> listener.afterTestStop(result)); + runSafely(testListeners, TestLifecycleListener::afterTestStop, result); } @Override public void beforeTestWrite(final TestResult result) { - testListeners.forEach(listener -> listener.beforeTestWrite(result)); + runSafely(testListeners, TestLifecycleListener::beforeTestWrite, result); } @Override public void afterTestWrite(final TestResult result) { - testListeners.forEach(listener -> listener.afterTestWrite(result)); + runSafely(testListeners, TestLifecycleListener::afterTestWrite, result); } @Override public void beforeContainerStart(final TestResultContainer container) { - containerListeners.forEach(listener -> listener.beforeContainerStart(container)); + runSafely(containerListeners, ContainerLifecycleListener::beforeContainerStart, container); } @Override public void afterContainerStart(final TestResultContainer container) { - containerListeners.forEach(listener -> listener.afterContainerStart(container)); + runSafely(containerListeners, ContainerLifecycleListener::afterContainerStart, container); } @Override public void beforeContainerUpdate(final TestResultContainer container) { - containerListeners.forEach(listener -> listener.beforeContainerUpdate(container)); + runSafely(containerListeners, ContainerLifecycleListener::beforeContainerUpdate, container); } @Override public void afterContainerUpdate(final TestResultContainer container) { - containerListeners.forEach(listener -> listener.afterContainerUpdate(container)); + runSafely(containerListeners, ContainerLifecycleListener::afterContainerUpdate, container); } @Override public void beforeContainerStop(final TestResultContainer container) { - containerListeners.forEach(listener -> listener.beforeContainerStop(container)); + runSafely(containerListeners, ContainerLifecycleListener::beforeContainerStop, container); } @Override public void afterContainerStop(final TestResultContainer container) { - containerListeners.forEach(listener -> listener.afterContainerStop(container)); + runSafely(containerListeners, ContainerLifecycleListener::afterContainerStop, container); } @Override public void beforeContainerWrite(final TestResultContainer container) { - containerListeners.forEach(listener -> listener.beforeContainerWrite(container)); + runSafely(containerListeners, ContainerLifecycleListener::beforeContainerWrite, container); } @Override public void afterContainerWrite(final TestResultContainer container) { - containerListeners.forEach(listener -> listener.afterContainerWrite(container)); + runSafely(containerListeners, ContainerLifecycleListener::afterContainerWrite, container); } @Override public void beforeFixtureStart(final FixtureResult result) { - fixtureListeners.forEach(listener -> listener.beforeFixtureStart(result)); + runSafely(fixtureListeners, FixtureLifecycleListener::beforeFixtureStart, result); } @Override public void afterFixtureStart(final FixtureResult result) { - fixtureListeners.forEach(listener -> listener.afterFixtureStart(result)); + runSafely(fixtureListeners, FixtureLifecycleListener::afterFixtureStart, result); } @Override public void beforeFixtureUpdate(final FixtureResult result) { - fixtureListeners.forEach(listener -> listener.beforeFixtureUpdate(result)); + runSafely(fixtureListeners, FixtureLifecycleListener::beforeFixtureUpdate, result); } @Override public void afterFixtureUpdate(final FixtureResult result) { - fixtureListeners.forEach(listener -> listener.afterFixtureUpdate(result)); + runSafely(fixtureListeners, FixtureLifecycleListener::afterFixtureUpdate, result); } @Override public void beforeFixtureStop(final FixtureResult result) { - fixtureListeners.forEach(listener -> listener.beforeFixtureStop(result)); + runSafely(fixtureListeners, FixtureLifecycleListener::beforeFixtureStop, result); } @Override public void afterFixtureStop(final FixtureResult result) { - fixtureListeners.forEach(listener -> listener.afterFixtureStop(result)); + runSafely(fixtureListeners, FixtureLifecycleListener::afterFixtureStop, result); } @Override public void beforeStepStart(final StepResult result) { - stepListeners.forEach(listener -> listener.beforeStepStart(result)); + runSafely(stepListeners, StepLifecycleListener::beforeStepStart, result); } @Override public void afterStepStart(final StepResult result) { - stepListeners.forEach(listener -> listener.afterStepStart(result)); + runSafely(stepListeners, StepLifecycleListener::afterStepStart, result); } @Override public void beforeStepUpdate(final StepResult result) { - stepListeners.forEach(listener -> listener.beforeStepUpdate(result)); + runSafely(stepListeners, StepLifecycleListener::beforeStepUpdate, result); } @Override public void afterStepUpdate(final StepResult result) { - stepListeners.forEach(listener -> listener.afterStepUpdate(result)); + runSafely(stepListeners, StepLifecycleListener::afterStepUpdate, result); } @Override public void beforeStepStop(final StepResult result) { - stepListeners.forEach(listener -> listener.beforeStepStop(result)); + runSafely(stepListeners, StepLifecycleListener::beforeStepStop, result); } @Override public void afterStepStop(final StepResult result) { - stepListeners.forEach(listener -> listener.afterStepStop(result)); + runSafely(stepListeners, StepLifecycleListener::afterStepStop, result); + } + + protected void runSafely(final List listeners, + final BiConsumer method, + final S object) { + listeners.forEach(listener -> { + try { + method.accept(listener, object); + } catch (Exception e) { + LOGGER.error("Could not invoke listener method", e); + } + }); } + } diff --git a/allure-java-commons/src/main/java/io/qameta/allure/listener/StepLifecycleListener.java b/allure-java-commons/src/main/java/io/qameta/allure/listener/StepLifecycleListener.java index fadb5d0d3..046ec2dd4 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/listener/StepLifecycleListener.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/listener/StepLifecycleListener.java @@ -7,7 +7,7 @@ * * @since 2.0 */ -public interface StepLifecycleListener { +public interface StepLifecycleListener extends LifecycleListener { default void beforeStepStart(StepResult result) { //do nothing diff --git a/allure-java-commons/src/main/java/io/qameta/allure/listener/TestLifecycleListener.java b/allure-java-commons/src/main/java/io/qameta/allure/listener/TestLifecycleListener.java index 64421bccb..f51e1e78f 100644 --- a/allure-java-commons/src/main/java/io/qameta/allure/listener/TestLifecycleListener.java +++ b/allure-java-commons/src/main/java/io/qameta/allure/listener/TestLifecycleListener.java @@ -7,7 +7,7 @@ * * @since 2.0 */ -public interface TestLifecycleListener { +public interface TestLifecycleListener extends LifecycleListener { default void beforeTestSchedule(TestResult result) { //do nothing diff --git a/allure-java-commons/src/test/java/io/qameta/allure/AllureLifecycleTest.java b/allure-java-commons/src/test/java/io/qameta/allure/AllureLifecycleTest.java index 8458f1bdf..d0dd5a7df 100644 --- a/allure-java-commons/src/test/java/io/qameta/allure/AllureLifecycleTest.java +++ b/allure-java-commons/src/test/java/io/qameta/allure/AllureLifecycleTest.java @@ -1,19 +1,42 @@ package io.qameta.allure; +import io.qameta.allure.model.Attachment; import io.qameta.allure.model.FixtureResult; import io.qameta.allure.model.StepResult; import io.qameta.allure.model.TestResult; import io.qameta.allure.model.TestResultContainer; +import io.qameta.allure.test.AllureResultsWriterStub; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.stream.Collectors; import static io.github.benas.randombeans.api.EnhancedRandom.random; +import static io.qameta.allure.Allure.addStreamAttachmentAsync; +import static io.qameta.allure.Allure.setLifecycle; +import static java.util.concurrent.CompletableFuture.allOf; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; import static org.mockito.ArgumentCaptor.forClass; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -256,6 +279,157 @@ void shouldCreateTestFixture() { .containsExactly(firstStepName, secondStepName); } + @SuppressWarnings("OptionalGetWithoutIsPresent") + @Test + void shouldAttachAsync() { + final List> features = new CopyOnWriteArrayList<>(); + final AllureResultsWriterStub writer = new AllureResultsWriterStub(); + + final AllureLifecycle lifecycle = new AllureLifecycle(writer); + setLifecycle(lifecycle); + + final String uuid = UUID.randomUUID().toString(); + final TestResult result = new TestResult().setUuid(uuid); + + lifecycle.scheduleTestCase(result); + lifecycle.startTestCase(uuid); + + final String attachment1Content = random(String.class); + final String attachment2Content = random(String.class); + + final String attachment1Name = random(String.class); + final String attachment2Name = random(String.class); + + features.add(addStreamAttachmentAsync( + attachment1Name, "video/mp4", getStreamWithTimeout(2, attachment1Content))); + features.add(addStreamAttachmentAsync( + attachment2Name, "text/plain", getStreamWithTimeout(1, attachment2Content))); + + lifecycle.stopTestCase(uuid); + lifecycle.writeTestCase(uuid); + + allOf(features.toArray(new CompletableFuture[0])).join(); + + final List attachments = writer.getTestResults().stream() + .map(TestResult::getAttachments) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + + assertThat(attachments) + .extracting(io.qameta.allure.model.Attachment::getName, io.qameta.allure.model.Attachment::getType) + .containsExactly( + tuple(attachment1Name, "video/mp4"), + tuple(attachment2Name, "text/plain") + ); + + final String[] sources = attachments.stream() + .map(io.qameta.allure.model.Attachment::getSource) + .toArray(String[]::new); + + + final Map attachmentFiles = writer.getAttachments(); + assertThat(attachmentFiles) + .containsKeys(sources); + + final io.qameta.allure.model.Attachment attachment1 = attachments.stream() + .filter(attachment -> Objects.equals(attachment.getName(), attachment1Name)) + .findAny() + .get(); + + final byte[] actual1 = attachmentFiles.get(attachment1.getSource()); + + assertThat(new String(actual1, StandardCharsets.UTF_8)) + .isEqualTo(attachment1Content); + + final Attachment attachment2 = attachments.stream() + .filter(attachment -> Objects.equals(attachment.getName(), attachment2Name)) + .findAny() + .get(); + + final byte[] actual2 = attachmentFiles.get(attachment2.getSource()); + + assertThat(new String(actual2, StandardCharsets.UTF_8)) + .isEqualTo(attachment2Content); + + } + + private Supplier getStreamWithTimeout(final long delay, final String content) { + return () -> { + try { + TimeUnit.SECONDS.sleep(delay); + } catch (InterruptedException e) { + throw new RuntimeException("Thread interrupted", e); + } + return new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)); + }; + } + + @Test + void supportForConcurrentUseOfChildThreads() throws Exception { + final String uuid = random(String.class); + final String name = random(String.class); + + final int threads = 20; + final int stepsCount = 1000; + + final TestResult result = new TestResult().setUuid(uuid).setName(name); + lifecycle.scheduleTestCase(result); + lifecycle.startTestCase(uuid); + lifecycle.startTestCase(uuid); + + final ExecutorService service = Executors.newFixedThreadPool(threads); + + final List> tasks = new ArrayList<>(); + for (int i = 0; i < threads; i++) { + tasks.add(new StepCall(lifecycle, i, stepsCount)); + } + + List> futures = service.invokeAll(tasks); + for (Future future : futures) { + future.get(); + } + + lifecycle.stopTestCase(uuid); + lifecycle.writeTestCase(uuid); + + final ArgumentCaptor captor = forClass(TestResult.class); + verify(writer, times(1)).write(captor.capture()); + + final List steps = captor.getValue().getSteps(); + final int expected = threads * stepsCount; + assertThat(steps) + .hasSize(expected); + + assertThat(steps) + .doesNotContain((StepResult) null); + + final long emptyNameCount = steps.stream() + .map(StepResult::getName) + .filter(Objects::isNull) + .count(); + + assertThat(emptyNameCount) + .describedAs("All steps should have non-empty names") + .isEqualTo(0); + + final boolean anyMatch = steps.stream() + .map(StepResult::getName) + .anyMatch(s -> s.matches("^Step \\d+$")); + + assertThat(anyMatch) + .describedAs("All steps names should start with Step") + .isTrue(); + + final long countDistinct = steps.stream() + .map(StepResult::getName) + .distinct() + .count(); + + assertThat(countDistinct) + .isEqualTo(expected); + + } + private String randomStep(String parentUuid) { final String uuid = random(String.class); final String name = random(String.class); @@ -264,4 +438,32 @@ private String randomStep(String parentUuid) { lifecycle.stopStep(uuid); return name; } + + private static class StepCall implements Callable { + + private final AllureLifecycle lifecycle; + + private final int id; + + private final int stepsCount; + + private StepCall(final AllureLifecycle lifecycle, final int id, final int stepsCount) { + this.lifecycle = lifecycle; + this.id = id; + this.stepsCount = stepsCount; + } + + @Override + public Void call() { + for (int j = 0; j < stepsCount; j++) { + final int stepId = id * stepsCount + j; + final String stepUuid = "step " + stepId; + final String stepName = "Step " + stepId; + final StepResult step = new StepResult().setName(stepName); + lifecycle.startStep(stepUuid, step); + lifecycle.stopStep(stepUuid); + } + return null; + } + } } \ No newline at end of file diff --git a/allure-java-commons/src/test/java/io/qameta/allure/AttachmentsTest.java b/allure-java-commons/src/test/java/io/qameta/allure/AttachmentsTest.java deleted file mode 100644 index 8d6b54d79..000000000 --- a/allure-java-commons/src/test/java/io/qameta/allure/AttachmentsTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package io.qameta.allure; - -import io.qameta.allure.model.Attachment; -import io.qameta.allure.model.TestResult; -import io.qameta.allure.test.AllureResultsWriterStub; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -import java.io.InputStream; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import static io.qameta.allure.Allure.addStreamAttachmentAsync; -import static io.qameta.allure.Allure.setLifecycle; -import static java.util.Arrays.asList; -import static java.util.concurrent.CompletableFuture.allOf; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; - -/** - * @author sskorol (Sergey Korol). - */ -public class AttachmentsTest { - - private static final List> STREAM_FUTURE = new CopyOnWriteArrayList<>(); - - @Test - void shouldAttachAsync() throws Exception { - final AllureResultsWriterStub results = spy(AllureResultsWriterStub.class); - - final ArgumentCaptor sources = ArgumentCaptor.forClass(String.class); - doNothing().when(results).write(sources.capture(), any(InputStream.class)); - - final AllureLifecycle lifecycle = new AllureLifecycle(results); - setLifecycle(lifecycle); - - final String uuid = UUID.randomUUID().toString(); - final TestResult result = new TestResult().setUuid(uuid); - - lifecycle.scheduleTestCase(result); - lifecycle.startTestCase(uuid); - - STREAM_FUTURE.add(addStreamAttachmentAsync( - "Async attachment 1", "video/mp4", getStreamWithTimeout(2))); - STREAM_FUTURE.add(addStreamAttachmentAsync( - "Async attachment 2", "text/plain", getStreamWithTimeout(1))); - - lifecycle.stopTestCase(uuid); - lifecycle.writeTestCase(uuid); - - allOf(STREAM_FUTURE.toArray(new CompletableFuture[0])).join(); - - final List attachments = results - .getTestResults() - .stream() - .flatMap(r -> r.getAttachments().stream()) - .collect(Collectors.toList()); - - assertThat(attachments) - .as("Attachments list") - .hasSize(2); - - assertThat(sources.getAllValues()) - .as("Sources list") - .hasSize(2); - - assertThat(attachments) - .flatExtracting(attachment -> asList(attachment.getName(), attachment.getType(), attachment.getSource())) - .as("Attachments content") - .containsExactly( - "Async attachment 1", "video/mp4", sources.getAllValues().get(0), - "Async attachment 2", "text/plain", sources.getAllValues().get(1)); - } - - private Supplier getStreamWithTimeout(final long sec) throws InterruptedException { - TimeUnit.SECONDS.sleep(sec); - return () -> mock(InputStream.class); - } -} diff --git a/allure-java-commons/src/test/java/io/qameta/allure/LinksTests.java b/allure-java-commons/src/test/java/io/qameta/allure/LinksTests.java deleted file mode 100644 index a5a6e46c9..000000000 --- a/allure-java-commons/src/test/java/io/qameta/allure/LinksTests.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.qameta.allure; - -import io.qameta.allure.model.Link; -import io.qameta.allure.util.ResultsUtils; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import java.util.Objects; -import java.util.stream.Stream; - -import static io.qameta.allure.util.ResultsUtils.getLinkTypePatternPropertyName; -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author charlie (Dmitry Baev). - */ -class LinksTests { - - public static Stream data() { - return Stream.of( - Arguments.of("a", "b", "c", "d", "e", link("a", "c", "d")), - Arguments.of("a", "b", "c", "d", null, link("a", "c", "d")), - Arguments.of("a", "b", null, "d", "invalid-pattern", link("a", "invalid-pattern", "d")), - Arguments.of("a", "b", null, "d", "pattern/{}/some", link("a", "pattern/a/some", "d")), - Arguments.of(null, null, null, "d", "pattern/{}/some", link(null, "pattern//some", "d")), - Arguments.of(null, null, null, null, "pattern/{}/some", link(null, null, null)), - Arguments.of(null, "b", null, "d", "pattern/{}/some/{}/and-more", link("b", "pattern/b/some/b/and-more", "d")), - Arguments.of(null, "b", null, "d", null, link("b", null, "d")) - ); - } - - public void setSystemProperty(final String type, final String sysProp) { - if (Objects.nonNull(type) && Objects.nonNull(sysProp)) { - System.setProperty(getLinkTypePatternPropertyName(type), sysProp); - } - } - - @ParameterizedTest - @MethodSource(value = "data") - public void shouldCreateLink(final String value, - final String name, - final String url, - final String type, - final String sysProp, - final Link expected) throws Exception { - setSystemProperty(type, sysProp); - try { - Link actual = ResultsUtils.createLink(value, name, url, type); - assertThat(actual) - .isNotNull() - .hasFieldOrPropertyWithValue("name", expected.getName()) - .hasFieldOrPropertyWithValue("url", expected.getUrl()) - .hasFieldOrPropertyWithValue("type", expected.getType()); - } finally { - clearSystemProperty(type, sysProp); - } - } - - public void clearSystemProperty(final String type, final String sysProp) { - if (Objects.nonNull(type) && Objects.nonNull(sysProp)) { - System.clearProperty(getLinkTypePatternPropertyName(type)); - } - } - - private static Link link(String name, String url, String type) { - return new Link().setName(name).setUrl(url).setType(type); - } -} diff --git a/allure-java-commons/src/test/java/io/qameta/allure/ResultsUtilsTest.java b/allure-java-commons/src/test/java/io/qameta/allure/ResultsUtilsTest.java index 343a372bf..27cb75978 100644 --- a/allure-java-commons/src/test/java/io/qameta/allure/ResultsUtilsTest.java +++ b/allure-java-commons/src/test/java/io/qameta/allure/ResultsUtilsTest.java @@ -1,14 +1,21 @@ package io.qameta.allure; +import io.qameta.allure.util.ResultsUtils; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import java.lang.annotation.Annotation; +import java.util.Objects; +import java.util.stream.Stream; import static io.qameta.allure.util.ResultsUtils.ISSUE_LINK_TYPE; import static io.qameta.allure.util.ResultsUtils.TMS_LINK_TYPE; import static io.qameta.allure.util.ResultsUtils.createIssueLink; import static io.qameta.allure.util.ResultsUtils.createLink; import static io.qameta.allure.util.ResultsUtils.createTmsLink; +import static io.qameta.allure.util.ResultsUtils.getLinkTypePatternPropertyName; import static org.assertj.core.api.Assertions.assertThat; /** @@ -120,4 +127,54 @@ public String value() { .hasFieldOrPropertyWithValue("url", null) .hasFieldOrPropertyWithValue("type", TMS_LINK_TYPE); } + + public static Stream data() { + return Stream.of( + Arguments.of("a", "b", "c", "d", "e", link("a", "c", "d")), + Arguments.of("a", "b", "c", "d", null, link("a", "c", "d")), + Arguments.of("a", "b", null, "d", "invalid-pattern", link("a", "invalid-pattern", "d")), + Arguments.of("a", "b", null, "d", "pattern/{}/some", link("a", "pattern/a/some", "d")), + Arguments.of(null, null, null, "d", "pattern/{}/some", link(null, "pattern//some", "d")), + Arguments.of(null, null, null, null, "pattern/{}/some", link(null, null, null)), + Arguments.of(null, "b", null, "d", "pattern/{}/some/{}/and-more", link("b", "pattern/b/some/b/and-more", "d")), + Arguments.of(null, "b", null, "d", null, link("b", null, "d")) + ); + } + + public void setSystemProperty(final String type, final String sysProp) { + if (Objects.nonNull(type) && Objects.nonNull(sysProp)) { + System.setProperty(getLinkTypePatternPropertyName(type), sysProp); + } + } + + @ParameterizedTest + @MethodSource(value = "data") + public void shouldCreateLink(final String value, + final String name, + final String url, + final String type, + final String sysProp, + final io.qameta.allure.model.Link expected) { + setSystemProperty(type, sysProp); + try { + io.qameta.allure.model.Link actual = ResultsUtils.createLink(value, name, url, type); + assertThat(actual) + .isNotNull() + .hasFieldOrPropertyWithValue("name", expected.getName()) + .hasFieldOrPropertyWithValue("url", expected.getUrl()) + .hasFieldOrPropertyWithValue("type", expected.getType()); + } finally { + clearSystemProperty(type, sysProp); + } + } + + public void clearSystemProperty(final String type, final String sysProp) { + if (Objects.nonNull(type) && Objects.nonNull(sysProp)) { + System.clearProperty(getLinkTypePatternPropertyName(type)); + } + } + + private static io.qameta.allure.model.Link link(String name, String url, String type) { + return new io.qameta.allure.model.Link().setName(name).setUrl(url).setType(type); + } } \ No newline at end of file diff --git a/allure-java-commons/src/test/java/io/qameta/allure/StepsTest.java b/allure-java-commons/src/test/java/io/qameta/allure/StepsTest.java index ab0af2e1e..0bdb7973e 100644 --- a/allure-java-commons/src/test/java/io/qameta/allure/StepsTest.java +++ b/allure-java-commons/src/test/java/io/qameta/allure/StepsTest.java @@ -11,6 +11,7 @@ import org.junit.jupiter.api.Test; import java.util.UUID; +import java.util.stream.Collectors; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; @@ -69,6 +70,37 @@ void shouldSupportArrayParameters() { ); } + @SuppressWarnings("unchecked") + @Test + void shouldSupportParallelStepsRun() { + final AllureResultsWriterStub results = runStep(() -> { + Thread[] threads = { + new Thread(this::outerStep), + new Thread(this::outerStep), + new Thread(this::outerStep) + }; + for (Thread thread : threads) { + thread.start(); + } + try { + for (Thread thread : threads) { + thread.join(); + } + } catch (InterruptedException ignored) { + } + }); + + assertThat(results.getTestResults()) + .flatExtracting(TestResult::getSteps) + .extracting( + StepResult::getName, + step -> step.getSteps().stream().map(StepResult::getName).collect(Collectors.toList()) + ) + .containsOnly( + tuple("outerStep", asList("innerStep", "innerStep", "innerStep")) + ); + } + @SuppressWarnings({"unused", "SameParameterValue"}) @Step("\"{user.emails.address}\", \"{user.emails}\", \"{user.emails.attachments}\", \"{user.password}\", \"{}\"," + " \"{user.card.number}\", \"{missing}\", {staySignedIn}") @@ -83,6 +115,21 @@ public void checkData(@SuppressWarnings("unused") final String value) { public void step(@SuppressWarnings("unused") final String... parameters) { } + @Step + private void outerStep() { + for (int i = 0; i < 3; i++) { + innerStep(); + } + } + + @Step + private void innerStep() { + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { + } + } + public static AllureResultsWriterStub runStep(final Runnable runnable) { final AllureResultsWriterStub results = new AllureResultsWriterStub(); final AllureLifecycle lifecycle = new AllureLifecycle(results); diff --git a/allure-java-commons/src/test/java/io/qameta/allure/internal/AllureThreadContextTest.java b/allure-java-commons/src/test/java/io/qameta/allure/internal/AllureThreadContextTest.java new file mode 100644 index 000000000..91feb8978 --- /dev/null +++ b/allure-java-commons/src/test/java/io/qameta/allure/internal/AllureThreadContextTest.java @@ -0,0 +1,131 @@ +package io.qameta.allure.internal; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author charlie (Dmitry Baev). + */ +class AllureThreadContextTest { + + @Test + void shouldCreateEmptyContext() { + final AllureThreadContext context = new AllureThreadContext(); + assertThat(context.getRoot()) + .isEmpty(); + + assertThat(context.getCurrent()) + .isEmpty(); + } + + @Test + void shouldStart() { + final AllureThreadContext context = new AllureThreadContext(); + final String first = UUID.randomUUID().toString(); + final String second = UUID.randomUUID().toString(); + + context.start(first); + context.start(second); + + assertThat(context.getRoot()) + .hasValue(first); + + assertThat(context.getCurrent()) + .hasValue(second); + } + + @Test + void shouldClear() { + final AllureThreadContext context = new AllureThreadContext(); + final String first = UUID.randomUUID().toString(); + final String second = UUID.randomUUID().toString(); + + context.start(first); + context.start(second); + + context.clear(); + + assertThat(context.getRoot()) + .isEmpty(); + } + + @Test + void shouldStop() { + final AllureThreadContext context = new AllureThreadContext(); + final String first = UUID.randomUUID().toString(); + final String second = UUID.randomUUID().toString(); + final String third = UUID.randomUUID().toString(); + + context.start(first); + context.start(second); + context.start(third); + + context.stop(); + + assertThat(context.getCurrent()) + .hasValue(second); + + context.stop(); + + assertThat(context.getCurrent()) + .hasValue(first); + + context.stop(); + + assertThat(context.getCurrent()) + .isEmpty(); + + } + + @Test + void shouldIgnoreStopForEmpty() { + final AllureThreadContext context = new AllureThreadContext(); + context.stop(); + + assertThat(context.getRoot()) + .isEmpty(); + } + + @Test + void shouldBeThreadSafe() throws ExecutionException, InterruptedException { + final AllureThreadContext context = new AllureThreadContext(); + + final int threads = 1000; + final int stepsCount = 200; + final ExecutorService service = Executors.newFixedThreadPool(threads); + + final List>> tasks = new ArrayList<>(); + for (int i = 0; i < threads; i++) { + tasks.add(() -> { + for (int j = 0; j < stepsCount; j++) { + context.start(UUID.randomUUID().toString()); + context.stop(); + } + return context.getCurrent(); + }); + } + + final String base = "ROOT"; + + context.start(base); + final List>> futures = service.invokeAll(tasks); + for (Future> future : futures) { + final Optional value = future.get(); + + assertThat(value) + .hasValue(base); + + } + } +} \ No newline at end of file