diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml
new file mode 100644
index 0000000..805ea69
--- /dev/null
+++ b/.github/workflows/java.yml
@@ -0,0 +1,66 @@
+name: Java Tests
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ workflow_dispatch:
+
+defaults:
+ run:
+ working-directory: java-example
+
+jobs:
+ test:
+ name: JUnit 5 + JaCoCo Coverage
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ java-version: "17"
+ distribution: "temurin"
+ cache: "maven"
+
+ - name: Run tests with coverage
+ run: mvn clean verify -Dmaven.test.failure.ignore=true
+
+ # Upload JUnit test results to Gaffer
+ # Skip on Dependabot PRs (no access to secrets)
+ - name: Upload JUnit results to Gaffer
+ if: always() && github.actor != 'dependabot[bot]'
+ uses: gaffer-sh/gaffer-uploader@v0.4.0
+ with:
+ gaffer_upload_token: ${{ secrets.GAFFER_UPLOAD_TOKEN }}
+ report_path: java-example/target/surefire-reports/
+ commit_sha: ${{ github.event.pull_request.head.sha || github.sha }}
+ branch: ${{ github.head_ref || github.ref_name }}
+ test_framework: junit
+ test_suite: junit-results
+
+ # Upload JaCoCo coverage to Gaffer
+ - name: Upload JaCoCo coverage to Gaffer
+ if: always() && github.actor != 'dependabot[bot]'
+ uses: gaffer-sh/gaffer-uploader@v0.4.0
+ with:
+ gaffer_upload_token: ${{ secrets.GAFFER_UPLOAD_TOKEN }}
+ report_path: java-example/target/site/jacoco/jacoco.xml
+ commit_sha: ${{ github.event.pull_request.head.sha || github.sha }}
+ branch: ${{ github.head_ref || github.ref_name }}
+ test_framework: junit
+ test_suite: jacoco-coverage
+
+ # Store artifacts for parser development
+ - name: Upload test artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: java-reports-${{ github.sha }}
+ path: |
+ java-example/target/surefire-reports/
+ java-example/target/site/jacoco/
+ retention-days: 7
diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
new file mode 100644
index 0000000..acdc1c7
--- /dev/null
+++ b/.github/workflows/phpunit.yml
@@ -0,0 +1,86 @@
+name: PHPUnit Tests
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ workflow_dispatch:
+
+defaults:
+ run:
+ working-directory: php-example
+
+jobs:
+ test:
+ name: PHPUnit + Clover Coverage
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: "8.2"
+ coverage: pcov
+ tools: composer:v2
+
+ - name: Get Composer cache directory
+ id: composer-cache
+ run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+
+ - name: Cache Composer dependencies
+ uses: actions/cache@v4
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-composer-${{ hashFiles('php-example/composer.json') }}
+ restore-keys: ${{ runner.os }}-composer-
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress
+
+ - name: Create reports directory
+ run: mkdir -p reports
+
+ - name: Run tests with coverage
+ run: |
+ vendor/bin/phpunit \
+ --coverage-clover reports/clover.xml \
+ --coverage-html reports/htmlcov \
+ --log-junit reports/phpunit-results.xml
+ continue-on-error: true
+
+ # Upload PHPUnit test results to Gaffer
+ # Skip on Dependabot PRs (no access to secrets)
+ - name: Upload PHPUnit results to Gaffer
+ if: always() && github.actor != 'dependabot[bot]'
+ uses: gaffer-sh/gaffer-uploader@v0.4.0
+ with:
+ gaffer_upload_token: ${{ secrets.GAFFER_UPLOAD_TOKEN }}
+ report_path: php-example/reports/phpunit-results.xml
+ commit_sha: ${{ github.event.pull_request.head.sha || github.sha }}
+ branch: ${{ github.head_ref || github.ref_name }}
+ test_framework: phpunit
+ test_suite: phpunit-results
+
+ # Upload Clover coverage to Gaffer
+ - name: Upload Clover coverage to Gaffer
+ if: always() && github.actor != 'dependabot[bot]'
+ uses: gaffer-sh/gaffer-uploader@v0.4.0
+ with:
+ gaffer_upload_token: ${{ secrets.GAFFER_UPLOAD_TOKEN }}
+ report_path: php-example/reports/clover.xml
+ commit_sha: ${{ github.event.pull_request.head.sha || github.sha }}
+ branch: ${{ github.head_ref || github.ref_name }}
+ test_framework: phpunit
+ test_suite: phpunit-coverage
+
+ # Store artifacts for parser development
+ - name: Upload test artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: phpunit-reports-${{ github.sha }}
+ path: php-example/reports/
+ retention-days: 7
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index eb96f7c..9f6b630 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -32,8 +32,14 @@ jobs:
- name: Create reports directory
run: mkdir -p reports
- - name: Run tests with HTML output
- run: pytest --html=reports/pytest-report.html --self-contained-html
+ - name: Run tests with HTML output and coverage
+ run: |
+ pytest \
+ --html=reports/pytest-report.html \
+ --self-contained-html \
+ --cov=src \
+ --cov-report=xml:reports/coverage.xml \
+ --cov-report=html:reports/htmlcov
continue-on-error: true
# Upload via GitHub Action (recommended)
@@ -49,6 +55,18 @@ jobs:
test_framework: pytest
test_suite: pytest-html
+ # Upload Cobertura coverage report
+ - name: Upload coverage to Gaffer
+ if: always() && github.actor != 'dependabot[bot]'
+ uses: gaffer-sh/gaffer-uploader@v0.4.0
+ with:
+ gaffer_upload_token: ${{ secrets.GAFFER_UPLOAD_TOKEN }}
+ report_path: pytest-example/reports/coverage.xml
+ commit_sha: ${{ github.event.pull_request.head.sha || github.sha }}
+ branch: ${{ github.head_ref || github.ref_name }}
+ test_framework: pytest
+ test_suite: pytest-coverage
+
# Alternative: Upload via curl (for documentation/reference)
# - name: Upload via curl (example)
# if: always() && github.actor != 'dependabot[bot]'
@@ -71,4 +89,4 @@ jobs:
with:
name: pytest-reports-${{ github.sha }}
path: pytest-example/reports/
- retention-days: 30
+ retention-days: 7
diff --git a/README.md b/README.md
index db4ddbe..b30a63b 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
Example test projects demonstrating [Gaffer](https://gaffer.sh) integration for various test frameworks.
-Parser Check: December 25 2025 - 1:10PM - Merry Christmas
+Parser Check: January 22 2026 - 8:16am
## Examples
diff --git a/java-example/.gitignore b/java-example/.gitignore
new file mode 100644
index 0000000..dfb3f40
--- /dev/null
+++ b/java-example/.gitignore
@@ -0,0 +1,22 @@
+# Maven
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
+
+# IDE
+.idea/
+*.iml
+*.ipr
+*.iws
+.project
+.classpath
+.settings/
+.vscode/
+*.swp
diff --git a/java-example/README.md b/java-example/README.md
new file mode 100644
index 0000000..696ee07
--- /dev/null
+++ b/java-example/README.md
@@ -0,0 +1,46 @@
+# Java Example (JUnit 5 + JaCoCo Coverage)
+
+This example demonstrates how to integrate Gaffer with JUnit 5 test reports and JaCoCo coverage reports.
+
+## Overview
+
+This project uses:
+- **JUnit 5** (Jupiter) for testing
+- **JaCoCo** for code coverage
+- **Maven** as the build tool
+
+## Requirements
+
+- Java 17+
+- Maven 3.8+
+
+## Local Development
+
+```bash
+# Run tests with coverage
+mvn clean test
+
+# View coverage report
+open target/site/jacoco/index.html
+```
+
+## Test Reports
+
+After running tests, reports are generated in:
+- `target/surefire-reports/*.xml` - JUnit XML test results
+- `target/site/jacoco/jacoco.xml` - JaCoCo coverage XML
+- `target/site/jacoco/index.html` - HTML coverage report
+
+## CI Integration
+
+The GitHub Actions workflow (`.github/workflows/java.yml`):
+1. Runs Maven test with JaCoCo coverage
+2. Generates JUnit test results and JaCoCo coverage
+3. Uploads both reports to Gaffer
+
+## Coverage Formats
+
+JaCoCo generates multiple formats:
+- **JaCoCo XML** - Parsed by Gaffer for coverage metrics
+- **JaCoCo CSV** - Machine-readable summary
+- **HTML** - Human-readable coverage browser
diff --git a/java-example/pom.xml b/java-example/pom.xml
new file mode 100644
index 0000000..e7c3115
--- /dev/null
+++ b/java-example/pom.xml
@@ -0,0 +1,85 @@
+
+
+ 4.0.0
+
+ com.example
+ gaffer-java-example
+ 1.0.0
+ jar
+
+ Gaffer Java Example
+ Gaffer JUnit 5 example with JaCoCo coverage reporting
+
+
+ 17
+ 17
+ UTF-8
+ 5.10.2
+ 0.8.12
+
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.12.1
+
+ 17
+ 17
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+ ${project.build.directory}/surefire-reports
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ ${jacoco.version}
+
+
+
+ prepare-agent
+
+ prepare-agent
+
+
+
+
+ report
+ verify
+
+ report
+
+
+ ${project.build.directory}/site/jacoco
+
+
+
+
+
+
+
diff --git a/java-example/src/main/java/com/example/Calculator.java b/java-example/src/main/java/com/example/Calculator.java
new file mode 100644
index 0000000..404a35d
--- /dev/null
+++ b/java-example/src/main/java/com/example/Calculator.java
@@ -0,0 +1,83 @@
+package com.example;
+
+/**
+ * Simple calculator class for demonstrating JUnit testing and JaCoCo coverage.
+ */
+public class Calculator {
+
+ /**
+ * Add two numbers.
+ */
+ public double add(double a, double b) {
+ return a + b;
+ }
+
+ /**
+ * Subtract second number from first.
+ */
+ public double subtract(double a, double b) {
+ return a - b;
+ }
+
+ /**
+ * Multiply two numbers.
+ */
+ public double multiply(double a, double b) {
+ return a * b;
+ }
+
+ /**
+ * Divide first number by second.
+ *
+ * @throws IllegalArgumentException if divisor is zero
+ */
+ public double divide(double a, double b) {
+ if (b == 0) {
+ throw new IllegalArgumentException("Cannot divide by zero");
+ }
+ return a / b;
+ }
+
+ /**
+ * Calculate the power of a number.
+ */
+ public double power(double base, int exponent) {
+ return Math.pow(base, exponent);
+ }
+
+ /**
+ * Calculate the factorial of a non-negative integer.
+ *
+ * @throws IllegalArgumentException if n is negative
+ */
+ public long factorial(int n) {
+ if (n < 0) {
+ throw new IllegalArgumentException("Factorial is not defined for negative numbers");
+ }
+ if (n == 0 || n == 1) {
+ return 1;
+ }
+ return n * factorial(n - 1);
+ }
+
+ /**
+ * Check if a number is prime.
+ */
+ public boolean isPrime(int n) {
+ if (n <= 1) {
+ return false;
+ }
+ if (n <= 3) {
+ return true;
+ }
+ if (n % 2 == 0 || n % 3 == 0) {
+ return false;
+ }
+ for (int i = 5; i * i <= n; i += 6) {
+ if (n % i == 0 || n % (i + 2) == 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/java-example/src/test/java/com/example/CalculatorTest.java b/java-example/src/test/java/com/example/CalculatorTest.java
new file mode 100644
index 0000000..ca789d3
--- /dev/null
+++ b/java-example/src/test/java/com/example/CalculatorTest.java
@@ -0,0 +1,236 @@
+package com.example;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests for Calculator class.
+ */
+class CalculatorTest {
+
+ private Calculator calculator;
+
+ @BeforeEach
+ void setUp() {
+ calculator = new Calculator();
+ }
+
+ // ==================== Addition Tests ====================
+
+ @Nested
+ @DisplayName("Addition Tests")
+ class AdditionTests {
+ @Test
+ @DisplayName("should add positive numbers")
+ void testAddPositiveNumbers() {
+ assertEquals(5.0, calculator.add(2.0, 3.0));
+ }
+
+ @Test
+ @DisplayName("should add negative numbers")
+ void testAddNegativeNumbers() {
+ assertEquals(-5.0, calculator.add(-2.0, -3.0));
+ }
+
+ @Test
+ @DisplayName("should add with zero")
+ void testAddWithZero() {
+ assertEquals(5.0, calculator.add(5.0, 0.0));
+ }
+
+ @Test
+ @DisplayName("should add floats")
+ void testAddFloats() {
+ assertEquals(5.5, calculator.add(2.5, 3.0), 0.001);
+ }
+ }
+
+ // ==================== Subtraction Tests ====================
+
+ @Nested
+ @DisplayName("Subtraction Tests")
+ class SubtractionTests {
+ @Test
+ @DisplayName("should subtract positive numbers")
+ void testSubtractPositiveNumbers() {
+ assertEquals(2.0, calculator.subtract(5.0, 3.0));
+ }
+
+ @Test
+ @DisplayName("should handle negative result")
+ void testSubtractResultingInNegative() {
+ assertEquals(-2.0, calculator.subtract(3.0, 5.0));
+ }
+
+ @Test
+ @DisplayName("should subtract zero")
+ void testSubtractWithZero() {
+ assertEquals(5.0, calculator.subtract(5.0, 0.0));
+ }
+ }
+
+ // ==================== Multiplication Tests ====================
+
+ @Nested
+ @DisplayName("Multiplication Tests")
+ class MultiplicationTests {
+ @Test
+ @DisplayName("should multiply positive numbers")
+ void testMultiplyPositiveNumbers() {
+ assertEquals(15.0, calculator.multiply(3.0, 5.0));
+ }
+
+ @Test
+ @DisplayName("should multiply by zero")
+ void testMultiplyWithZero() {
+ assertEquals(0.0, calculator.multiply(5.0, 0.0));
+ }
+
+ @Test
+ @DisplayName("should multiply negative numbers")
+ void testMultiplyNegativeNumbers() {
+ assertEquals(15.0, calculator.multiply(-3.0, -5.0));
+ }
+
+ @Test
+ @DisplayName("should multiply mixed signs")
+ void testMultiplyMixedSigns() {
+ assertEquals(-15.0, calculator.multiply(3.0, -5.0));
+ }
+ }
+
+ // ==================== Division Tests ====================
+
+ @Nested
+ @DisplayName("Division Tests")
+ class DivisionTests {
+ @Test
+ @DisplayName("should divide positive numbers")
+ void testDividePositiveNumbers() {
+ assertEquals(2.0, calculator.divide(10.0, 5.0));
+ }
+
+ @Test
+ @DisplayName("should return float result")
+ void testDivideResultingInFloat() {
+ assertEquals(2.5, calculator.divide(5.0, 2.0), 0.001);
+ }
+
+ @Test
+ @DisplayName("should throw on divide by zero")
+ void testDivideByZeroThrowsException() {
+ IllegalArgumentException exception = assertThrows(
+ IllegalArgumentException.class,
+ () -> calculator.divide(10.0, 0.0)
+ );
+ assertEquals("Cannot divide by zero", exception.getMessage());
+ }
+ }
+
+ // ==================== Power Tests ====================
+
+ @Nested
+ @DisplayName("Power Tests")
+ class PowerTests {
+ @Test
+ @DisplayName("should calculate positive exponent")
+ void testPowerPositiveExponent() {
+ assertEquals(8.0, calculator.power(2.0, 3));
+ }
+
+ @Test
+ @DisplayName("should handle zero exponent")
+ void testPowerZeroExponent() {
+ assertEquals(1.0, calculator.power(5.0, 0));
+ }
+
+ @Test
+ @DisplayName("should handle negative exponent")
+ void testPowerNegativeExponent() {
+ assertEquals(0.25, calculator.power(2.0, -2), 0.001);
+ }
+ }
+
+ // ==================== Factorial Tests ====================
+
+ @Nested
+ @DisplayName("Factorial Tests")
+ class FactorialTests {
+ @Test
+ @DisplayName("factorial of 0 should be 1")
+ void testFactorialOfZero() {
+ assertEquals(1L, calculator.factorial(0));
+ }
+
+ @Test
+ @DisplayName("factorial of 1 should be 1")
+ void testFactorialOfOne() {
+ assertEquals(1L, calculator.factorial(1));
+ }
+
+ @Test
+ @DisplayName("factorial of 5 should be 120")
+ void testFactorialOfFive() {
+ assertEquals(120L, calculator.factorial(5));
+ }
+
+ @Test
+ @DisplayName("should throw on negative input")
+ void testFactorialOfNegativeThrowsException() {
+ IllegalArgumentException exception = assertThrows(
+ IllegalArgumentException.class,
+ () -> calculator.factorial(-1)
+ );
+ assertEquals("Factorial is not defined for negative numbers", exception.getMessage());
+ }
+ }
+
+ // ==================== Prime Tests ====================
+
+ @Nested
+ @DisplayName("Prime Tests")
+ class PrimeTests {
+ @Test
+ @DisplayName("2 should be prime")
+ void testTwoIsPrime() {
+ assertTrue(calculator.isPrime(2));
+ }
+
+ @Test
+ @DisplayName("17 should be prime")
+ void testSeventeenIsPrime() {
+ assertTrue(calculator.isPrime(17));
+ }
+
+ @Test
+ @DisplayName("4 should not be prime")
+ void testFourIsNotPrime() {
+ assertFalse(calculator.isPrime(4));
+ }
+
+ @Test
+ @DisplayName("1 should not be prime")
+ void testOneIsNotPrime() {
+ assertFalse(calculator.isPrime(1));
+ }
+
+ @Test
+ @DisplayName("negative numbers should not be prime")
+ void testNegativeIsNotPrime() {
+ assertFalse(calculator.isPrime(-5));
+ }
+ }
+
+ // ==================== Intentional Failure (for demo) ====================
+
+ @Test
+ @DisplayName("Intentional failure for demo purposes")
+ void testIntentionalFailure() {
+ // This assertion is intentionally wrong to show failure reporting
+ assertEquals(42.0, calculator.add(1.0, 1.0), "This test is intentionally failing");
+ }
+}
diff --git a/php-example/.gitignore b/php-example/.gitignore
new file mode 100644
index 0000000..104842e
--- /dev/null
+++ b/php-example/.gitignore
@@ -0,0 +1,5 @@
+/vendor/
+/reports/
+composer.lock
+.phpunit.cache/
+.phpunit.result.cache
diff --git a/php-example/README.md b/php-example/README.md
new file mode 100644
index 0000000..ab7c153
--- /dev/null
+++ b/php-example/README.md
@@ -0,0 +1,50 @@
+# PHP Example (PHPUnit + Clover Coverage)
+
+This example demonstrates how to integrate Gaffer with PHPUnit test reports and Clover coverage reports.
+
+## Overview
+
+This project uses:
+- **PHPUnit 10** for testing
+- **Clover XML** format for coverage reporting
+- **JUnit XML** format for test results
+
+## Requirements
+
+- PHP 8.1+
+- Composer
+- Xdebug or PCOV for coverage (CI uses PCOV)
+
+## Local Development
+
+```bash
+# Install dependencies
+composer install
+
+# Run tests
+composer test
+
+# Run tests with coverage (requires Xdebug or PCOV)
+composer test:coverage
+```
+
+## Test Reports
+
+After running tests with coverage, reports are generated in:
+- `reports/phpunit-results.xml` - JUnit-style test results
+- `reports/clover.xml` - Clover coverage format
+- `reports/htmlcov/` - HTML coverage report
+
+## CI Integration
+
+The GitHub Actions workflow (`.github/workflows/phpunit.yml`):
+1. Runs PHPUnit tests with coverage enabled
+2. Generates JUnit test results and Clover coverage
+3. Uploads both reports to Gaffer
+
+## Coverage Formats
+
+PHPUnit can generate multiple coverage formats:
+- **Clover XML** - Used by many CI tools, parsed by Gaffer
+- **Cobertura** - Alternative XML format (use `--coverage-cobertura`)
+- **HTML** - Human-readable coverage browser
diff --git a/php-example/composer.json b/php-example/composer.json
new file mode 100644
index 0000000..ab35d5c
--- /dev/null
+++ b/php-example/composer.json
@@ -0,0 +1,26 @@
+{
+ "name": "gaffer/php-example",
+ "description": "Gaffer PHPUnit example with Clover coverage reporting",
+ "type": "project",
+ "license": "MIT",
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.5"
+ },
+ "autoload": {
+ "psr-4": {
+ "GafferExample\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "GafferExample\\Tests\\": "tests/"
+ }
+ },
+ "scripts": {
+ "test": "phpunit",
+ "test:coverage": "phpunit --coverage-clover reports/clover.xml --coverage-html reports/htmlcov"
+ }
+}
diff --git a/php-example/phpunit.xml b/php-example/phpunit.xml
new file mode 100644
index 0000000..4f74028
--- /dev/null
+++ b/php-example/phpunit.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+ tests
+
+
+
+
+
+ src
+
+
+
+
+
+
+
diff --git a/php-example/src/Calculator.php b/php-example/src/Calculator.php
new file mode 100644
index 0000000..5f9911f
--- /dev/null
+++ b/php-example/src/Calculator.php
@@ -0,0 +1,72 @@
+factorial($n - 1);
+ }
+}
diff --git a/php-example/tests/CalculatorTest.php b/php-example/tests/CalculatorTest.php
new file mode 100644
index 0000000..1c9ee79
--- /dev/null
+++ b/php-example/tests/CalculatorTest.php
@@ -0,0 +1,154 @@
+calculator = new Calculator();
+ }
+
+ // ==================== Addition Tests ====================
+
+ public function testAddPositiveNumbers(): void
+ {
+ $this->assertEquals(5, $this->calculator->add(2, 3));
+ }
+
+ public function testAddNegativeNumbers(): void
+ {
+ $this->assertEquals(-5, $this->calculator->add(-2, -3));
+ }
+
+ public function testAddWithZero(): void
+ {
+ $this->assertEquals(5, $this->calculator->add(5, 0));
+ }
+
+ public function testAddFloats(): void
+ {
+ $this->assertEqualsWithDelta(5.5, $this->calculator->add(2.5, 3.0), 0.001);
+ }
+
+ // ==================== Subtraction Tests ====================
+
+ public function testSubtractPositiveNumbers(): void
+ {
+ $this->assertEquals(2, $this->calculator->subtract(5, 3));
+ }
+
+ public function testSubtractResultingInNegative(): void
+ {
+ $this->assertEquals(-2, $this->calculator->subtract(3, 5));
+ }
+
+ public function testSubtractWithZero(): void
+ {
+ $this->assertEquals(5, $this->calculator->subtract(5, 0));
+ }
+
+ // ==================== Multiplication Tests ====================
+
+ public function testMultiplyPositiveNumbers(): void
+ {
+ $this->assertEquals(15, $this->calculator->multiply(3, 5));
+ }
+
+ public function testMultiplyWithZero(): void
+ {
+ $this->assertEquals(0, $this->calculator->multiply(5, 0));
+ }
+
+ public function testMultiplyNegativeNumbers(): void
+ {
+ $this->assertEquals(15, $this->calculator->multiply(-3, -5));
+ }
+
+ public function testMultiplyMixedSigns(): void
+ {
+ $this->assertEquals(-15, $this->calculator->multiply(3, -5));
+ }
+
+ // ==================== Division Tests ====================
+
+ public function testDividePositiveNumbers(): void
+ {
+ $this->assertEquals(2, $this->calculator->divide(10, 5));
+ }
+
+ public function testDivideResultingInFloat(): void
+ {
+ $this->assertEqualsWithDelta(2.5, $this->calculator->divide(5, 2), 0.001);
+ }
+
+ public function testDivideByZeroThrowsException(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Cannot divide by zero');
+ $this->calculator->divide(10, 0);
+ }
+
+ // ==================== Power Tests ====================
+
+ public function testPowerPositiveExponent(): void
+ {
+ $this->assertEquals(8, $this->calculator->power(2, 3));
+ }
+
+ public function testPowerZeroExponent(): void
+ {
+ $this->assertEquals(1, $this->calculator->power(5, 0));
+ }
+
+ public function testPowerNegativeExponent(): void
+ {
+ $this->assertEqualsWithDelta(0.25, $this->calculator->power(2, -2), 0.001);
+ }
+
+ // ==================== Factorial Tests ====================
+
+ public function testFactorialOfZero(): void
+ {
+ $this->assertEquals(1, $this->calculator->factorial(0));
+ }
+
+ public function testFactorialOfOne(): void
+ {
+ $this->assertEquals(1, $this->calculator->factorial(1));
+ }
+
+ public function testFactorialOfFive(): void
+ {
+ $this->assertEquals(120, $this->calculator->factorial(5));
+ }
+
+ public function testFactorialOfNegativeThrowsException(): void
+ {
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage('Factorial is not defined for negative numbers');
+ $this->calculator->factorial(-1);
+ }
+
+ // ==================== Intentional Failure (for demo) ====================
+
+ /**
+ * This test intentionally fails to demonstrate test failure reporting.
+ * Comment out or fix in production.
+ */
+ public function testIntentionalFailure(): void
+ {
+ // This assertion is intentionally wrong to show failure reporting
+ $this->assertEquals(42, $this->calculator->add(1, 1), 'This test is intentionally failing');
+ }
+}
diff --git a/pytest-example/requirements.txt b/pytest-example/requirements.txt
index 6404995..5d75aba 100644
--- a/pytest-example/requirements.txt
+++ b/pytest-example/requirements.txt
@@ -1,2 +1,3 @@
pytest>=8.0.0
pytest-html>=4.1.1
+pytest-cov>=4.1.0