Skip to content

Commit 2fc99f1

Browse files
committed
Fix workflow failures: improve CI/CD pipeline, add concurrency control, fix test configuration
- Add concurrency control to prevent resource conflicts between matrix jobs - Implement proper virtual environment usage in workflow - Add fail-fast: false to continue other Python versions if one fails - Improve dependency caching with Python version in cache key - Add schema file validation step before tests run - Fix conftest.py configuration management and validation - Update test_user_retrieval.py to use fixtures properly - Add better error handling and validation throughout - Separate smoke test and full test results into different directories - Enhance artifact upload to include all relevant test files - Add environment properties generation for Allure reports These changes address the root causes of workflow failures and should significantly improve the success rate from 11/35 to near 100%.
1 parent 525528b commit 2fc99f1

3 files changed

Lines changed: 119 additions & 66 deletions

File tree

.github/workflows/ci.yml

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ name: API Test Automation CI/CD
1010
- cron: '0 6 * * *'
1111
workflow_dispatch:
1212

13+
# Add concurrency control to prevent resource conflicts
14+
concurrency:
15+
group: ${{ github.workflow }}-${{ github.ref }}-${{ matrix.python-version }}
16+
cancel-in-progress: true
17+
1318
permissions:
1419
contents: read
1520
pages: write
@@ -19,13 +24,16 @@ permissions:
1924

2025
env:
2126
PYTHON_VERSION_DEFAULT: "3.11"
27+
PYTHONPATH: ${{ github.workspace }}
2228

2329
jobs:
2430
test:
2531
runs-on: ubuntu-latest
2632
strategy:
2733
matrix:
2834
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
35+
# Add fail-fast false to continue with other versions if one fails
36+
fail-fast: false
2937

3038
steps:
3139
- uses: actions/checkout@v4
@@ -39,17 +47,29 @@ jobs:
3947
uses: actions/cache@v4
4048
with:
4149
path: ~/.cache/pip
42-
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
50+
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/requirements.txt') }}
4351
restore-keys: |
44-
${{ runner.os }}-pip-
52+
${{ runner.os }}-pip-${{ matrix.python-version }}-
4553
4654
- name: Install dependencies
4755
run: |
4856
python -m pip install --upgrade pip
57+
python -m venv .venv
58+
source .venv/bin/activate
4959
pip install -r requirements.txt
60+
61+
- name: Verify test data and schemas
62+
run: |
63+
source .venv/bin/activate
64+
mkdir -p schemas
65+
if [ ! -f "schemas/user_list_schema.json" ]; then
66+
echo "Error: Missing schema files"
67+
exit 1
68+
fi
5069
5170
- name: Lint with flake8
5271
run: |
72+
source .venv/bin/activate
5373
pip install flake8
5474
# Stop the build if there are Python syntax errors or undefined names
5575
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
@@ -58,12 +78,14 @@ jobs:
5878
5979
- name: Run smoke tests
6080
run: |
61-
python -m pytest tests/ -m smoke -v --tb=short
81+
source .venv/bin/activate
82+
python -m pytest tests/ -m smoke -v --tb=short --alluredir=allure-results/smoke
6283
6384
- name: Run all tests with coverage
6485
run: |
86+
source .venv/bin/activate
6587
pip install pytest-cov
66-
python -m pytest tests/ -v --cov=utils --cov=tests --cov-report=xml --cov-report=html
88+
python -m pytest tests/ -v --cov=utils --cov=tests --cov-report=xml --cov-report=html --alluredir=allure-results/all
6789
6890
- name: Upload coverage to Codecov
6991
uses: codecov/codecov-action@v4
@@ -74,11 +96,6 @@ jobs:
7496
token: ${{ secrets.CODECOV_TOKEN }}
7597
fail_ci_if_error: false
7698

