Skip to content

Commit

Permalink
Run OpenTelemetry Instrumentations tests with EDOT (#146) (#160)
Browse files Browse the repository at this point in the history
  • Loading branch information
intuibase authored Feb 8, 2025
1 parent 7e7c381 commit c6cd363
Show file tree
Hide file tree
Showing 9 changed files with 593 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ jobs:
with:
build_arch: all

test-otel-unit:
needs:
- build-packages
uses: ./.github/workflows/test-otel-unit.yml


# The very last job to report whether the Workflow passed.
# This will act as the Branch Protection gatekeeper
ci:
Expand Down
52 changes: 52 additions & 0 deletions .github/workflows/test-otel-unit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---

name: test-otel-unit

on:
workflow_call: ~

permissions:
contents: read

env:
BUILD_PACKAGES: build/packages

jobs:
generate-php-versions:
uses: ./.github/workflows/generate-php-versions.yml

test-otel-unit:
name: test-otel-unit
runs-on: 'ubuntu-latest'
needs: generate-php-versions
timeout-minutes: 300
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-php-versions.outputs.php-versions) }}
env:
PHP_VERSION: ${{ matrix.php-version }}
steps:
- uses: actions/checkout@v4
- name: Download package artifacts for Debian AMD64
uses: actions/download-artifact@v4
with:
pattern: packages-linux-x86-64
path: ${{ env.BUILD_PACKAGES }}

- name: Run otel instrumentation unit tests
continue-on-error: true
run: |
PACKAGE_FILE=$(find ${{ env.BUILD_PACKAGES }} -name elastic*amd64.deb)
./tools/build/test_otel_unit_tests_in_docker.sh --php_versions "${PHP_VERSION}" --results_path "build/otel_unit_results" --deb_package ${PACKAGE_FILE}
- name: Generate test summary
if: always()
run: ./tools/build/junit_summary.py --path-to-test-results "build/otel_unit_results/**/*.xml" --header "Test summary for PHP ${PHP_VERSION}" >>$GITHUB_STEP_SUMMARY

- uses: actions/upload-artifact@v4
if: always()
continue-on-error: true
with:
name: test-otel-unit-${{ matrix.php-version }}
path: |
build/otel_unit_results/*
89 changes: 89 additions & 0 deletions docker/otel-tests/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
services:
php:
build:
context: ./docker
dockerfile: Dockerfile
args:
- PHP_VERSION
volumes:
- ./:/usr/src/myapp
user: "${PHP_USER}:root"
environment:
XDEBUG_MODE: ${XDEBUG_MODE:-off}
XDEBUG_CONFIG: ${XDEBUG_CONFIG:-''}
PHP_IDE_CONFIG: ${PHP_IDE_CONFIG:-''}
RABBIT_HOST: ${RABBIT_HOST:-rabbitmq}
KAFKA_HOST: ${KAFKA_HOST:-kafka}
MONGODB_HOST: ${MONGODB_HOST:-mongodb}
MONGODB_PORT: ${MONGODB_PORT:-27017}
MYSQL_HOST: ${MYSQL_HOST:-mysql}

zipkin:
image: openzipkin/zipkin-slim
ports:
- 9411:9411
jaeger:
image: jaegertracing/all-in-one
environment:
COLLECTOR_ZIPKIN_HOST_PORT: 9412
ports:
- 9412:9412
- 16686:16686

collector:
image: otel/opentelemetry-collector-contrib
command: [ "--config=/etc/otel-collector-config.yml" ]
volumes:
- ./files/collector/otel-collector-config.yml:/etc/otel-collector-config.yml

rabbitmq:
image: rabbitmq:3
hostname: rabbitmq
healthcheck:
test: rabbitmq-diagnostics -q ping
interval: 30s
timeout: 30s
retries: 3
ports:
- "5672:5672/tcp"
kafka:
image: confluentinc/cp-kafka:7.2.1
hostname: kafka
ports:
- "9092:9092/tcp"
environment:
KAFKA_PROCESS_ROLES: 'broker,controller'
KAFKA_NODE_ID: 1
KAFKA_ADVERTISED_LISTENERS: ${KAFKA_ADVERTISED_LISTENERS:-PLAINTEXT://kafka:29092,PLAINTEXT_HOST://kafka:9092}
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_LISTENERS: 'PLAINTEXT://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092'
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093'
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
command: "bash -c '/tmp/update_run.sh && /etc/confluent/docker/run'"
volumes:
- ./docker/kafka/update_run.sh:/tmp/update_run.sh

mongodb:
image: mongo:4
hostname: mongodb
ports:
- "27017:27017/tcp"

mysql:
image: mysql:8.0
hostname: mysql
ports:
- "3306:3306/tcp"
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: otel_db
MYSQL_USER: otel_user
MYSQL_PASSWORD: otel_passwd
healthcheck:
test: mysql -uotel_user -potel_passwd -e "USE otel_db;"
interval: 30s
timeout: 30s
retries: 3
volumes:
- ./docker/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
18 changes: 18 additions & 0 deletions docker/otel-tests/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
ARG PHP_VERSION=8.1
FROM php:${PHP_VERSION}-cli

USER root

RUN apt-get update && apt-get install -y \
jq git unzip tar

ADD https://getcomposer.org/installer composer-setup.php

RUN php composer-setup.php --quiet --install-dir="/usr/local/bin" --filename="composer" \
&& chmod +x /usr/local/bin/composer \
&& rm composer-setup.php

RUN docker-php-ext-configure mysqli \
&& docker-php-ext-install -j$(nproc) mysqli

USER php
11 changes: 11 additions & 0 deletions docker/otel-tests/docker/kafka/update_run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# This script is required to run kafka cluster (without zookeeper)
#!/bin/sh

# Docker workaround: Remove check for KAFKA_ZOOKEEPER_CONNECT parameter
sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure

# Docker workaround: Ignore cub zk-ready
sed -i 's/cub zk-ready/echo ignore zk-ready/' /etc/confluent/docker/ensure

# KRaft required step: Format the storage directory with a new cluster ID
echo "kafka-storage format --ignore-formatted -t $(kafka-storage random-uuid) -c /etc/kafka/kafka.properties" >> /etc/confluent/docker/ensure
34 changes: 34 additions & 0 deletions docker/otel-tests/docker/mysql/init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
CREATE DATABASE IF NOT EXISTS otel_db2;
CREATE USER 'otel_user2'@'%' IDENTIFIED BY 'otel_passwd';


GRANT ALL PRIVILEGES ON *.* TO 'otel_user'@'%';
GRANT ALL PRIVILEGES ON *.* TO 'otel_user2'@'%';
FLUSH PRIVILEGES;


USE otel_db;

CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO users (name, email) VALUES
('John Doe', '[email protected]'),
('Jane Smith', '[email protected]'),
('Bob Johnson', '[email protected]');

CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
stock INT NOT NULL DEFAULT 0
);

INSERT INTO products (name, price, stock) VALUES
('Laptop', 999.99, 10),
('Smartphone', 499.99, 25),
('Headphones', 49.99, 50);
72 changes: 72 additions & 0 deletions tools/build/junit_summary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/bin/env python3
import argparse
import glob
import xml.etree.ElementTree as ET

def parse_arguments():
parser = argparse.ArgumentParser(description="Parse JUnit XML test results and generate a summary.")
parser.add_argument("--path-to-test-results", required=True, help="Path pattern to JUnit XML test result files. Supports wildcards (e.g., results/**/*.xml).")
parser.add_argument("--header", required=True, help="The title of the summary output.")
return parser.parse_args()

