From a97b13e44000c25a889ac250f9fa04af2cb4a912 Mon Sep 17 00:00:00 2001 From: Nick Date: Sun, 14 Jun 2026 20:07:01 +0300 Subject: [PATCH 1/2] Issue-113. feat: add @TmsLink support and @Artifact annotations. Added support for resolving Test IDs from Allure `@TmsLink` annotations in the Testomat Allure Adapter. Added support for automatic artifact attachment via the `@Artifact` annotation. Updated README with `@Artifact` usage examples and documentation improvements --- README.md | 354 +++++++++++------- java-reporter-core/pom.xml | 8 +- .../io/testomat/core/annotation/Artifact.java | 11 + .../core/constants/CommonConstants.java | 2 +- .../io/testomat/core/facade/Testomatio.java | 10 +- .../methods/artifact/ArtifactAspect.java | 57 +++ .../artifact/manager/ArtifactManager.java | 3 + .../io/testomat/core/step/StepAspect.java | 27 +- .../src/main/resources/META-INF/aop.xml | 1 + .../core/artifact/ArtifactAspectTest.java | 138 +++++++ .../io/testomat/core/step/StepAspectTest.java | 18 +- java-reporter-cucumber/pom.xml | 4 +- java-reporter-junit/pom.xml | 4 +- java-reporter-karate/pom.xml | 4 +- java-reporter-testng/pom.xml | 4 +- pom.xml | 2 +- testomat-allure-adapter/pom.xml | 10 +- .../aspect/AllureAttachmentAspect.java | 59 ++- .../testomat/aspect/AllureTmsLinkAspect.java | 32 ++ .../testomat/resolver/AllureTmsResolver.java | 16 + .../src/main/resources/META-INF/aop.xml | 1 + .../aspect/AllureAttachmentAspectTest.java | 169 +++++++-- .../java/aspect/AllureTmsLinkAspectTest.java | 114 ++++++ .../java/resolver/AllureTmsResolverTest.java | 47 +++ 24 files changed, 871 insertions(+), 224 deletions(-) create mode 100644 java-reporter-core/src/main/java/io/testomat/core/annotation/Artifact.java create mode 100644 java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/ArtifactAspect.java create mode 100644 java-reporter-core/src/test/java/io/testomat/core/artifact/ArtifactAspectTest.java create mode 100644 testomat-allure-adapter/src/main/java/io/testomat/aspect/AllureTmsLinkAspect.java create mode 100644 testomat-allure-adapter/src/main/java/io/testomat/resolver/AllureTmsResolver.java create mode 100644 testomat-allure-adapter/src/test/java/aspect/AllureTmsLinkAspectTest.java create mode 100644 testomat-allure-adapter/src/test/java/resolver/AllureTmsResolverTest.java diff --git a/README.md b/README.md index a9731fc..39d752e 100644 --- a/README.md +++ b/README.md @@ -1,151 +1,153 @@ # Testomat.io Java Reporter ---- +Run tests with JUnit, TestNG, Cucumber, Karate and automatically publish results, steps, artifacts and metadata to Testomat. -## What is this? +--- -This is the **official Java reporter** for [Testomat.io](https://testomat.io/) - a powerful test management platform. -It automatically sends your test results to the platform, giving you comprehensive reports, analytics, -and team collaboration features. +## Features -### 🔄 Current Status & Roadmap +- Automatic test result reporting +- TestId synchronization with Testomat.io +- Test filtering by IDs +- Step-by-step execution reporting +- Artifact uploads (screenshots, logs, videos) +- Shared and public test runs +- Allure integration (via Testomat Allure Adapter) -> 🚧 **Actively developed** - New features added regularly! --- -## Features +## Supported Frameworks -| Feature | Description | JUnit | TestNG | Cucumber | Karate | -|------------------------------------|----------------------------------------------------|:-----:|:------:|:--------:|:------:| -| **Complete framework integration** | Full framework support and compatibility | ✅ | ✅ | ✅ | ✅ | -| **Autostart on tests run** | Automatic integration with test execution | ✅ | ✅ | ✅ | ✅ | -| **Shared run** | Collaborative test execution sharing | ✅ | ✅ | ✅ | ✅ | -| **Test runs grouping** | Organize and categorize test executions | ✅ | ✅ | ✅ | ✅ | -| **Public sharable link** | Generate public URLs for test run results | ✅ | ✅ | ✅ | ✅ | -| **Test code export** | Export test code from codebase to platform | ✅ | ✅ | ✅ | ✅ | -| **Advanced error reporting** | Detailed test failure/skip descriptions | ✅ | ✅ | ✅ | ✅ | -| **TestId import** | Import test IDs from testomat.io into the codebase | ✅ | ✅ | ✅ | ✅ | -| **Test filter by ID** | Run tests filtered by IDs | ✅ | ✅ | ✅ | ✅ | -| **Parametrized tests support** | Enhanced support for parameterized testing | ✅ | ✅ | ✅ | ✅ | -| **Test artifacts support** | Screenshots, logs, and file attachments | ✅ | ✅ | ✅ | ✅ | -| **Step-by-step reporting** | Detailed test step execution tracking | ✅ | ✅ | ✅ | ✅ | -| **Custom hooks** | Allows user's own reporting enhancements | ✅ | ✅ | ✅ | ✅ | -| **Other frameworks support** | Gauge, etc. (Priority may change) | ⏳ | ⏳ | ⏳ | ⏳ | - -## 🖥️ Supported test frameworks versions - -| What you need | Version | We tested with | Supported java version | -|---------------|:-------:|:--------------:|:----------------------:| -| **JUnit** | 5.x | 5.9.2 | Java 11+ | -| **Cucumber** | 7.x | 7.14.0 | Java 11+ | -| **Karate** | 1.x | 1.5.0 | Java 17+ | +| Framework | Supported Version | Java Version | +|-----------|------------------|--------------| +| JUnit | 5.x | 11+ | +| TestNG | 7.x | 11+ | +| Cucumber | 7.x | 11+ | +| Karate | 1.x | 17+ | --- -## Common setup for all frameworks: +## Quick Start -1. **Add the latest version** of the dependency to your POM.xml: - [TestNG](https://central.sonatype.com/artifact/io.testomat/java-reporter-testng) - [JUnit](https://central.sonatype.com/artifact/io.testomat/java-reporter-junit) - [Cucumber](https://central.sonatype.com/artifact/io.testomat/java-reporter-cucumber) - [Karate](https://central.sonatype.com/artifact/io.testomat/java-reporter-karate) +### 1. Add the Reporter Dependency -2. **Get your API key** from [Testomat.io](https://app.testomat.io/) (starts with `tstmt_`) -3. **Set your API key** as environment variable: - ```bash - export testomatio=tstmt_your_key_here - ``` - - Or add to the `testomatio.properties` : - ```properties - testomatio=tstmt_your_key_here - ``` - Or provide it as a JVM property on run via -D flag. -4. Also provide run title in the `testomatio.run.title` property, otherwise runs will have the name "Default Test Run". -5. IMPORTANT: The reporter will run automatically if the API_KEY is provided in any way! To disable, use - `testomatio.reporting.disable=1`. +Add the Testomat reporter dependency that matches your test framework. Detailed setup instructions for JUnit, TestNG, Cucumber, and Karate are provided below. + +### 2. Configure API Key + +Using environment variables: + +```bash +export testomatio=tstmt_your_project_api_key +``` + +Or using `testomatio.properties`: + +```properties +testomatio=tstmt_your_project_api_key +testomatio.run.title=Nightly Regression +``` + +Or pass properties via JVM arguments: + +```bash +mvn test \ + -Dtestomatio=tstmt_your_project_api_key \ + -Dtestomatio.run.title="Nightly Regression" +``` + +### 3. Run Tests + +```bash +mvn test +``` + +If a valid API key is provided reporting starts automatically. + +### Disable Reporting + +```properties +testomatio.reporting.disable=1 +``` --- -## Framework specific setup +## Framework Setup ### JUnit -**Step 1:** Create file `src/main/resources/junit-platform.properties` +Create: -**Step 2:** Add this single line: +```text +src/main/resources/junit-platform.properties +``` - ```properties - junit.jupiter.extensions.autodetection.enabled=true - ``` +Add: + +```properties +junit.jupiter.extensions.autodetection.enabled=true +``` ### TestNG -No additional actions needed as TestNG handles the extension implicitly. +No additional setup required. ### Cucumber -Add `io.testomat.cucumber.listener.CucumberListener` as @ConfigurationParameter value to your TestRunner class. -Like this: +Register the listener: ```java - @ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty, io.testomat.cucumber.listener.CucumberListener") +@ConfigurationParameter( + key = PLUGIN_PROPERTY_NAME, + value = "pretty, io.testomat.cucumber.listener.CucumberListener" +) ``` ### Karate -Add `KarateHookFactory` as the hook factory `.hookFactory(new KarateHookFactory())` to your TestRunner class. +Register the hook factory: ```java -class KarateTest { - - @Test - void testParallel() { - - Results results = Runner.path("classpath:karateTests") - .hookFactory(new KarateHookFactory()) - .outputCucumberJson(true) - .outputJunitXml(true) - .parallel(4); - - Assertions.assertEquals( - 0, - results.getFailCount(), - results.getErrorMessages() - ); - } -} +.hookFactory(new KarateHookFactory()) ``` + --- -## Test codebase sync +## Test Code Synchronization -> For proper usage of this library it is **strongly recommended** to sync your test codebase with Testomat.io base. +Keeping Test IDs synchronized between your codebase and Testomat.io is strongly recommended. -### JUnit, TestNG +Use Java Check Tests CLI to: -For this purpose you can use the [Java-Check-Tests CLI](https://github.com/testomatio/java-check-tests). -What this is for: +- Import tests into Testomat.io +- Synchronize Test IDs +- Update existing IDs +- Remove obsolete IDs -- Import your test source code to Testomat.io -- Sync test IDs between Testomat.io project and your codebase -- Remove test IDs and related imports if you need to +### Download and Update IDs -Use these one-liners to **download jar and update** IDs in one move: +UNIX / macOS: -UNIX, MACOS: -`export TESTOMATIO_URL=... && \export TESTOMATIO=... && curl -L -O https://github.com/testomatio/java-check-tests/releases/latest/download/java-check-tests.jar && java -jar java-check-tests.jar update-ids` +```bash +export TESTOMATIO_URL=... +export TESTOMATIO=... -WINDOWS cmd: -`set TESTOMATIO_URL=...&& set TESTOMATIO=...&& curl -L -O https://github.com/testomatio/java-check-tests/releases/latest/download/java-check-tests.jar&& java -jar java-check-tests.jar update-ids` +curl -L -O \ +https://github.com/testomatio/java-check-tests/releases/latest/download/java-check-tests.jar -**Where TESTOMATIO_URL is server URL and TESTOMATIO is your project API key.** -**Be careful with whitespaces in the Windows command.** +java -jar java-check-tests.jar update-ids +``` -> For more details please read the description of full CLI functionality here: -> https://github.com/testomatio/java-check-tests +Windows: ---- -**For most cases, the library is ready to use with this setup** +```cmd +set TESTOMATIO_URL=... +set TESTOMATIO=... + +curl -L -O https://github.com/testomatio/java-check-tests/releases/latest/download/java-check-tests.jar + +java -jar java-check-tests.jar update-ids +``` --- @@ -159,7 +161,7 @@ testomatio=tstmt_your_key_here ``` Or provide it as JVM property or ENV variable. -IMPORTANT: The reporter will run automatically if the API_KEY is provided in any way! +IMPORTANT: The reporter runs automatically when the `testomatio` API key is configured. ### Customization @@ -173,7 +175,7 @@ Here are the options to customize the reporting in the way you need: | **`testomatio.run.group`** | Group related runs together | _(none)_ | `"sprint-23"` | | **`testomatio.publish`** | Make results publicly shareable | _(private)_ | Any not null/empty/"0" string, "0" to disable | -### 🔗 Advanced Integration +### Advanced Integration | Setting | What it does | Example | |-------------------------------------|------------------------------------------|----------------------------| @@ -187,17 +189,17 @@ Here are the options to customize the reporting in the way you need: --- -## 🏷️ Test Identification & Titles +## Test Identification & Titles Connect your code tests directly to your Testomat.io test cases using simple annotations! -As mentioned above, test IDs are recommended to be synced with Java-Check-Tests CLI. +As mentioned above test IDs are recommended to be synced with Java-Check-Tests CLI. But @Title usage is up to you. -### 📋 For JUnit & TestNG +### For JUnit & TestNG Use `@TestId` and `@Title` annotations to make your tests perfectly trackable: -> 💡 **Tip**: With `@TestId` annotations in place, you can filter and run specific tests by their IDs - see [Test Filtering by ID](#-test-filtering-by-id) below. +> **Tip**: With `@TestId` annotations in place you can filter and run specific tests by their IDs see [Test Filtering by ID](#-test-filtering-by-id) below. ```java import com.testomatio.reporter.annotation.TestId; @@ -220,28 +222,26 @@ public class LoginTests { } @Test - @Title("User sees helpful error message") // Just title, auto-generated ID + @Title("User sees helpful error message") // Just title, auto generated ID public void testErrorMessage() { // Your test code here } } ``` -## LinkTest +### Linking Multiple Test Cases Links test IDs to the current test in the report. This allows you to associate multiple test cases with the current test execution. ```java @TestId("aba4b142") - @LinkTest({"aba4b144", "aba4b144"}) + @LinkTest({"aba4b143", "aba4b144"}) @Test public void test() { // Your test code here } ``` -### 🥒 For Cucumber - -Use tags to identify your scenarios: +### For Cucumber ```gherkin Feature: User Authentication @@ -266,7 +266,7 @@ Feature: User Authentication ``` ### For Karate -Test ID format: ```@T + 8 alphanumeric characters.``` + ```gherkin Feature: Posts API @@ -318,11 +318,11 @@ Feature: Posts API ``` -## 📎 Test Artifacts Support +## Test Artifacts Support -The Java Reporter supports attaching files (screenshots, logs, videos, etc.) to your test results and uploading them to -S3-compatible storage. -Artifacts handling is enabled by default, but it won't affect the run if there are no artifacts provided (see options +The Java Reporter supports attaching files (screenshots, logs, videos etc.) to your test results and uploading them to +S3 compatible storage. +Artifacts handling is enabled by default but it won't affect the run if there are no artifacts provided (see options below). ### Configuration @@ -337,7 +337,7 @@ Artifacts are stored in external S3 buckets. S3 Access can be configured in **tw 2. Provide options as environment variables/jvm property/testomatio.properties file. -> NOTE: Environment variables(env/jvm/testomatio.properties) take precedence over server-provided credentials. +> NOTE: Environment variables(env/jvm/testomatio.properties) take precedence over server provided credentials. | Setting | Description | Default | |-------------------------------------|-------------------------------------------------------|-------------| @@ -370,7 +370,7 @@ public class MyTest { public void testWithScreenshot() { // Your test logic - // Attach artifacts (screenshots, logs, etc.) + // Attach artifacts (screenshots, logs etc.) Testomatio.artifact( "/path/to/screenshot.png", "/path/to/test.log" @@ -392,19 +392,78 @@ Karate Please make sure you provide the path to the artifact file including its extension. +### Using `@Artifact` + +This provides a declarative alternative to `Testomatio.artifact(...)`. + +Artifacts can also be attached automatically by annotating a method with `@Artifact`. + +Supported return types: + +- `String` – file path +- `Path` +- `File` + +Example: + +```java +import io.testomat.core.annotation.Artifact; + +@Artifact +public Path screenshot(Path file) { + return file; +} +``` + +When the annotated method completes successfully the returned file is automatically collected and uploaded as an artifact. + +`@Artifact` can be used: + +- At the test level – the artifact is attached to the current test +- Inside a `@Step` method or an active step the artifact is attached to the current step + +Example: + +```java +@Test +public void loginTest() { + Path screenshot = takeScreenshot(); + + attachTestomatScreenshot(screenshot); +} +``` + +Step level example: + +```java +@Step("Verify dashboard") +public void verifyDashboard() { + Path screenshot = takeScreenshot(); + + attachTestomatScreenshot(screenshot); +} + +@Artifact +public Path attachTestomatScreenshot(Path screenshot) { + return screenshot; +} +``` + +> **Note:** Methods annotated with `@Artifact` must return a valid file path (`String`, `Path` or `File`). Unsupported return types are ignored. + ### How It Works 1. **S3 Upload**: Files are uploaded to your S3 bucket with organized folder structure 2. **Link Generation**: Public URLs are generated and attached to test results 3. Artifacts are visible at the test info on UI -As a result, you will see something like this in the UI after the run is completed: +As a result you will see something like this in the UI after the run is completed: ![artifact example](./img/artifactExample.png) --- -## 📝 Step-by-Step Reporting +## Step-by-Step Reporting Track detailed test execution flow using the `@Step` annotation. Steps provide granular visibility into test logic and help identify exactly where tests succeed or fail. @@ -491,7 +550,7 @@ private void login(String username, String password) { } ``` -To enable named placeholders, add to `pom.xml`: +To enable named placeholders add to `pom.xml`: ```xml org.apache.maven.plugins @@ -509,7 +568,7 @@ To enable named placeholders, add to `pom.xml`: * def stepMarker = Java.type('io.testomat.karate.marker.StepMarker') * def step = stepMarker.mark ``` -After this, step() can be used as a regular Karate function. +After this step() can be used as a regular Karate function. #### Usage without a title ```gherkin * step() @@ -524,7 +583,7 @@ Log example: ```path 'posts'``` Log example: ``` Send get request``` #### Logging all steps with @LogSteps -If a scenario is annotated with the @LogSteps tag, all Karate steps in that scenario will be logged automatically. +If a scenario is annotated with the @LogSteps tag all Karate steps in that scenario will be logged automatically. #### Example ```gherkin @@ -584,6 +643,8 @@ If executed inside another step (including methods annotated with `@Step`) a sub Artifacts can be attached to a step using the `artifacts` attribute of the `@Step` annotation. +Methods annotated with `@Artifact` also support step-level attachment when executed inside a step. + ```java @Step(value = "Login", artifacts = {"path_to_artifact1", "path_to_artifact2"}) public void login() { @@ -599,9 +660,9 @@ You can also attach artifacts to a step programmatically: Testomatio.stepArtifact("path_to_attachment1", "path_to_attachment2"); ``` -If called inside a step, the artifacts will be attached to the current step. +If called inside a step the artifacts will be attached to the current step. -If called after a step finishes, it will be attached to the last completed step. +If called after a step finishes it will be attached to the last completed step. ### What You'll See @@ -614,13 +675,13 @@ This provides complete transparency into test flow and helps debug failures quic --- -## 🎯 Test Filtering by ID +## Test Filtering by ID **JUnit & TestNG only** > **Note**: >