77-
- name: Generate Allure results
78-
if: always()
79-
run: |
80-
python -m pytest tests/ --alluredir=allure-results --tb=short || true
81-
8299
- name: Install Allure CLI
83100
if: always()
84101
run: |
@@ -92,32 +109,21 @@ jobs:
92109
- name: Generate Allure HTML report
93110
if: always()
94111
run: |
95-
allure generate allure-results --clean -o allure-report || echo "Allure report generation failed"
96-
97-
- name: Upload Allure results
98-
if: always()
99-
uses: actions/upload-artifact@v4
100-
with:
101-
name: allure-results-${{ matrix.python-version }}
102-
path: allure-results/
103-
retention-days: 30
104-
if-no-files-found: warn
105-
106-
- name: Upload Allure HTML report
107-
if: always()
108-
uses: actions/upload-artifact@v4
109-
with:
110-
name: allure-report-${{ matrix.python-version }}
111-
path: allure-report/
112-
retention-days: 30
113-
if-no-files-found: warn
112+
source .venv/bin/activate
113+
allure generate allure-results/all --clean -o allure-report/all || echo "Allure report generation failed"
114+
allure generate allure-results/smoke --clean -o allure-report/smoke || echo "Smoke tests report generation failed"
114115
115116
- name: Upload test reports
116117
if: always()
117118
uses: actions/upload-artifact@v4
118119
with:
119120
name: test-reports-${{ matrix.python-version }}
120-
path: reports/
121+
path: |
122+
allure-results/
123+
allure-report/
124+
reports/
125+
coverage.xml
126+
htmlcov/
121127
retention-days: 30
122128
if-no-files-found: warn
123129

conftest.py

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,89 @@
11
import pytest
22
import allure
3+
import os
34
from utils.api_utils import load_config
45

56

7+
def pytest_configure(config):
8+
"""
9+
Initialize test configuration and environment.
10+
"""
11+
config.addinivalue_line(
12+
"markers",
13+
"smoke: marks tests as smoke tests"
14+
)
15+
config.addinivalue_line(
16+
"markers",
17+
"user_retrieval: marks tests related to user retrieval operations"
18+
)
19+
config.addinivalue_line(
20+
"markers",
21+
"regression: marks tests as regression tests"
22+
)
23+
24+
# Create reports directory if it doesn't exist
25+
import os
26+
os.makedirs("reports", exist_ok=True)
27+
28+
# Create Allure environment properties file for reporting context
29+
try:
30+
config_data = load_config()
31+
with open("reports/environment.properties", "w") as f:
32+
f.write(f"BaseURL={config_data['environments'][config_data['env']]['base_url']}\n")
33+
f.write("Framework=API Test Automation\n")
34+
f.write("Language=Python\n")
35+
except Exception:
36+
# If config loading fails during pytest configure, skip environment file creation
37+
pass
38+
39+
640
@pytest.fixture(scope="session")
741
def config():
842
"""
943
Load and provide the entire config dictionary to tests.
44+
Raises an error if configuration or schema files are missing.
1045
"""
11-
return load_config()
46+
# Ensure config file exists
47+
if not os.path.exists(os.path.join("config", "config.yaml")):
48+
raise FileNotFoundError("config.yaml not found in config directory")
49+
50+
# Load config
51+
config_data = load_config()
52+
53+
# Validate required config sections
54+
required_sections = ["env", "environments", "timeout_seconds", "default_headers"]
55+
for section in required_sections:
56+
if section not in config_data:
57+
raise KeyError(f"Missing required config section: {section}")
58+
59+
# Ensure schemas directory exists with required schemas
60+
schemas_dir = os.path.join("schemas")
61+
if not os.path.exists(schemas_dir):
62+
raise FileNotFoundError("schemas directory not found")
63+
64+
required_schemas = ["user_list_schema.json"]
65+
for schema in required_schemas:
66+
if not os.path.exists(os.path.join(schemas_dir, schema)):
67+
raise FileNotFoundError(f"Required schema file missing: {schema}")
68+
69+
return config_data
1270

1371

1472
@pytest.fixture(scope="session")
1573
def base_url(config):
1674
"""
1775
Provide the base URL for the current environment.
76+
Validates that the URL is properly configured.
1877
"""
1978
env = config["env"]
20-
return config["environments"][env]["base_url"]
79+
if env not in config["environments"]:
80+
raise KeyError(f"Environment '{env}' not found in config")
81+
82+
url = config["environments"][env]["base_url"]
83+
if not url:
84+
raise ValueError(f"Base URL not configured for environment: {env}")
85+
86+
return url
2187

2288

2389
@pytest.fixture(scope="function")
@@ -55,23 +121,4 @@ def attach_allure_logs(request):
55121
)
56122

57123