def parse_junit_xml(file):
try:
tree = ET.parse(file)
root = tree.getroot()

# Extract main test summary from the first-level testsuite (overall summary)
main_suite = root.find("./testsuite[@name='']")
if main_suite is None:
main_suite = root.find("./testsuite")

total_tests = int(main_suite.attrib.get("tests", 0))
total_failures = int(main_suite.attrib.get("failures", 0))
total_errors = int(main_suite.attrib.get("errors", 0))
total_skipped = int(main_suite.attrib.get("skipped", 0))
total_time = float(main_suite.attrib.get("time", 0))
passed_tests = total_tests - total_failures - total_errors - total_skipped

failures = []
# Extract failures from nested test suites
for ts in root.findall(".//testsuite[@name!='']"):
for tc in ts.findall("testcase[failure]"):
class_name = tc.attrib.get("classname", "Unknown")
test_name = tc.attrib.get("name", "Unknown")
file_path = tc.attrib.get("file", "Unknown")
line = tc.attrib.get("line", "Unknown")
failure_message = tc.find("failure").text.strip() if tc.find("failure") is not None else "Unknown"
failure_message = failure_message.replace("\n", "<br>") # Convert newlines to Markdown format
failures.append((class_name, test_name, f"{file_path}:{line}", failure_message))

return file, passed_tests, total_failures, total_skipped, total_errors, total_time, failures
except Exception as e:
print(f"Error processing {file}: {e}")
return file, 0, 0, 0, 0, 0.0, []

def main():
args = parse_arguments()
files = glob.glob(args.path_to_test_results, recursive=True)

print(f"## {args.header}\n")
print("| Status | File | ✅ Passed | ❌ Failed | ⚠ Skipped | 🔍 Errors | ⏱ Time (s) |")
print("|--------|------|---------|---------|---------|---------|---------|")

all_failures = []
for file in files:
file, passed, failed, skipped, errors, total_time, failures = parse_junit_xml(file)
status = "✅" if failed == 0 else "❌"
print(f"| {status} | {file} | {passed} | {failed} | {skipped} | {errors} | {total_time:.3f} |")
all_failures.extend(failures)

if all_failures:
print("<details>\n<summary>Failure Details</summary>\n")
print("### Failure Details\n")
print("| Test Class | Test Name | File:Line | Failure Message |")
print("|------------|----------|----------|----------------|")
for class_name, test_name, file_line, failure_message in all_failures:
print(f"| {class_name} | {test_name} | {file_line} | {failure_message} |")
print("\n</details>\n")


if __name__ == "__main__":
main()
Loading

0 comments on commit c6cd363

Please sign in to comment.