Cucumber tests can be filtered using native Cucumber tags functionality (`@tag` in feature files and `cucumber.filter.tags` property). ->

Karate supports tagging of features and scenarios using the standard Gherkin tag syntax (@tag). Tags allow you to organize, group, and selectively run tests. +>

Karate supports tagging of features and scenarios using the standard Gherkin tag syntax (@tag). Tags allow you to organize, group and selectively run tests. Run specific tests by their `@TestId` values using the `-Dids` parameter. This is useful for: - Running smoke tests or critical path tests @@ -678,7 +739,7 @@ public class LoginTests { --- -## 💡 Library Usage Examples +## Common Usage Scenarios ### Basic Usage @@ -711,40 +772,40 @@ mvn test \ --- -## 📊 What You'll See +## What You'll See -When your tests start running, you'll see helpful output like this: +When your tests start running you'll see helpful output like this: ![console img](./img/console.png) **You get two types of links:** -- **🔒 Private Link**: Full access on Testomat.io platform (for your team) -- **🌐 Public Link**: Shareable read-only view (only if you set `testomatio.publish=1`) +- **Private Link**: Full access on Testomat.io platform (for your team) +- **Public Link**: Shareable read only view (only if you set `testomatio.publish=1`) -And the dashboard - something like this: +And the dashboard something like this: ![Description](./img/platform.png) --- -## Advanced customization +## Advanced Customization There are void hooks in the listeners that allow you to customize reporting much more. These hooks are located in the listeners' tests lifecycle methods according to their names. -External API calls, logging, and any custom logic can be added to the hooks. +External API calls, logging and any custom logic can be added to the hooks. The hooks are executed **after** the lifecycle method logic finishes and do not replace it. ### JUnit, TestNG 1. Complete the Simple Setup first -2. Create a new class that extends JunitListener or TestNgListener, based on your needs. +2. Create a new class that extends JunitListener or TestNgListener based on your needs. Implement protected methods from the library listener and add custom logic to them. 3. Create the `services` directory: ``` - 📁 src/main/resources/META-INF/services/ + src/main/resources/META-INF/services/ ``` 4. Create the right configuration file: @@ -804,7 +865,7 @@ The hooks are executed **after** the lifecycle method logic finishes and do not Testomat Allure Reporter is a Java integration library that bridges Allure reporting with Testomat.io test management system. -The library automatically captures test metadata, titles, steps, and attachments from Allure and sends them to Testomat.io, providing seamless synchronization between test execution and test management. +The library automatically captures test metadata, titles, steps and attachments from Allure and sends them to Testomat.io providing seamless synchronization between test execution and test management. Key features: - Automatic test title synchronization @@ -813,7 +874,7 @@ Key features: - JUnit and TestNG integration - Minimal configuration required -To enable Testomat Allure integration, add the following dependency: +To enable Testomat Allure integration add the following dependency: ```xml @@ -839,7 +900,7 @@ To enable Testomat Allure integration, add the following dependency: ``` -## 🆘 Troubleshooting +## Troubleshooting ### Tests not appearing in Testomat.io? @@ -852,7 +913,7 @@ To enable Testomat Allure integration, add the following dependency: 1. **JUnit 5**: Make sure `junit-platform.properties` exists with autodetection enabled 2. **Cucumber**: Verify the listener is in your `@CucumberOptions` plugins 3. **TestNG**: Should work automatically if nothing is overridden - check your TestNG version (need 7.x) -4. **Karate**: **Karate**: Verify that the KarateHookFactory is installed `.hookFactory(KarateHookFactory.create())` +4. **Karate**: Verify that the KarateHookFactory is installed `.hookFactory(KarateHookFactory.create())` --- @@ -860,4 +921,13 @@ To enable Testomat Allure integration, add the following dependency: 1. Create an issue. We'll fix it! -> 💝 **Love this tool?** Star the repo and share with your team! \ No newline at end of file +## Support + +If you find this project useful: + +- Star the repository +- Report issues +- Suggest improvements +- Share feedback + +We appreciate your support. \ No newline at end of file diff --git a/java-reporter-core/pom.xml b/java-reporter-core/pom.xml index 94f94df..97da840 100644 --- a/java-reporter-core/pom.xml +++ b/java-reporter-core/pom.xml @@ -7,7 +7,7 @@ io.testomat java-reporter-core - 0.13.1 + 0.14.0 jar Testomat.io Reporter Core @@ -104,6 +104,12 @@ 5.8.0 test + + commons-io + commons-io + 2.22.0 + compile + diff --git a/java-reporter-core/src/main/java/io/testomat/core/annotation/Artifact.java b/java-reporter-core/src/main/java/io/testomat/core/annotation/Artifact.java new file mode 100644 index 0000000..c2d8298 --- /dev/null +++ b/java-reporter-core/src/main/java/io/testomat/core/annotation/Artifact.java @@ -0,0 +1,11 @@ +package io.testomat.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Artifact { +} diff --git a/java-reporter-core/src/main/java/io/testomat/core/constants/CommonConstants.java b/java-reporter-core/src/main/java/io/testomat/core/constants/CommonConstants.java index 2cb21ef..9826cc7 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/constants/CommonConstants.java +++ b/java-reporter-core/src/main/java/io/testomat/core/constants/CommonConstants.java @@ -1,7 +1,7 @@ package io.testomat.core.constants; public class CommonConstants { - public static final String REPORTER_VERSION = "0.13.1"; + public static final String REPORTER_VERSION = "0.14.0"; public static final String TESTS_STRING = "tests"; public static final String API_KEY_STRING = "api_key"; diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/Testomatio.java b/java-reporter-core/src/main/java/io/testomat/core/facade/Testomatio.java index c970821..29d6f94 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/facade/Testomatio.java +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/Testomatio.java @@ -16,7 +16,6 @@ import java.io.StringWriter; import java.util.List; import java.util.Map; -import java.util.Optional; /** * Main public API facade for Testomat.io integration. @@ -45,10 +44,6 @@ public static void stepArtifact(String... directories) { } TestStep testStep = StepLifecycle.current(); - if (directories == null || directories.length == 0){ - return; - } - if (testStep == null) { testStep = StepLifecycle.lastFinished(); } @@ -81,8 +76,9 @@ public static void step(String stepName, Runnable action) { step.setStatus(StepStatus.failed); step.setLog(getStackTrace(t)); step.setError( - Optional.ofNullable(t.getMessage()) - .orElse(t.getClass().getSimpleName()) + t.getMessage() == null || t.getMessage().isBlank() + ? t.getClass().getSimpleName() + : t.getClass().getSimpleName() + ": " + t.getMessage() ); throwUnchecked(t); } finally { diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/ArtifactAspect.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/ArtifactAspect.java new file mode 100644 index 0000000..3c23f81 --- /dev/null +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/ArtifactAspect.java @@ -0,0 +1,57 @@ +package io.testomat.core.facade.methods.artifact; + +import io.testomat.core.facade.Testomatio; +import io.testomat.core.step.StepLifecycle; +import io.testomat.core.step.TestStep; +import java.io.File; +import java.nio.file.Path; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Aspect +public class ArtifactAspect { + private static final Logger log = LoggerFactory.getLogger(ArtifactAspect.class); + + @AfterReturning( + pointcut = "@annotation(io.testomat.core.annotation.Artifact)", + returning = "result" + ) + public void afterArtifact(Object result) { + String fileName = resolveFileName(result); + if (fileName == null) { + return; + } + + TestStep testStep = StepLifecycle.current(); + if (testStep == null) { + testStep = StepLifecycle.lastFinished(); + } + if (testStep == null || testStep.getId() == null) { + Testomatio.artifact(fileName); + } else { + Testomatio.stepArtifact(fileName); + } + } + + private String resolveFileName(Object result) { + if (result instanceof String) { + return (String) result; + } + if (result instanceof Path) { + return ((Path) result).toString(); + } + if (result instanceof File) { + return ((File) result).getAbsolutePath(); + } + + log.warn( + "@Artifact ignored: method returned unsupported type '{}'. " + + "Supported types are String, Path and File.", + result == null ? "null" : result.getClass().getName() + ); + + return null; + } +} diff --git a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/manager/ArtifactManager.java b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/manager/ArtifactManager.java index 5019a8f..008a413 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/manager/ArtifactManager.java +++ b/java-reporter-core/src/main/java/io/testomat/core/facade/methods/artifact/manager/ArtifactManager.java @@ -32,6 +32,9 @@ public void storeStepDirectories(UUID stepId, String... directories) { } private void store(String[] directories, Consumer storage, String logMessage) { + if (directories == null) { + return; + } for (String dir : directories) { if (!isValidFilePath(dir)) { diff --git a/java-reporter-core/src/main/java/io/testomat/core/step/StepAspect.java b/java-reporter-core/src/main/java/io/testomat/core/step/StepAspect.java index 1332944..82979c2 100644 --- a/java-reporter-core/src/main/java/io/testomat/core/step/StepAspect.java +++ b/java-reporter-core/src/main/java/io/testomat/core/step/StepAspect.java @@ -3,8 +3,9 @@ import static io.testomat.core.facade.Testomatio.stepArtifact; import io.testomat.core.annotation.Step; +import java.io.PrintWriter; +import java.io.StringWriter; import java.lang.reflect.Method; -import java.util.Arrays; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.*; import org.aspectj.lang.annotation.Aspect; @@ -95,10 +96,10 @@ public void afterSuccess(JoinPoint joinPoint) { * and completes the step lifecycle. * * @param joinPoint intercepted method invocation - * @param e thrown exception + * @param t thrown exception */ - @AfterThrowing(pointcut = "stepAnnotation()", throwing = "e") - public void afterFailure(JoinPoint joinPoint, Throwable e) { + @AfterThrowing(pointcut = "stepAnnotation()", throwing = "t") + public void afterFailure(JoinPoint joinPoint, Throwable t) { Step step = resolveStepAnnotation(joinPoint); TestStep testStep = StepLifecycle.current(); @@ -113,20 +114,24 @@ public void afterFailure(JoinPoint joinPoint, Throwable e) { testStep.setStatus(StepStatus.failed); testStep.setDuration(duration); - testStep.setError(e.getMessage()); - testStep.setLog(Arrays.toString(e.getStackTrace())); + testStep.setError( + t.getMessage() == null || t.getMessage().isBlank() + ? t.getClass().getSimpleName() + : t.getClass().getSimpleName() + ": " + t.getMessage() + ); + testStep.setLog(getStackTrace(t)); if (artifacts != null) { stepArtifact(artifacts); } - log.debug("Step '{}' failed in {} ms", stepName, duration, e); + log.debug("Step '{}' failed in {} ms", stepName, duration, t); StepLifecycle.finish(); } private long calculateDuration(String stepId) { - return System.currentTimeMillis() - StepTimer.stop(stepId); + return StepTimer.stop(stepId); } private Step resolveStepAnnotation(JoinPoint joinPoint) { @@ -215,4 +220,10 @@ private String substituteParameters(String stepName, JoinPoint joinPoint) { private String format(Object value) { return value == null ? "null" : value.toString(); } + + private static String getStackTrace(Throwable t) { + StringWriter sw = new StringWriter(); + t.printStackTrace(new PrintWriter(sw)); + return sw.toString(); + } } \ No newline at end of file diff --git a/java-reporter-core/src/main/resources/META-INF/aop.xml b/java-reporter-core/src/main/resources/META-INF/aop.xml index 6b93280..1bf6b9c 100644 --- a/java-reporter-core/src/main/resources/META-INF/aop.xml +++ b/java-reporter-core/src/main/resources/META-INF/aop.xml @@ -2,6 +2,7 @@ + diff --git a/java-reporter-core/src/test/java/io/testomat/core/artifact/ArtifactAspectTest.java b/java-reporter-core/src/test/java/io/testomat/core/artifact/ArtifactAspectTest.java new file mode 100644 index 0000000..dfab981 --- /dev/null +++ b/java-reporter-core/src/test/java/io/testomat/core/artifact/ArtifactAspectTest.java @@ -0,0 +1,138 @@ +package io.testomat.core.artifact; + +import io.testomat.core.facade.Testomatio; +import io.testomat.core.facade.methods.artifact.ArtifactAspect; +import io.testomat.core.step.StepLifecycle; +import io.testomat.core.step.TestStep; +import java.io.File; +import java.nio.file.Path; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import static org.mockito.Mockito.*; + +class ArtifactAspectTest { + + private final ArtifactAspect aspect = new ArtifactAspect(); + + @Test + void shouldSendStringArtifact() { + try (MockedStatic lifecycle = mockStatic(StepLifecycle.class); + MockedStatic testomatio = mockStatic(Testomatio.class)) { + + lifecycle.when(StepLifecycle::current).thenReturn(null); + lifecycle.when(StepLifecycle::lastFinished).thenReturn(null); + + aspect.afterArtifact("file.txt"); + + testomatio.verify(() -> Testomatio.artifact("file.txt")); + } + } + + @Test + void shouldSendPathArtifact() { + try (MockedStatic lifecycle = mockStatic(StepLifecycle.class); + MockedStatic testomatio = mockStatic(Testomatio.class)) { + + lifecycle.when(StepLifecycle::current).thenReturn(null); + lifecycle.when(StepLifecycle::lastFinished).thenReturn(null); + + Path path = Path.of("file.txt"); + + aspect.afterArtifact(path); + + testomatio.verify(() -> Testomatio.artifact(path.toString())); + } + } + + @Test + void shouldSendFileArtifact() { + try (MockedStatic lifecycle = mockStatic(StepLifecycle.class); + MockedStatic testomatio = mockStatic(Testomatio.class)) { + + lifecycle.when(StepLifecycle::current).thenReturn(null); + lifecycle.when(StepLifecycle::lastFinished).thenReturn(null); + + File file = new File("file.txt"); + + aspect.afterArtifact(file); + + testomatio.verify(() -> Testomatio.artifact(file.getAbsolutePath())); + } + } + + @Test + void shouldSendStepArtifactWhenCurrentStepExists() { + try (MockedStatic lifecycle = mockStatic(StepLifecycle.class); + MockedStatic testomatio = mockStatic(Testomatio.class)) { + + TestStep step = mock(TestStep.class); + + when(step.getId()).thenReturn(UUID.randomUUID()); + + lifecycle.when(StepLifecycle::current).thenReturn(step); + + aspect.afterArtifact("file.txt"); + + testomatio.verify(() -> Testomatio.stepArtifact("file.txt")); + } + } + + @Test + void shouldSendStepArtifactWhenLastFinishedStepExists() { + try (MockedStatic lifecycle = mockStatic(StepLifecycle.class); + MockedStatic testomatio = mockStatic(Testomatio.class)) { + + TestStep step = mock(TestStep.class); + + when(step.getId()).thenReturn(UUID.randomUUID()); + + lifecycle.when(StepLifecycle::current).thenReturn(null); + lifecycle.when(StepLifecycle::lastFinished).thenReturn(step); + + aspect.afterArtifact("file.txt"); + + testomatio.verify(() -> Testomatio.stepArtifact("file.txt")); + } + } + + @Test + void shouldSendArtifactWhenStepHasNullId() { + try (MockedStatic lifecycle = mockStatic(StepLifecycle.class); + MockedStatic testomatio = mockStatic(Testomatio.class)) { + + TestStep step = mock(TestStep.class); + + when(step.getId()).thenReturn(null); + + lifecycle.when(StepLifecycle::current).thenReturn(step); + + aspect.afterArtifact("file.txt"); + + testomatio.verify(() -> Testomatio.artifact("file.txt")); + } + } + + @Test + void shouldIgnoreUnsupportedType() { + try (MockedStatic lifecycle = mockStatic(StepLifecycle.class); + MockedStatic testomatio = mockStatic(Testomatio.class)) { + + aspect.afterArtifact(new Object()); + + testomatio.verifyNoInteractions(); + } + } + + @Test + void shouldIgnoreNull() { + try (MockedStatic lifecycle = mockStatic(StepLifecycle.class); + MockedStatic testomatio = mockStatic(Testomatio.class)) { + + aspect.afterArtifact(null); + + testomatio.verifyNoInteractions(); + } + } +} \ No newline at end of file diff --git a/java-reporter-core/src/test/java/io/testomat/core/step/StepAspectTest.java b/java-reporter-core/src/test/java/io/testomat/core/step/StepAspectTest.java index 16b6171..6be43ff 100644 --- a/java-reporter-core/src/test/java/io/testomat/core/step/StepAspectTest.java +++ b/java-reporter-core/src/test/java/io/testomat/core/step/StepAspectTest.java @@ -215,6 +215,11 @@ private void stepThatThrows() { throw new RuntimeException("Test exception"); } + @Step("Failing step") + void stepThatThrowsWithoutMessage() { + throw new RuntimeException(); + } + @Step("Process user: {0}") private void stepWithComplexObject(User user) { log.info("Processing user: {}", user); @@ -244,10 +249,17 @@ void testFailureStatus(){ } @Test - void testErrorMessageStored(){ + void testErrorMessageStored() { assertThrows(RuntimeException.class, this::stepThatThrows); - TestStep step= StepStorage.getSteps().get(0); - assertEquals("Test exception", step.getError()); + TestStep step = StepStorage.getSteps().get(0); + assertTrue(step.getError().contains("Test exception")); + } + + @Test + void shouldStoreExceptionClassWhenMessageIsNull() { + assertThrows(RuntimeException.class, this::stepThatThrowsWithoutMessage); + TestStep step = StepStorage.getSteps().get(0); + assertEquals("RuntimeException", step.getError()); } @Test diff --git a/java-reporter-cucumber/pom.xml b/java-reporter-cucumber/pom.xml index 17601a5..8cca9f0 100644 --- a/java-reporter-cucumber/pom.xml +++ b/java-reporter-cucumber/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-cucumber - 0.7.17 + 0.7.18 jar Testomat.io Java Reporter Cucumber @@ -51,7 +51,7 @@ io.testomat java-reporter-core - 0.13.1 + 0.14.0 org.slf4j diff --git a/java-reporter-junit/pom.xml b/java-reporter-junit/pom.xml index 1a00a3b..3c9ff99 100644 --- a/java-reporter-junit/pom.xml +++ b/java-reporter-junit/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-junit - 0.8.9 + 0.8.10 jar Testomat.io Java Reporter JUnit @@ -51,7 +51,7 @@ io.testomat java-reporter-core - 0.13.1 + 0.14.0 org.slf4j diff --git a/java-reporter-karate/pom.xml b/java-reporter-karate/pom.xml index d603e8f..46e8026 100644 --- a/java-reporter-karate/pom.xml +++ b/java-reporter-karate/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-karate - 0.2.11 + 0.2.12 jar Testomat.io Java Reporter Karate @@ -52,7 +52,7 @@ io.testomat java-reporter-core - 0.13.1 + 0.14.0 io.karatelabs diff --git a/java-reporter-testng/pom.xml b/java-reporter-testng/pom.xml index f47d10b..b25d1f8 100644 --- a/java-reporter-testng/pom.xml +++ b/java-reporter-testng/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter-testng - 0.7.16 + 0.7.17 jar Testomat.io Java Reporter TestNG @@ -47,7 +47,7 @@ io.testomat java-reporter-core - 0.13.1 + 0.14.0 org.slf4j diff --git a/pom.xml b/pom.xml index beda0a9..6079a9c 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.testomat java-reporter - 0.3.5 + 0.3.6 pom Testomat.io Java Reporter diff --git a/testomat-allure-adapter/pom.xml b/testomat-allure-adapter/pom.xml index 1685fc7..81fc96a 100644 --- a/testomat-allure-adapter/pom.xml +++ b/testomat-allure-adapter/pom.xml @@ -6,7 +6,7 @@ io.testomat testomat-allure-adapter - 0.0.7 + 0.1.0 jar Testomat.io Testomat Allure adapter @@ -43,6 +43,7 @@ 7.12.0 3.27.7 2.0.17 + 3.3.1 3.11.0 3.3.0 @@ -66,7 +67,7 @@ io.testomat java-reporter-core - 0.13.1 + 0.14.0 io.qameta.allure @@ -126,6 +127,11 @@ slf4j-api ${slf4j.api.version} + + org.apache.tika + tika-core + ${tika.core.version} + diff --git a/testomat-allure-adapter/src/main/java/io/testomat/aspect/AllureAttachmentAspect.java b/testomat-allure-adapter/src/main/java/io/testomat/aspect/AllureAttachmentAspect.java index ce83c45..89abde2 100644 --- a/testomat-allure-adapter/src/main/java/io/testomat/aspect/AllureAttachmentAspect.java +++ b/testomat-allure-adapter/src/main/java/io/testomat/aspect/AllureAttachmentAspect.java @@ -7,9 +7,12 @@ import io.testomat.testomat.TestomatClient; import io.testomat.testomat.TestomatClientImpl; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import org.apache.tika.mime.MimeTypes; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -26,7 +29,6 @@ public class AllureAttachmentAspect { private static final Logger log = LoggerFactory.getLogger(AllureAttachmentAspect.class); private static final Map attachments = new ConcurrentHashMap<>(); - private static final ThreadLocal userAttachment = ThreadLocal.withInitial(() -> false); private final AllureClient allure; private final TestomatClient testomatio; private final AttachmentFileResolver resolver; @@ -45,27 +47,12 @@ public AllureAttachmentAspect(AllureClient allure, } - /** Marks user attachments created via Allure API. */ - @Around("execution(* io.qameta.allure.Allure.addAttachment(..))") - public Object interceptUserAttachment(ProceedingJoinPoint joinPoint) throws Throwable { - userAttachment.set(true); - try { - return joinPoint.proceed(); - } finally { - userAttachment.remove(); - } - } - /** Collects attachment metadata during preparation phase. */ @Around("execution(* io.qameta.allure.AllureLifecycle.prepareAttachment(..))") public Object interceptPrepare(ProceedingJoinPoint joinPoint) throws Throwable { Object result = joinPoint.proceed(); String uuid = (String) result; - if (!userAttachment.get()) { - return result; - } - AttachmentMeta meta = attachments.computeIfAbsent(uuid, k -> new AttachmentMeta()); Object[] args = joinPoint.getArgs(); @@ -129,10 +116,11 @@ private Nodes resolveLevel(AttachmentMeta meta) { /** Sends attachment to Testomat. */ private void sendToTestomat(AttachmentMeta meta) { + String filePath = addExtension(meta.path, meta.type); if (meta.level.equals(Nodes.step.name())) { - testomatio.stepArtifact(meta.path); + testomatio.stepArtifact(filePath); } else if (meta.level.equals(Nodes.test.name())) { - testomatio.artifact(meta.path); + testomatio.artifact(filePath); } log.debug("===== TESTOMAT ATTACHMENT ====="); @@ -147,6 +135,41 @@ private void sendToTestomat(AttachmentMeta meta) { log.debug("=============================="); } + private String addExtension(String fileName, String mimeType) { + try { + String extension = MimeTypes.getDefaultMimeTypes() + .forName(mimeType) + .getExtension(); + + if (fileName.endsWith(extension)) { + return fileName; + } + + Path source = Path.of(fileName); + Path target = Path.of(fileName + extension); + + if (!Files.exists(source)) { + log.debug("Attachment file not found: {}", source); + return fileName; + } + + if (Files.exists(target)) { + log.debug("Attachment copy already exists: {}", target); + return target.toString(); + } + + Files.copy(source, target); + + log.debug("Created attachment copy with extension: {} -> {}", source, target); + + return target.toString(); + + } catch (Exception e) { + log.debug("Failed to add extension '{}' to attachment '{}'", mimeType, fileName, e); + return fileName; + } + } + static class AttachmentMeta { private String uuid; private String testUuid; diff --git a/testomat-allure-adapter/src/main/java/io/testomat/aspect/AllureTmsLinkAspect.java b/testomat-allure-adapter/src/main/java/io/testomat/aspect/AllureTmsLinkAspect.java new file mode 100644 index 0000000..602fb30 --- /dev/null +++ b/testomat-allure-adapter/src/main/java/io/testomat/aspect/AllureTmsLinkAspect.java @@ -0,0 +1,32 @@ +package io.testomat.aspect; + +import io.testomat.resolver.AllureTmsResolver; +import java.lang.reflect.Method; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +@Aspect +public class AllureTmsLinkAspect { + + private final AllureTmsResolver resolver; + + public AllureTmsLinkAspect() { + this(new AllureTmsResolver()); + } + + public AllureTmsLinkAspect(AllureTmsResolver resolver) { + this.resolver = resolver; + } + + @Around("execution(String io.testomat.*.extractor..*.*TestId(java.lang.reflect.Method))") + public Object intercept(ProceedingJoinPoint pjp) throws Throwable { + String result = (String) pjp.proceed(); + if (result != null && !result.isBlank()) { + return result; + } + Method method = (Method) pjp.getArgs()[0]; + String tmsId = resolver.resolve(method); + return tmsId != null ? tmsId : result; + } +} diff --git a/testomat-allure-adapter/src/main/java/io/testomat/resolver/AllureTmsResolver.java b/testomat-allure-adapter/src/main/java/io/testomat/resolver/AllureTmsResolver.java new file mode 100644 index 0000000..537ae8a --- /dev/null +++ b/testomat-allure-adapter/src/main/java/io/testomat/resolver/AllureTmsResolver.java @@ -0,0 +1,16 @@ +package io.testomat.resolver; + +import io.qameta.allure.TmsLink; +import java.lang.reflect.Method; + +public class AllureTmsResolver implements TestMetadataResolver { + + @Override + public String resolve(Method method) { + TmsLink tmsLink = method.getAnnotation(TmsLink.class); + if (tmsLink != null) { + return tmsLink.value(); + } + return null; + } +} diff --git a/testomat-allure-adapter/src/main/resources/META-INF/aop.xml b/testomat-allure-adapter/src/main/resources/META-INF/aop.xml index 941936f..57783b4 100644 --- a/testomat-allure-adapter/src/main/resources/META-INF/aop.xml +++ b/testomat-allure-adapter/src/main/resources/META-INF/aop.xml @@ -3,6 +3,7 @@ + diff --git a/testomat-allure-adapter/src/test/java/aspect/AllureAttachmentAspectTest.java b/testomat-allure-adapter/src/test/java/aspect/AllureAttachmentAspectTest.java index 1619dcf..ddccad6 100644 --- a/testomat-allure-adapter/src/test/java/aspect/AllureAttachmentAspectTest.java +++ b/testomat-allure-adapter/src/test/java/aspect/AllureAttachmentAspectTest.java @@ -4,6 +4,9 @@ import io.testomat.aspect.AllureAttachmentAspect; import io.testomat.resolver.AttachmentFileResolver; import io.testomat.testomat.TestomatClient; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; import org.aspectj.lang.ProceedingJoinPoint; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -36,22 +39,6 @@ void clearStaticState() throws Exception { Field attachments = AllureAttachmentAspect.class.getDeclaredField("attachments"); attachments.setAccessible(true); ((Map) attachments.get(null)).clear(); - - Field threadLocal = AllureAttachmentAspect.class.getDeclaredField("userAttachment"); - threadLocal.setAccessible(true); - ThreadLocal tl = (ThreadLocal) threadLocal.get(null); - tl.remove(); - } - - @Test - void shouldInterceptUserAttachment() throws Throwable { - ProceedingJoinPoint joinPoint = mock(ProceedingJoinPoint.class); - - when(joinPoint.proceed()).thenReturn("ok"); - Object result = aspect.interceptUserAttachment(joinPoint); - - assertThat(result).isEqualTo("ok"); - verify(joinPoint).proceed(); } @Test @@ -63,7 +50,6 @@ void shouldCreateTestLevelMeta() throws Throwable { when(joinPoint.getArgs()).thenReturn(new Object[]{"file", "text/plain"}); when(joinPoint.proceed()).thenReturn("uuid"); - aspect.interceptUserAttachment(joinPoint); Object result = aspect.interceptPrepare(joinPoint); assertThat(result).isEqualTo("uuid"); @@ -78,7 +64,6 @@ void shouldCreateStepLevelMeta() throws Throwable { when(joinPoint.getArgs()).thenReturn(new Object[]{"file", "text/plain"}); when(joinPoint.proceed()).thenReturn("uuid"); - aspect.interceptUserAttachment(joinPoint); aspect.interceptPrepare(joinPoint); } @@ -91,18 +76,12 @@ void shouldCreateFixtureLevelMeta() throws Throwable { when(joinPoint.getArgs()).thenReturn(new Object[]{"file", "text/plain"}); when(joinPoint.proceed()).thenReturn("uuid"); - aspect.interceptUserAttachment(joinPoint); aspect.interceptPrepare(joinPoint); } @Test void shouldHandleByteAttachment() throws Throwable { String uuid = "uuid"; - Field field = AllureAttachmentAspect.class.getDeclaredField("userAttachment"); - field.setAccessible(true); - ThreadLocal tl = (ThreadLocal) field.get(null); - tl.set(true); - ProceedingJoinPoint prepare = mock(ProceedingJoinPoint.class); when(allure.getCurrentTest()).thenReturn(Optional.of("test")); @@ -118,18 +97,11 @@ void shouldHandleByteAttachment() throws Throwable { verify(resolver).find(uuid); verify(testomatio).artifact("file.txt"); - - tl.remove(); } @Test void shouldHandleStreamAttachment() throws Throwable { String uuid = "uuid"; - Field field = AllureAttachmentAspect.class.getDeclaredField("userAttachment"); - field.setAccessible(true); - ThreadLocal tl = (ThreadLocal) field.get(null); - tl.set(true); - ProceedingJoinPoint prepare = mock(ProceedingJoinPoint.class); when(allure.getCurrentTest()).thenReturn(Optional.of("test")); @@ -144,8 +116,6 @@ void shouldHandleStreamAttachment() throws Throwable { aspect.interceptWrite(write); verify(testomatio).stepArtifact("file.txt"); - - tl.remove(); } @Test @@ -160,4 +130,137 @@ void shouldIgnoreUnknownAttachment() throws Throwable { verifyNoInteractions(testomatio); } + @Test + void shouldNotSendFixtureAttachment() throws Throwable { + String uuid = "uuid"; + + ProceedingJoinPoint prepare = mock(ProceedingJoinPoint.class); + when(allure.getCurrentTest()).thenReturn(Optional.empty()); + when(allure.getCurrentTestOrStep()).thenReturn(Optional.empty()); + when(prepare.getArgs()).thenReturn(new Object[]{"file", "text/plain"}); + when(prepare.proceed()).thenReturn(uuid); + + aspect.interceptPrepare(prepare); + + ProceedingJoinPoint write = mock(ProceedingJoinPoint.class); + when(write.getArgs()).thenReturn(new Object[]{uuid, "data".getBytes()}); + when(write.proceed()).thenReturn(null); + when(resolver.find(uuid)).thenReturn("file.txt"); + + aspect.interceptWrite(write); + + verifyNoInteractions(testomatio); + } + + @Test + void shouldRemoveAttachmentAfterWrite() throws Throwable { + String uuid = "uuid"; + + ProceedingJoinPoint prepare = mock(ProceedingJoinPoint.class); + + when(allure.getCurrentTest()).thenReturn(Optional.of("test")); + when(allure.getCurrentTestOrStep()).thenReturn(Optional.of("test")); + when(prepare.getArgs()).thenReturn(new Object[]{"file", "text/plain"}); + when(prepare.proceed()).thenReturn(uuid); + + aspect.interceptPrepare(prepare); + + ProceedingJoinPoint write = mock(ProceedingJoinPoint.class); + when(write.getArgs()).thenReturn(new Object[]{uuid, "data".getBytes()}); + when(write.proceed()).thenReturn(null); + when(resolver.find(uuid)).thenReturn("file.txt"); + + aspect.interceptWrite(write); + + Field field = AllureAttachmentAspect.class.getDeclaredField("attachments"); + field.setAccessible(true); + + Map attachments = (Map) field.get(null); + + assertThat(attachments.containsKey(uuid)).isFalse(); + } + + @Test + void shouldReturnProceedResultFromWrite() throws Throwable { + String uuid = "uuid"; + + ProceedingJoinPoint prepare = mock(ProceedingJoinPoint.class); + + when(allure.getCurrentTest()).thenReturn(Optional.of("test")); + when(allure.getCurrentTestOrStep()).thenReturn(Optional.of("test")); + when(prepare.getArgs()).thenReturn(new Object[]{"file", "text/plain"}); + when(prepare.proceed()).thenReturn(uuid); + + aspect.interceptPrepare(prepare); + + ProceedingJoinPoint write = mock(ProceedingJoinPoint.class); + Object expected = new Object(); + + when(write.getArgs()).thenReturn(new Object[]{uuid, "data".getBytes()}); + when(write.proceed()).thenReturn(expected); + when(resolver.find(uuid)).thenReturn("file.txt"); + + Object result = aspect.interceptWrite(write); + + assertThat(result).isSameAs(expected); + } + + @Test + void shouldNotResolveUnknownAttachment() throws Throwable { + ProceedingJoinPoint write = mock(ProceedingJoinPoint.class); + + when(write.getArgs()).thenReturn(new Object[]{"unknown", "data".getBytes()}); + when(write.proceed()).thenReturn(null); + + aspect.interceptWrite(write); + + verifyNoInteractions(resolver); + verifyNoInteractions(testomatio); + } + + @Test + void shouldReturnOriginalFileWhenMimeTypeIsInvalid() throws Exception { + String result = invokeAddExtension("file", "invalid/type"); + + assertThat(result).isEqualTo("file"); + } + + @Test + void shouldReturnOriginalFileWhenSourceDoesNotExist() throws Exception { + String file = "not-existing-file"; + + String result = invokeAddExtension(file, "text/plain"); + + assertThat(result).isEqualTo(file); + } + + @Test + void shouldNotAddExtensionWhenAlreadyPresent() throws Exception { + String result = invokeAddExtension("file.txt", "text/plain"); + + assertThat(result).isEqualTo("file.txt"); + } + + @Test + void shouldCreateCopyWithExtension() throws Exception { + Path source = Files.createTempFile("attachment", ""); + + String result = invokeAddExtension(source.toString(), "text/plain"); + + assertThat(result).endsWith(".txt"); + assertThat(Files.exists(Path.of(result))).isTrue(); + + Files.deleteIfExists(source); + Files.deleteIfExists(Path.of(result)); + } + + private String invokeAddExtension(String fileName, String mimeType) throws Exception { + Method method = AllureAttachmentAspect.class + .getDeclaredMethod("addExtension", String.class, String.class); + + method.setAccessible(true); + + return (String) method.invoke(aspect, fileName, mimeType); + } + } \ No newline at end of file diff --git a/testomat-allure-adapter/src/test/java/aspect/AllureTmsLinkAspectTest.java b/testomat-allure-adapter/src/test/java/aspect/AllureTmsLinkAspectTest.java new file mode 100644 index 0000000..0f2148d --- /dev/null +++ b/testomat-allure-adapter/src/test/java/aspect/AllureTmsLinkAspectTest.java @@ -0,0 +1,114 @@ +package aspect; + +import io.testomat.aspect.AllureTmsLinkAspect; +import io.testomat.resolver.AllureTmsResolver; +import java.lang.reflect.Method; +import org.aspectj.lang.ProceedingJoinPoint; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +class AllureTmsLinkAspectTest { + + private AllureTmsResolver resolver; + private AllureTmsLinkAspect aspect; + + @BeforeEach + void setup() { + resolver = mock(AllureTmsResolver.class); + aspect = new AllureTmsLinkAspect(resolver); + } + + static class TestClass { + void testMethod() { + } + } + + @Test + void shouldReturnExistingTestId() throws Throwable { + ProceedingJoinPoint pjp = mock(ProceedingJoinPoint.class); + + when(pjp.proceed()).thenReturn("TMS-123"); + + Object result = aspect.intercept(pjp); + + assertThat(result).isEqualTo("TMS-123"); + + verify(pjp).proceed(); + verifyNoInteractions(resolver); + } + + @Test + void shouldResolveTestIdWhenExtractorReturnsNull() throws Throwable { + Method method = TestClass.class.getDeclaredMethod("testMethod"); + + ProceedingJoinPoint pjp = mock(ProceedingJoinPoint.class); + + when(pjp.proceed()).thenReturn(null); + when(pjp.getArgs()).thenReturn(new Object[]{method}); + when(resolver.resolve(method)).thenReturn("TMS-456"); + + Object result = aspect.intercept(pjp); + + assertThat(result).isEqualTo("TMS-456"); + + verify(pjp).proceed(); + verify(resolver).resolve(method); + } + + @Test + void shouldResolveTestIdWhenExtractorReturnsBlank() throws Throwable { + Method method = TestClass.class.getDeclaredMethod("testMethod"); + + ProceedingJoinPoint pjp = mock(ProceedingJoinPoint.class); + + when(pjp.proceed()).thenReturn(" "); + when(pjp.getArgs()).thenReturn(new Object[]{method}); + when(resolver.resolve(method)).thenReturn("TMS-789"); + + Object result = aspect.intercept(pjp); + + assertThat(result).isEqualTo("TMS-789"); + + verify(pjp).proceed(); + verify(resolver).resolve(method); + } + + @Test + void shouldReturnNullWhenNothingResolved() throws Throwable { + Method method = TestClass.class.getDeclaredMethod("testMethod"); + + ProceedingJoinPoint pjp = mock(ProceedingJoinPoint.class); + + when(pjp.proceed()).thenReturn(null); + when(pjp.getArgs()).thenReturn(new Object[]{method}); + when(resolver.resolve(method)).thenReturn(null); + + Object result = aspect.intercept(pjp); + + assertThat(result).isNull(); + + verify(pjp).proceed(); + verify(resolver).resolve(method); + } + + @Test + void shouldReturnBlankWhenResolverReturnsNull() throws Throwable { + Method method = TestClass.class.getDeclaredMethod("testMethod"); + + ProceedingJoinPoint pjp = mock(ProceedingJoinPoint.class); + + when(pjp.proceed()).thenReturn(""); + when(pjp.getArgs()).thenReturn(new Object[]{method}); + when(resolver.resolve(method)).thenReturn(null); + + Object result = aspect.intercept(pjp); + + assertThat(result).isEqualTo(""); + + verify(pjp).proceed(); + verify(resolver).resolve(method); + } +} \ No newline at end of file diff --git a/testomat-allure-adapter/src/test/java/resolver/AllureTmsResolverTest.java b/testomat-allure-adapter/src/test/java/resolver/AllureTmsResolverTest.java new file mode 100644 index 0000000..c4d1cc6 --- /dev/null +++ b/testomat-allure-adapter/src/test/java/resolver/AllureTmsResolverTest.java @@ -0,0 +1,47 @@ +package resolver; + +import io.qameta.allure.TmsLink; +import io.testomat.resolver.AllureTmsResolver; +import java.lang.reflect.Method; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class AllureTmsResolverTest { + + private AllureTmsResolver resolver; + + @BeforeEach + void setup() { + resolver = new AllureTmsResolver(); + } + + static class TestClass { + + @TmsLink("TMS-123") + void methodWithTmsLink() { + } + + void methodWithoutTmsLink() { + } + } + + @Test + void shouldReturnTmsLinkValue() throws Exception { + Method method = TestClass.class.getDeclaredMethod("methodWithTmsLink"); + + String result = resolver.resolve(method); + + assertThat(result).isEqualTo("TMS-123"); + } + + @Test + void shouldReturnNullWhenTmsLinkIsAbsent() throws Exception { + Method method = TestClass.class.getDeclaredMethod("methodWithoutTmsLink"); + + String result = resolver.resolve(method); + + assertThat(result).isNull(); + } +} \ No newline at end of file From 71f080ca38644193efef8c5c3a6918ff2c6e0b36 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 15 Jun 2026 17:27:37 +0300 Subject: [PATCH 2/2] Issue-113. refactor: refactor README.MD --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 39d752e..c10afad 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,53 @@ Run tests with JUnit, TestNG, Cucumber, Karate and automatically publish results ## Quick Start -### 1. Add the Reporter Dependency +### 1. Install Reporter Add the Testomat reporter dependency that matches your test framework. Detailed setup instructions for JUnit, TestNG, Cucumber, and Karate are provided below. +### JUnit 5 + +```xml + + io.testomat + java-reporter-junit + + +``` + +### TestNG + +```xml + + io.testomat + java-reporter-testng + + +``` + +### Cucumber + +```xml + + io.testomat + java-reporter-cucumber + + +``` + +### Karate + +```xml + + io.testomat + java-reporter-karate + + +``` +Choose only one dependency corresponding to your test framework. + +After adding the dependency continue with the framework-specific setup instructions below. + ### 2. Configure API Key Using environment variables: @@ -108,7 +151,7 @@ Register the listener: Register the hook factory: ```java -.hookFactory(new KarateHookFactory()) +.hookFactory(KarateHookFactory.create()) ``` ---