58-
def pytest_configure(config):
59-
"""
60-
Pytest hook to customize test configuration.
61-
62-
- Sets up custom markers for smoke and regression tests.
63-
- Generates Allure environment properties file for reporting.
64-
"""
65-
# Register custom markers for test categorization
66-
config.addinivalue_line("markers", "smoke: mark test as smoke test")
67-
config.addinivalue_line("markers", "regression: mark test as regression test")
68-
69-
# Create reports directory if it doesn't exist
70-
import os
71-
os.makedirs("reports", exist_ok=True)
72124

73-
# Create Allure environment properties file for reporting context
74-
with open("reports/environment.properties", "w") as f:
75-
f.write(f"BaseURL={load_config()['environments'][load_config()['env']]['base_url']}\n")
76-
f.write("Framework=API Test Automation\n")
77-
f.write("Language=Python\n")

tests/test_user_retrieval.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,7 @@
99
import requests
1010
import allure
1111
from jsonschema import validate
12-
from utils.api_utils import load_config, load_schema, get_headers
13-
14-
# Load base configuration from config.yaml
15-
config = load_config()
16-
BASE_URL = config["environments"][config["env"]]["base_url"]
12+
from utils.api_utils import load_schema, get_headers
1713

1814

1915
@pytest.mark.user_retrieval
@@ -22,22 +18,26 @@
2218
@allure.feature("User Retrieval")
2319
@allure.title("GET List of Users")
2420
@allure.severity(allure.severity_level.NORMAL)
25-
def test_get_user_list():
21+
def test_get_user_list(base_url):
2622
"""
2723
Test retrieving a paginated list of users.
2824
Validates response status and schema.
2925
"""
3026
with allure.step("Send GET request to retrieve user list"):
31-
url = f"{BASE_URL}/users?page=2"
27+
url = f"{base_url}/users?page=2"
3228
response = requests.get(url, headers=get_headers())
3329

3430
with allure.step("Verify response status is 200"):
35-
assert response.status_code == 200
31+
assert response.status_code == 200, f"Expected status 200, got {response.status_code}"
3632

3733
with allure.step("Validate response against schema"):
3834
data = response.json()
39-
schema = load_schema("user_list_schema.json")
40-
validate(instance=data, schema=schema)
35+
try:
36+
schema = load_schema("user_list_schema.json")
37+
validate(instance=data, schema=schema)
38+
except Exception as e:
39+
allure.attach(str(e), "Schema Validation Error", allure.attachment_type.TEXT)
40+
raise
4141

4242
with allure.step("Attach response details to report"):
4343
allure.attach(response.text, "Response Body", allure.attachment_type.JSON)
@@ -50,17 +50,17 @@ def test_get_user_list():
5050
@allure.title("GET Single User by ID")
5151
@allure.severity(allure.severity_level.CRITICAL)
5252
@pytest.mark.parametrize("user_id", [1, 2, 5])
53-
def test_get_single_user(user_id):
53+
def test_get_single_user(user_id, base_url):
5454
"""
5555
Test retrieving a single user by ID.
5656
Validates response status and schema.
5757
"""
5858
with allure.step(f"Send GET request to retrieve user {user_id}"):
59-
url = f"{BASE_URL}/users/{user_id}"
59+
url = f"{base_url}/users/{user_id}"
6060
response = requests.get(url, headers=get_headers())
6161

6262
with allure.step("Verify response status is 200"):
63-
assert response.status_code == 200
63+
assert response.status_code == 200, f"Expected status 200, got {response.status_code}"
6464

6565
with allure.step("Validate response against schema"):
6666
data = response.json()
@@ -81,17 +81,17 @@ def test_get_single_user(user_id):
8181
@allure.title("GET User List - Different Pages")
8282
@allure.severity(allure.severity_level.NORMAL)
8383
@pytest.mark.parametrize("page", [1, 2])
84-
def test_get_user_list_pagination(page):
84+
def test_get_user_list_pagination(page, base_url):
8585
"""
8686
Test retrieving user lists from different pages.
8787
Validates pagination functionality.
8888
"""
8989
with allure.step(f"Send GET request to retrieve page {page}"):
90-
url = f"{BASE_URL}/users?page={page}"
90+
url = f"{base_url}/users?page={page}"
9191
response = requests.get(url, headers=get_headers())
9292

9393
with allure.step("Verify response status is 200"):
94-
assert response.status_code == 200
94+
assert response.status_code == 200, f"Expected status 200, got {response.status_code}"
9595

9696
with allure.step("Validate response contains pagination info"):
9797
data = response.json()

0 commit comments

Comments
 (0)