ExtentReporter.java
package io.github.giulong.spectrum.utils;
import com.aventstack.extentreports.ExtentReports;
import com.aventstack.extentreports.ExtentTest;
import com.aventstack.extentreports.Status;
import com.aventstack.extentreports.markuputils.ExtentColor;
import com.aventstack.extentreports.model.Report;
import com.aventstack.extentreports.model.Test;
import com.aventstack.extentreports.reporter.ExtentSparkReporter;
import com.aventstack.extentreports.reporter.configuration.ExtentSparkReporterConfig;
import com.aventstack.extentreports.reporter.configuration.Theme;
import io.github.giulong.spectrum.SpectrumTest;
import io.github.giulong.spectrum.interfaces.SessionHook;
import io.github.giulong.spectrum.interfaces.reports.CanProduceMetadata;
import io.github.giulong.spectrum.types.TestData;
import io.github.giulong.spectrum.utils.video.Video;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.extension.ExtensionContext;
import java.awt.*;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static com.aventstack.extentreports.Status.INFO;
import static com.aventstack.extentreports.Status.SKIP;
import static com.aventstack.extentreports.markuputils.ExtentColor.*;
import static com.aventstack.extentreports.markuputils.MarkupHelper.createLabel;
import static io.github.giulong.spectrum.extensions.resolvers.StatefulExtentTestResolver.STATEFUL_EXTENT_TEST;
import static io.github.giulong.spectrum.extensions.resolvers.TestDataResolver.*;
import static lombok.AccessLevel.PROTECTED;
@Slf4j
@NoArgsConstructor(access = PROTECTED)
@Getter
public class ExtentReporter implements SessionHook, CanProduceMetadata {
private static final ExtentReporter INSTANCE = new ExtentReporter();
protected final FileUtils fileUtils = FileUtils.getInstance();
protected final Configuration configuration = Configuration.getInstance();
private final ContextManager contextManager = ContextManager.getInstance();
private ExtentReports extentReports;
public static ExtentReporter getInstance() {
return INSTANCE;
}
@Override
public void sessionOpened() {
log.debug("Session opened hook");
final Configuration.Extent extent = configuration.getExtent();
final String reportPath = getReportPathFrom(extent).toString().replace("\\", "/");
final String reportName = extent.getReportName();
final String css = fileUtils.read("css/internal-report.css") + fileUtils.read(extent.getCss());
final String js = fileUtils.read("js/internal-report.js") + fileUtils.read(extent.getJs());
extentReports = new ExtentReports();
extentReports.attachReporter(new ExtentSparkReporter(reportPath)
.config(ExtentSparkReporterConfig
.builder()
.documentTitle(extent.getDocumentTitle())
.reportName(reportName)
.theme(Theme.valueOf(extent.getTheme()))
.timeStampFormat(extent.getTimeStampFormat())
.css(css)
.js(js)
.build()));
log.info("After the execution, you'll find the '{}' report at file:///{}", reportName, reportPath);
}
@SneakyThrows
@Override
public void sessionClosed() {
log.debug("Session closed hook");
sortTests();
extentReports.flush();
final Configuration.Extent extent = configuration.getExtent();
if (extent.isOpenAtEnd()) {
log.debug("Opening extent report in default browser");
Desktop.getDesktop().open(getReportPathFrom(extent).toFile());
}
cleanupOldReportsIn(extent.getReportFolder());
}
@Override
public Retention getRetention() {
return configuration.getExtent().getRetention();
}
@Override
public void produceMetadata() {
final MetadataManager metadataManager = MetadataManager.getInstance();
final File file = getMetadata().toFile();
final int maxSize = getRetention().getSuccessful();
final FixedSizeQueue<File> queue = metadataManager.getSuccessfulQueueOf(this);
log.debug("Adding metadata '{}'. Current size: {}, max capacity: {}", file, queue.size(), maxSize);
queue.shrinkTo(maxSize - 1).add(file);
metadataManager.setSuccessfulQueueOf(this, queue);
}
public ExtentTest createExtentTestFrom(final ExtensionContext context) {
final TestData testData = contextManager.get(context, TEST_DATA, TestData.class);
final String id = testData.getTestId();
return extentReports
.createTest(String.format("<div id=\"%s\">%s</div><div id=\"%s-test-name\">%s</div>", id, testData.getClassDisplayName(), id, testData.getDisplayName()))
.assignCategory(context.getTags().toArray(new String[0]));
}
public void attachVideo(final ExtentTest extentTest, final Video.ExtentTest videoExtentTest, final String testId, final Path path) {
final String width = videoExtentTest.getWidth();
final String height = videoExtentTest.getHeight();
final String videoTag = "<video id=\"video-%s\" controls width=\"%s\" height=\"%s\" src=\"%s\" type=\"video/mp4\" " +
"ontimeupdate=\"syncVideoWithStep(event)\" onseeking=\"syncVideoWithStep(event)\"" +
"onseeked=\"videoPaused(event)\" onpause=\"videoPaused(event)\"/>";
extentTest.info(String.format(videoTag, testId, width, height, path));
}
public void logTestStartOf(final ExtentTest extentTest) {
extentTest.info(createLabel("START TEST", getColorOf(INFO)));
}
public void logTestEnd(final ExtensionContext context, final Status status) {
final TestContext testContext = contextManager.get(context);
final StatefulExtentTest statefulExtentTest = testContext.computeIfAbsent(STATEFUL_EXTENT_TEST, k -> {
final Class<?> clazz = context.getRequiredTestClass();
final String className = clazz.getSimpleName();
final String methodName = context.getRequiredTestMethod().getName();
final String classDisplayName = fileUtils.sanitize(getDisplayNameOf(clazz));
final String displayName = fileUtils.sanitize(joinTestDisplayNamesIn(context));
final String testId = buildTestIdFrom(className, displayName);
testContext.put(TEST_DATA, TestData
.builder()
.className(className)
.methodName(methodName)
.classDisplayName(classDisplayName)
.displayName(displayName)
.testId(testId)
.build());
return StatefulExtentTest
.builder()
.currentNode(createExtentTestFrom(context))
.build();
}, StatefulExtentTest.class);
switch (status) {
case SKIP -> {
final String disabledValue = context.getRequiredTestMethod().getAnnotation(Disabled.class).value();
final String reason = "".equals(disabledValue) ? "no reason" : disabledValue;
statefulExtentTest.getCurrentNode().skip(createLabel("Skipped: " + reason, getColorOf(SKIP)));
}
case FAIL -> {
final SpectrumTest<?> spectrumTest = (SpectrumTest<?>) context.getRequiredTestInstance();
statefulExtentTest.getCurrentNode().fail(context.getExecutionException().orElse(new RuntimeException("Test Failed with no exception")));
spectrumTest.screenshotFail(createLabel("TEST FAILED", RED).getMarkup());
}
default -> statefulExtentTest.getCurrentNode().log(status, createLabel("END TEST", getColorOf(status)));
}
}
Path getMetadata() {
return getReportPathFrom(configuration.getExtent()).getParent();
}
Path getReportPathFrom(final Configuration.Extent extent) {
final String fileName = extent.getFileName();
return Path.of(extent.getReportFolder(), fileUtils.removeExtensionFrom(fileName), fileName).toAbsolutePath();
}
void sortTests() {
final Report report = extentReports.getReport();
final List<Test> tests = new ArrayList<>(report.getTestList());
tests.stream().map(Test::getName).forEach(extentReports::removeTest);
tests.sort(configuration.getExtent().getSort());
tests.forEach(report::addTest);
}
void cleanupOldReportsIn(final String folder) {
final Retention retention = configuration.getExtent().getRetention();
log.info("Extent reports to keep in {}: {}", folder, retention.getTotal());
final File[] folderContent = Path
.of(folder)
.toFile()
.listFiles();
if (folderContent == null) {
log.debug("Extent reports folder {} is empty already", folder);
return;
}
retention.deleteArtifactsFrom(List.of(folderContent), this);
}
ExtentColor getColorOf(final Status status) {
return switch (status) {
case FAIL -> RED;
case SKIP -> AMBER;
default -> GREEN;
};
}
}