Skip to content

Commit 6faae30

Browse files
authored
Test distro with contrib unit tests (#8)
1 parent 40a15b2 commit 6faae30

File tree

11 files changed

+646
-1
lines changed

11 files changed

+646
-1
lines changed

.github/workflows/build.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,18 @@ jobs:
4646
with:
4747
build_arch: all
4848

49+
test-otel-unit:
50+
needs:
51+
- build-packages
52+
uses: ./.github/workflows/test-otel-unit.yml
53+
4954
# The very last job to report whether the Workflow passed.
5055
# This will act as the Branch Protection gatekeeper
5156
ci:
5257
needs:
5358
- tests-phpt
5459
- build-packages
60+
- test-otel-unit
5561
runs-on: ubuntu-latest
5662
steps:
5763
- name: report

.github/workflows/generate-php-versions.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
- id: generate
2626
run: |
2727
source ./tools/read_properties.sh
28-
read_properties opentelemetry-php-distro PROJECT_PROPERTIES
28+
read_properties project.properties PROJECT_PROPERTIES
2929
PHP_VERSIONS=${PROJECT_PROPERTIES_SUPPORTED_PHP_VERSIONS//[()]/}
3030
echo "PHP_VERSIONS: ${PHP_VERSIONS}"
3131
PHP_VERSIONS_JSON=$(echo -n ${PHP_VERSIONS} | jq --raw-input --slurp --compact-output 'split(" ") | map(select(length > 0)) | map({ "php-version": . } )')
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
3+
name: test-otel-unit
4+
5+
on:
6+
workflow_call: ~
7+
8+
permissions:
9+
contents: read
10+
11+
env:
12+
BUILD_PACKAGES: build/packages
13+
14+
jobs:
15+
generate-php-versions:
16+
uses: ./.github/workflows/generate-php-versions.yml
17+
18+
test-otel-unit:
19+
name: test-otel-unit
20+
runs-on: 'ubuntu-latest'
21+
needs: generate-php-versions
22+
timeout-minutes: 300
23+
strategy:
24+
fail-fast: false
25+
matrix: ${{ fromJson(needs.generate-php-versions.outputs.php-versions) }}
26+
env:
27+
PHP_VERSION: ${{ matrix.php-version }}
28+
steps:
29+
- uses: actions/checkout@v6
30+
- name: Download package artifacts for Debian AMD64
31+
uses: actions/download-artifact@v7
32+
with:
33+
pattern: packages-linux-x86-64
34+
path: ${{ env.BUILD_PACKAGES }}
35+
36+
- name: Run otel instrumentation unit tests
37+
continue-on-error: true
38+
run: |
39+
PACKAGE_FILE=$(find ${{ env.BUILD_PACKAGES }} -name opentelemetry-php-distro*amd64.deb)
40+
./tools/build/test_otel_unit_tests_in_docker.sh --php_versions "${PHP_VERSION}" --results_path "build/otel_unit_results" --deb_package ${PACKAGE_FILE}
41+
42+
- name: Generate test summary
43+
if: always()
44+
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
45+
46+
- uses: actions/upload-artifact@v6
47+
if: always()
48+
continue-on-error: true
49+
with:
50+
name: test-otel-unit-${{ matrix.php-version }}
51+
path: |
52+
build/otel_unit_results/*
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
services:
2+
php:
3+
build:
4+
context: ./docker
5+
dockerfile: Dockerfile
6+
args:
7+
- PHP_VERSION
8+
volumes:
9+
- ./:/usr/src/myapp
10+
user: "${PHP_USER}:root"
11+
environment:
12+
XDEBUG_MODE: ${XDEBUG_MODE:-off}
13+
XDEBUG_CONFIG: ${XDEBUG_CONFIG:-''}
14+
PHP_IDE_CONFIG: ${PHP_IDE_CONFIG:-''}
15+
RABBIT_HOST: ${RABBIT_HOST:-rabbitmq}
16+
KAFKA_HOST: ${KAFKA_HOST:-kafka}
17+
MONGODB_HOST: ${MONGODB_HOST:-mongodb}
18+
MONGODB_PORT: ${MONGODB_PORT:-27017}
19+
MYSQL_HOST: ${MYSQL_HOST:-mysql}
20+
POSTGRESQL_HOST: ${POSTGRESQL_HOST:-postgresql}
21+
22+
zipkin:
23+
image: openzipkin/zipkin-slim
24+
ports:
25+
- 9411:9411
26+
jaeger:
27+
image: jaegertracing/all-in-one
28+
environment:
29+
COLLECTOR_ZIPKIN_HOST_PORT: 9412
30+
ports:
31+
- 9412:9412
32+
- 16686:16686
33+
34+
collector:
35+
image: otel/opentelemetry-collector-contrib
36+
command: [ "--config=/etc/otel-collector-config.yml" ]
37+
volumes:
38+
- ./files/collector/otel-collector-config.yml:/etc/otel-collector-config.yml
39+
40+
rabbitmq:
41+
image: rabbitmq:3
42+
hostname: rabbitmq
43+
healthcheck:
44+
test: rabbitmq-diagnostics -q ping
45+
interval: 30s
46+
timeout: 30s
47+
retries: 3
48+
ports:
49+
- "5672:5672/tcp"
50+
kafka:
51+
image: confluentinc/cp-kafka:7.2.1
52+
hostname: kafka
53+
ports:
54+
- "9092:9092/tcp"
55+
environment:
56+
KAFKA_PROCESS_ROLES: 'broker,controller'
57+
KAFKA_NODE_ID: 1
58+
KAFKA_ADVERTISED_LISTENERS: ${KAFKA_ADVERTISED_LISTENERS:-PLAINTEXT://kafka:29092,PLAINTEXT_HOST://kafka:9092}
59+
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
60+
KAFKA_LISTENERS: 'PLAINTEXT://kafka:29092,CONTROLLER://kafka:29093,PLAINTEXT_HOST://0.0.0.0:9092'
61+
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
62+
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka:29093'
63+
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
64+
command: "bash -c '/tmp/update_run.sh && /etc/confluent/docker/run'"
65+
volumes:
66+
- ./docker/kafka/update_run.sh:/tmp/update_run.sh
67+
68+
mongodb:
69+
image: mongo:4
70+
hostname: mongodb
71+
ports:
72+
- "27017:27017/tcp"
73+
74+
mysql:
75+
image: mysql:8.0
76+
hostname: mysql
77+
ports:
78+
- "3306:3306/tcp"
79+
environment:
80+
MYSQL_ROOT_PASSWORD: root_password
81+
MYSQL_DATABASE: otel_db
82+
MYSQL_USER: otel_user
83+
MYSQL_PASSWORD: otel_passwd
84+
healthcheck:
85+
test: mysql -uotel_user -potel_passwd -e "USE otel_db;"
86+
interval: 30s
87+
timeout: 30s
88+
retries: 3
89+
volumes:
90+
- ./docker/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
91+
92+
postgresql:
93+
image: postgres:17.5
94+
hostname: postgresql
95+
ports:
96+
- "5432:5432/tcp"
97+
environment:
98+
POSTGRES_DB: otel_db
99+
POSTGRES_USER: otel_user
100+
POSTGRES_PASSWORD: otel_passwd
101+
healthcheck:
102+
test: ["CMD-SHELL", "PGPASSWORD=otel_passwd psql -U otel_user -d otel_db -h 127.0.0.1 -c 'SELECT 1'"]
103+
interval: 30s
104+
timeout: 90s
105+
retries: 3
106+
volumes:
107+
- ./docker/postgresql/init.sql:/docker-entrypoint-initdb.d/init.sql
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
ARG PHP_VERSION=8.1
2+
FROM php:${PHP_VERSION}-cli
3+
4+
USER root
5+
6+
RUN apt-get update && apt-get install -y \
7+
jq git unzip tar libpq-dev
8+
9+
ADD https://getcomposer.org/installer composer-setup.php
10+
11+
RUN php composer-setup.php --quiet --install-dir="/usr/local/bin" --filename="composer" \
12+
&& chmod +x /usr/local/bin/composer \
13+
&& rm composer-setup.php
14+
15+
RUN docker-php-ext-configure mysqli \
16+
&& docker-php-ext-install -j$(nproc) mysqli pgsql
17+
18+
USER php
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# This script is required to run kafka cluster (without zookeeper)
2+
#!/bin/sh
3+
4+
# Docker workaround: Remove check for KAFKA_ZOOKEEPER_CONNECT parameter
5+
sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure
6+
7+
# Docker workaround: Ignore cub zk-ready
8+
sed -i 's/cub zk-ready/echo ignore zk-ready/' /etc/confluent/docker/ensure
9+
10+
# KRaft required step: Format the storage directory with a new cluster ID
11+
echo "kafka-storage format --ignore-formatted -t $(kafka-storage random-uuid) -c /etc/kafka/kafka.properties" >> /etc/confluent/docker/ensure
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
CREATE DATABASE IF NOT EXISTS otel_db2;
2+
CREATE USER 'otel_user2'@'%' IDENTIFIED BY 'otel_passwd';
3+
4+
5+
GRANT ALL PRIVILEGES ON *.* TO 'otel_user'@'%';
6+
GRANT ALL PRIVILEGES ON *.* TO 'otel_user2'@'%';
7+
FLUSH PRIVILEGES;
8+
9+
10+
USE otel_db;
11+
12+
CREATE TABLE users (
13+
id INT AUTO_INCREMENT PRIMARY KEY,
14+
name VARCHAR(255) NOT NULL,
15+
email VARCHAR(255) UNIQUE NOT NULL,
16+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
17+
);
18+
19+
INSERT INTO users (name, email) VALUES
20+
('John Doe', 'john.doe@example.com'),
21+
('Jane Smith', 'jane.smith@example.com'),
22+
('Bob Johnson', 'bob.johnson@example.com');
23+
24+
CREATE TABLE products (
25+
id INT AUTO_INCREMENT PRIMARY KEY,
26+
name VARCHAR(255) NOT NULL,
27+
price DECIMAL(10, 2) NOT NULL,
28+
stock INT NOT NULL DEFAULT 0
29+
);
30+
31+
INSERT INTO products (name, price, stock) VALUES
32+
('Laptop', 999.99, 10),
33+
('Smartphone', 499.99, 25),
34+
('Headphones', 49.99, 50);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
2+
CREATE DATABASE otel_db2;
3+
4+
\connect otel_db2;
5+
6+
CREATE USER otel_user2 WITH PASSWORD 'otel_passwd';
7+
8+
GRANT ALL PRIVILEGES ON DATABASE otel_db2 TO otel_user2;
9+
10+
\connect otel_db;
11+
12+
CREATE TABLE users (
13+
id SERIAL PRIMARY KEY,
14+
name VARCHAR(255) NOT NULL,
15+
email VARCHAR(255) UNIQUE NOT NULL,
16+
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
17+
);
18+
19+
INSERT INTO users (name, email) VALUES
20+
('John Doe', 'john.doe@example.com'),
21+
('Jane Smith', 'jane.smith@example.com'),
22+
('Bob Johnson', 'bob.johnson@example.com');
23+
24+
CREATE TABLE products (
25+
id SERIAL PRIMARY KEY,
26+
name VARCHAR(255) NOT NULL,
27+
price NUMERIC(10, 2) NOT NULL,
28+
stock INT NOT NULL DEFAULT 0
29+
);
30+
31+
INSERT INTO products (name, price, stock) VALUES
32+
('Laptop', 999.99, 10),
33+
('Smartphone', 499.99, 25),
34+
('Headphones', 49.99, 50);

tools/build/junit_summary.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/bin/env python3
2+
import argparse
3+
import glob
4+
import xml.etree.ElementTree as ET
5+
6+
def parse_arguments():
7+
parser = argparse.ArgumentParser(description="Parse JUnit XML test results and generate a summary.")
8+
parser.add_argument("--path-to-test-results", required=True, help="Path pattern to JUnit XML test result files. Supports wildcards (e.g., results/**/*.xml).")
9+
parser.add_argument("--header", required=True, help="The title of the summary output.")
10+
return parser.parse_args()
11+
12+
def parse_junit_xml(file):
13+
try:
14+
tree = ET.parse(file)
15+
root = tree.getroot()
16+
17+
# Extract main test summary from the first-level testsuite (overall summary)
18+
main_suite = root.find("./testsuite[@name='']")
19+
if main_suite is None:
20+
main_suite = root.find("./testsuite")
21+
22+
total_tests = int(main_suite.attrib.get("tests", 0))
23+
total_failures = int(main_suite.attrib.get("failures", 0))
24+
total_errors = int(main_suite.attrib.get("errors", 0))
25+
total_skipped = int(main_suite.attrib.get("skipped", 0))
26+
total_time = float(main_suite.attrib.get("time", 0))
27+
passed_tests = total_tests - total_failures - total_errors - total_skipped
28+
29+
failures = []
30+
# Extract failures from nested test suites
31+
for ts in root.findall(".//testsuite[@name!='']"):
32+
for tc in ts.findall("testcase[failure]"):
33+
class_name = tc.attrib.get("classname", "Unknown")
34+
test_name = tc.attrib.get("name", "Unknown")
35+
file_path = tc.attrib.get("file", "Unknown")
36+
line = tc.attrib.get("line", "Unknown")
37+
failure_message = tc.find("failure").text.strip() if tc.find("failure") is not None else "Unknown"
38+
failure_message = failure_message.replace("\n", "<br>") # Convert newlines to Markdown format
39+
failures.append((class_name, test_name, f"{file_path}:{line}", failure_message))
40+
41+
return file, passed_tests, total_failures, total_skipped, total_errors, total_time, failures
42+
except Exception as e:
43+
print(f"Error processing {file}: {e}")
44+
return file, 0, 0, 0, 0, 0.0, []
45+
46+
def main():
47+
args = parse_arguments()
48+
files = glob.glob(args.path_to_test_results, recursive=True)
49+
50+
print(f"## {args.header}\n")
51+
print("| Status | File | ✅ Passed | ❌ Failed | ⚠ Skipped | 🔍 Errors | ⏱ Time (s) |")
52+
print("|--------|------|---------|---------|---------|---------|---------|")
53+
54+
all_failures = []
55+
for file in files:
56+
file, passed, failed, skipped, errors, total_time, failures = parse_junit_xml(file)
57+
status = "✅" if failed == 0 else "❌"
58+
print(f"| {status} | {file} | {passed} | {failed} | {skipped} | {errors} | {total_time:.3f} |")
59+
all_failures.extend(failures)
60+
61+
if all_failures:
62+
print("<details>\n<summary>Failure Details</summary>\n")
63+
print("### Failure Details\n")
64+
print("| Test Class | Test Name | File:Line | Failure Message |")
65+
print("|------------|----------|----------|----------------|")
66+
for class_name, test_name, file_line, failure_message in all_failures:
67+
print(f"| {class_name} | {test_name} | {file_line} | {failure_message} |")
68+
print("\n</details>\n")
69+
70+
71+
if __name__ == "__main__":
72+
main()

0 commit comments

Comments
 (0)