From 7d9728f7762d15568716a8c5ffcb330e030d0ac9 Mon Sep 17 00:00:00 2001 From: Iaroslav Iershov Date: Tue, 19 Jun 2018 22:32:50 +0300 Subject: [PATCH 1/2] #20: Implemented ability to collect and attach Selenium logs to RP --- .../reportportal/EnhancedLogEntry.java | 30 ++++++++++ .../invictum/reportportal/LogStorage.java | 60 +++++++++++++++++++ .../reportportal/ReportPortalListener.java | 21 ++++++- .../reportportal/StepsSetProfile.java | 3 +- .../injector/SerenityPortalModule.java | 6 +- .../processor/SeleniumLogsAttacher.java | 45 ++++++++++++++ .../reportportal/EnhancedLogEntityTest.java | 18 ++++++ .../invictum/reportportal/LogStorageTest.java | 60 +++++++++++++++++++ .../reportportal/StepsSetProfileTest.java | 3 +- 9 files changed, 239 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/github/invictum/reportportal/EnhancedLogEntry.java create mode 100644 src/main/java/com/github/invictum/reportportal/LogStorage.java create mode 100644 src/main/java/com/github/invictum/reportportal/processor/SeleniumLogsAttacher.java create mode 100644 src/test/java/com/github/invictum/reportportal/EnhancedLogEntityTest.java create mode 100644 src/test/java/com/github/invictum/reportportal/LogStorageTest.java diff --git a/src/main/java/com/github/invictum/reportportal/EnhancedLogEntry.java b/src/main/java/com/github/invictum/reportportal/EnhancedLogEntry.java new file mode 100644 index 0000000..1ece6cd --- /dev/null +++ b/src/main/java/com/github/invictum/reportportal/EnhancedLogEntry.java @@ -0,0 +1,30 @@ +package com.github.invictum.reportportal; + +import org.openqa.selenium.logging.LogEntry; + +import java.util.Map; +import java.util.logging.Level; + +/** + * Enhanced version of {@link LogEntry} with added type field + */ +public class EnhancedLogEntry extends LogEntry { + + private String type; + + public EnhancedLogEntry(String type, Level level, long timestamp, String message) { + super(level, timestamp, message); + this.type = type; + } + + public String getType() { + return type; + } + + @Override + public Map toJson() { + Map json = super.toJson(); + json.put("type", type); + return json; + } +} diff --git a/src/main/java/com/github/invictum/reportportal/LogStorage.java b/src/main/java/com/github/invictum/reportportal/LogStorage.java new file mode 100644 index 0000000..b28e71f --- /dev/null +++ b/src/main/java/com/github/invictum/reportportal/LogStorage.java @@ -0,0 +1,60 @@ +package com.github.invictum.reportportal; + +import org.openqa.selenium.logging.Logs; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Central storage for collected logs provided by Selenium + */ +public class LogStorage { + + private ThreadLocal> logs = ThreadLocal.withInitial(ArrayList::new); + private ThreadLocal>> types = ThreadLocal.withInitial(Optional::empty); + + /** + * Collects {@link org.openqa.selenium.WebDriver} logs if available and stores its to internal storage + * + * @param seleniumLogs to collect from + */ + public void collect(Logs seleniumLogs) { + // Discover available log types + if (!types.get().isPresent()) { + Set discoveredTypes = seleniumLogs.getAvailableLogTypes(); + types.set(Optional.of(discoveredTypes)); + } + // Collect all available logs + types.get().ifPresent(types -> types.forEach(type -> { + List typedLogs = seleniumLogs.get(type).getAll().stream() + .map(log -> new EnhancedLogEntry(type, log.getLevel(), log.getTimestamp(), log.getMessage())) + .collect(Collectors.toList()); + logs.get().addAll(typedLogs); + })); + } + + /** + * Clears all logs related to current thread + */ + public void clean() { + logs.remove(); + types.remove(); + } + + /** + * Returns logs that meets defined condition and removes them from storage. + * Logs could be queried only once + * + * @param predicate condition to use to find logs + * @return a list of queried logs + */ + public List query(Predicate predicate) { + List result = logs.get().stream().filter(predicate).collect(Collectors.toList()); + logs.get().removeAll(result); + return result; + } +} diff --git a/src/main/java/com/github/invictum/reportportal/ReportPortalListener.java b/src/main/java/com/github/invictum/reportportal/ReportPortalListener.java index 1286bc3..79f5447 100644 --- a/src/main/java/com/github/invictum/reportportal/ReportPortalListener.java +++ b/src/main/java/com/github/invictum/reportportal/ReportPortalListener.java @@ -4,21 +4,26 @@ import com.github.invictum.reportportal.handler.Handler; import com.github.invictum.reportportal.handler.HandlerType; import com.github.invictum.reportportal.handler.TreeHandler; +import com.github.invictum.reportportal.injector.IntegrationInjector; import net.thucydides.core.model.DataTable; import net.thucydides.core.model.Story; import net.thucydides.core.model.TestOutcome; import net.thucydides.core.steps.ExecutedStepDescription; import net.thucydides.core.steps.StepFailure; import net.thucydides.core.steps.StepListener; +import net.thucydides.core.webdriver.ThucydidesWebDriverSupport; +import org.openqa.selenium.logging.Logs; import java.util.Map; public class ReportPortalListener implements StepListener { private Handler handler; + private LogStorage logStorage; public ReportPortalListener() { handler = (ReportIntegrationConfig.handlerType == HandlerType.FLAT) ? new FlatHandler() : new TreeHandler(); + logStorage = IntegrationInjector.getInjector().getInstance(LogStorage.class); } public void testSuiteStarted(Class storyClass) { @@ -38,11 +43,12 @@ public void testStarted(String description) { } public void testStarted(String description, String id) { - handler.startTest(description); + testStarted(description); } public void testFinished(TestOutcome result) { handler.finishTest(result); + logStorage.clean(); } public void testRetried() { @@ -59,26 +65,32 @@ public void skippedStepStarted(ExecutedStepDescription description) { public void stepFailed(StepFailure failure) { /* Not used by listener */ + collectDriverLogs(); } public void lastStepFailed(StepFailure failure) { /* Not used by listener */ + collectDriverLogs(); } public void stepIgnored() { /* Not used by listener */ + collectDriverLogs(); } public void stepPending() { /* Not used by listener */ + collectDriverLogs(); } public void stepPending(String message) { /* Not used by listener */ + collectDriverLogs(); } public void stepFinished() { /* Not used by listener */ + collectDriverLogs(); } public void testFailed(TestOutcome testOutcome, Throwable cause) { @@ -128,4 +140,11 @@ public void assumptionViolated(String message) { public void testRunFinished() { /* Not used by listener */ } + + private void collectDriverLogs() { + if (ThucydidesWebDriverSupport.isInitialised()) { + Logs logs = ThucydidesWebDriverSupport.getDriver().manage().logs(); + logStorage.collect(logs); + } + } } diff --git a/src/main/java/com/github/invictum/reportportal/StepsSetProfile.java b/src/main/java/com/github/invictum/reportportal/StepsSetProfile.java index cb7e311..0eb979d 100644 --- a/src/main/java/com/github/invictum/reportportal/StepsSetProfile.java +++ b/src/main/java/com/github/invictum/reportportal/StepsSetProfile.java @@ -38,7 +38,8 @@ public StepProcessor[] processors() { new ScreenshotAttacher(), new FinishStepLogger(), new ErrorLogger(true), - new HtmlSourceAttacher() + new HtmlSourceAttacher(), + new SeleniumLogsAttacher() }; } diff --git a/src/main/java/com/github/invictum/reportportal/injector/SerenityPortalModule.java b/src/main/java/com/github/invictum/reportportal/injector/SerenityPortalModule.java index 3a7b6fd..28013f1 100644 --- a/src/main/java/com/github/invictum/reportportal/injector/SerenityPortalModule.java +++ b/src/main/java/com/github/invictum/reportportal/injector/SerenityPortalModule.java @@ -1,10 +1,7 @@ package com.github.invictum.reportportal.injector; import com.epam.reportportal.service.Launch; -import com.github.invictum.reportportal.NarrativeFormatter; -import com.github.invictum.reportportal.ReportIntegrationConfig; -import com.github.invictum.reportportal.StepProcessorsHolder; -import com.github.invictum.reportportal.StepProcessorsHolderProvider; +import com.github.invictum.reportportal.*; import com.google.inject.AbstractModule; import com.google.inject.Provides; import com.google.inject.Scopes; @@ -15,6 +12,7 @@ public class SerenityPortalModule extends AbstractModule { protected void configure() { bind(Launch.class).toProvider(ReportLaunchProvider.class).in(Scopes.SINGLETON); bind(StepProcessorsHolder.class).toProvider(StepProcessorsHolderProvider.class).in(Scopes.SINGLETON); + bind(LogStorage.class).in(Scopes.SINGLETON); } @Provides diff --git a/src/main/java/com/github/invictum/reportportal/processor/SeleniumLogsAttacher.java b/src/main/java/com/github/invictum/reportportal/processor/SeleniumLogsAttacher.java new file mode 100644 index 0000000..aedb2ac --- /dev/null +++ b/src/main/java/com/github/invictum/reportportal/processor/SeleniumLogsAttacher.java @@ -0,0 +1,45 @@ +package com.github.invictum.reportportal.processor; + +import com.epam.reportportal.service.ReportPortal; +import com.github.invictum.reportportal.EnhancedLogEntry; +import com.github.invictum.reportportal.LogLevel; +import com.github.invictum.reportportal.LogStorage; +import com.github.invictum.reportportal.Utils; +import com.github.invictum.reportportal.injector.IntegrationInjector; +import net.thucydides.core.model.TestStep; + +import java.util.Date; +import java.util.function.Predicate; + +/** + * Attaches logs provided by Selenium + */ +public class SeleniumLogsAttacher implements StepProcessor { + + private Predicate filter; + + public SeleniumLogsAttacher(Predicate filter) { + this.filter = filter; + } + + public SeleniumLogsAttacher() { + this(log -> true); + } + + @Override + public void proceed(final TestStep step) { + long start = Utils.stepEndDate(step).getTime(); + long end = Utils.stepEndDate(step).getTime(); + LogStorage storage = IntegrationInjector.getInjector().getInstance(LogStorage.class); + storage.query(filter.and(entry -> start <= entry.getTimestamp() && entry.getTimestamp() >= end)) + .forEach(log -> { + String message = String.format("[Selenium-%s] [%s] %s", log.getType(), log.getLevel(), log.getMessage()); + ReportPortal.emitLog(message, LogLevel.DEBUG.toString(), new Date(log.getTimestamp())); + }); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof SeleniumLogsAttacher; + } +} diff --git a/src/test/java/com/github/invictum/reportportal/EnhancedLogEntityTest.java b/src/test/java/com/github/invictum/reportportal/EnhancedLogEntityTest.java new file mode 100644 index 0000000..7983de9 --- /dev/null +++ b/src/test/java/com/github/invictum/reportportal/EnhancedLogEntityTest.java @@ -0,0 +1,18 @@ +package com.github.invictum.reportportal; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.util.logging.Level; + +@RunWith(JUnit4.class) +public class EnhancedLogEntityTest { + + @Test + public void toJsonTest() { + EnhancedLogEntry entry = new EnhancedLogEntry("type", Level.INFO, 0, "Message"); + Assert.assertTrue(entry.toJson().containsKey("type")); + } +} diff --git a/src/test/java/com/github/invictum/reportportal/LogStorageTest.java b/src/test/java/com/github/invictum/reportportal/LogStorageTest.java new file mode 100644 index 0000000..b9db43b --- /dev/null +++ b/src/test/java/com/github/invictum/reportportal/LogStorageTest.java @@ -0,0 +1,60 @@ +package com.github.invictum.reportportal; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mockito; +import org.openqa.selenium.logging.LogEntries; +import org.openqa.selenium.logging.LogEntry; +import org.openqa.selenium.logging.Logs; + +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; + +@RunWith(JUnit4.class) +public class LogStorageTest { + + private LogStorage storage; + private Logs logsMock; + + @Before + public void beforeTest() { + storage = new LogStorage(); + logsMock = Mockito.mock(Logs.class); + Mockito.when(logsMock.getAvailableLogTypes()).thenReturn(Collections.singleton("data")); + LogEntry entry = new EnhancedLogEntry("data", Level.INFO, 42, "Message"); + LogEntries entries = new LogEntries(Collections.singleton(entry)); + Mockito.when(logsMock.get("data")).thenReturn(entries); + } + + @Test + public void collectLogsTest() { + storage.collect(logsMock); + List actual = storage.query(item -> true); + Assert.assertEquals(1, actual.size()); + } + + @Test + public void queryLogsRemoveTest() { + storage.query(item -> true); + List actual = storage.query(item -> true); + Assert.assertTrue(actual.isEmpty()); + } + + @Test + public void cleanLogsTest() { + storage.collect(logsMock); + storage.clean(); + Assert.assertTrue(storage.query(item -> true).isEmpty()); + } + + @Test + public void collectAvailableTypesOnlyOnceTest() { + storage.collect(logsMock); + storage.collect(logsMock); + Mockito.verify(logsMock, Mockito.times(1)).getAvailableLogTypes(); + } +} diff --git a/src/test/java/com/github/invictum/reportportal/StepsSetProfileTest.java b/src/test/java/com/github/invictum/reportportal/StepsSetProfileTest.java index 751f7a3..a5a8a5c 100644 --- a/src/test/java/com/github/invictum/reportportal/StepsSetProfileTest.java +++ b/src/test/java/com/github/invictum/reportportal/StepsSetProfileTest.java @@ -17,7 +17,8 @@ public void fullProfileTest() { new ScreenshotAttacher(), new FinishStepLogger(), new ErrorLogger(true), - new HtmlSourceAttacher() + new HtmlSourceAttacher(), + new SeleniumLogsAttacher() }; Assert.assertArrayEquals(actual, expected); } From 89fdbfeba1a6fe8b9cf1a0f5cfb4ac33992ee658 Mon Sep 17 00:00:00 2001 From: Iaroslav Iershov Date: Wed, 20 Jun 2018 12:19:48 +0300 Subject: [PATCH 2/2] #20: Updated documentation in accordance to new features --- README.md | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8c47bcc..673760b 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ Setup ------------- To add support of integration between Serenity and Report Portal simply add dependencies to your project based on used build tool. +> **Warning** +> Don't add any extra Report Portal listeners or agents. Integration is provided by single module for all availavle Serenity approaches + **Maven** Edit project's `pom.xml` file @@ -18,10 +21,10 @@ Edit project's `pom.xml` file com.github.invictum serenity-reportportal-integration - 1.1.1 + 1.1.3 ``` -Report Portal core libraries are used, but it uses external repository, so it URL also should be added to your build configuration +Report Portal core libraries are used, but they placed in repository, so its URL also should be added to your build configuration ``` @@ -39,7 +42,7 @@ Report Portal core libraries are used, but it uses external repository, so it UR Edit `build.gradle` file in the project root ``` -compile group: 'com.github.invictum', name: 'serenity-reportportal-integration', version: '1.1.1' +compile group: 'com.github.invictum', name: 'serenity-reportportal-integration', version: '1.1.3' ``` External Report Portal repository should be defined the same as for Maven ``` @@ -85,13 +88,28 @@ Each Serenity `TestStep` object is passed through chain of configured `StepProce - CUSTOM `DEFAULT` profile is used by default and contains all usually required reporting details. It generates in Report Portal a nice log that does not cluttered with extra details. -`FULL` profile contains all available `StepProcessors` and generates full reporting. To customize what should be logged `CUSTOM` profile should be used. + +`FULL` profile contains all available `StepProcessors` and generates full reporting. + +To customize what should be logged `CUSTOM` profile should be used. ``` StepsSetProfile config = StepsSetProfile.CUSTOM; config.registerProcessors(new StartStepLogger(), new FinishStepLogger()); ReportIntegrationConfig.profile = config; ``` -In example above `CUSTOM` profile with `StartStepLogger` and `FinishStepLogger` processors is configured. All step processors available out of the box may be observed in `com.github.invictum.reportportal.processor` package. +In example above `CUSTOM` profile with `StartStepLogger` and `FinishStepLogger` processors is configured. + +**Processors** + +All step processors available out of the box may be observed in `com.github.invictum.reportportal.processor` package. +For now following processors are available: +- `StartStepLogger` logs all started steps. +- `FinishStepLogger` logs all finished steps. Log level depends on step results. +- `ErrorLogger` reports error if present. Includes regular errors as well as assertion fails. It is possible to pass a boolean flag to the constructor in order to have short or long error reporting format. +- `ScreenshotAttacher` emits screenshots to RP if present. It simply attaches all available step's screenshots, so screenshot strategy is configured on Serenity level. +- `HtmlSourceAttacher` logs page source if available. +- `SeleniumLogsAttacher` reports logs supplied by Selenium. It is possible to pass predicate to constructor in order to push particular logs. By default emits all available logs. + It is possible to use integrated processors as well as implemented by your own. To make own processor implement `StepProcessor` interface. In custom implementation access to Serenity's `TestStep` object is provided ``` public class MyCustomLoggerLogger implements StepProcessor { @@ -103,7 +121,7 @@ public class MyCustomLoggerLogger implements StepProcessor { } ``` > **Warning** -To emit log to Report Portal time should be specified. If log timestamp is out of range of step it won't be emitted at all. `TestStep` object contains all data to calculate start, end dates and duration +> To emit log to Report Portal time should be specified. If log timestamp is out of range of step it won't be emitted at all. `TestStep` object contains all data to calculate start, end dates and duration The order of processors registration is matters, this order the same as order of invocation. @@ -147,7 +165,7 @@ Integration provides two strategies of storing Serenity's test data to Report Po Report Portal has a few limitations regurding to flexible nested structures support for now. As a result test report may contain some inaccuracate data. E. g. test count for launch will show total number of tests + total number of steps. -Nevertheless `TREE` configuration allows to get additional features with RP. E. g. integrated RP test analisys facility will be changed from test to step. +Nevertheless `TREE` configuration allows to get additional features with RP. E. g. integrated RP test analisys facility scope will be changed from test to step. Handler type may be changed with following configuration ``` @@ -230,12 +248,12 @@ Report Portal integration uses 3 digit version format - x.y.z Important release notes ----------------------- -Consolidated release notes are described below +Important release notes are described below. Use [releases](https://github.com/Invictum/serenity-reportportal-integration/releases) section for details regarding regular release notes. Version | Note ---------------|--------------------------- 1.0.0 - 1.0.6 | Supports RP v3 and below -1.1.0 | Minor version update due RP v4 release. Versions older than 1.1.0 are not compatible with RP v4+ and vise versa +1.1.0+ | Minor version update due RP v4 release. Versions older than 1.1.0 are not compatible with RP v4+ and vise versa Limitations -------------