Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/compatibility.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"schemaVersion": 1, "label": "compatibility", "message": "0.00%", "color": "brightgreen"}
41 changes: 39 additions & 2 deletions .github/workflows/test-openapi-directory.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
test-openapi-directory:
runs-on: ubuntu-latest
permissions:
contents: read
contents: write # Need write permission to commit the badge data
steps:
#----------------------------------------------
# check-out repo and set-up python
Expand Down Expand Up @@ -44,4 +44,41 @@ jobs:
#----------------------------------------------
- name: Test against APIs-guru/openapi-directory
run: |
uv run python3 test_openapi_directory.py --verbose
uv run python3 test_openapi_directory.py --verbose 2>&1 | tee test_output.txt

#----------------------------------------------
# Extract success rate and create badge data
#----------------------------------------------
- name: Extract compatibility percentage
run: |
# Extract the percentage from the test output using sed for portability
PERCENTAGE=$(sed -n 's/.*Success rate: \([0-9]*\(\.[0-9]*\)\?\)%.*/\1/p' test_output.txt)

# Check if extraction was successful
if [ -z "$PERCENTAGE" ]; then
echo "Error: Failed to extract percentage from test output"
echo "Falling back to previous value if exists, or using 0.00"
if [ -f .github/compatibility.json ]; then
echo "Keeping existing compatibility.json"
exit 0
else
PERCENTAGE="0.00"
fi
fi

# Create JSON file for shields.io endpoint badge
mkdir -p .github
echo "{\"schemaVersion\": 1, \"label\": \"compatibility\", \"message\": \"${PERCENTAGE}%\", \"color\": \"brightgreen\"}" > .github/compatibility.json

echo "Extracted compatibility: ${PERCENTAGE}%"

#----------------------------------------------
# Commit and push the badge data
#----------------------------------------------
- name: Commit compatibility data
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add .github/compatibility.json
git diff --staged --quiet || git commit -m "Update compatibility badge data: $(date -u '+%Y-%m-%d')"
git push
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[![Package version](https://img.shields.io/pypi/v/cicerone?color=%2334D058&label=latest%20version)](https://pypi.org/project/cicerone)
[![codecov](https://codecov.io/github/phalt/cicerone/graph/badge.svg?token=BAQE27Z4Y7)](https://codecov.io/github/phalt/cicerone)
![PyPI - License](https://img.shields.io/pypi/l/cicerone)
![OpenAPI Compatibility](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/phalt/cicerone/main/.github/compatibility.json)

Cicerone parses OpenAPI schemas into Pydantic models for introspection and traversal.

Expand Down
74 changes: 59 additions & 15 deletions test_openapi_directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,44 +46,68 @@ def find_schema_files(base_dir: pathlib.Path) -> List[pathlib.Path]:
return sorted(schema_files)


def test_schema_file(schema_path: pathlib.Path) -> Tuple[bool, str, Exception | None]:
def test_schema_file(schema_path: pathlib.Path) -> Tuple[str, str, Exception | None]:
"""Test parsing a single schema file.

Returns:
Tuple of (success: bool, error_message: str, exception: Exception | None)
Tuple of (status: str, error_message: str, exception: Exception | None)
where status is one of: "success", "skipped", "failed"
"""
try:
spec = cicerone_parse.parse_spec_from_file(schema_path)
# Basic validation - ensure we got a spec with some content
if spec is None:
return False, "Parsed spec is None", None
return True, "", None
return "failed", "Parsed spec is None", None

# Check if this is a Swagger 2.x file (even if cicerone auto-converts it)
# Cicerone preserves the original format in spec.raw
if "swagger" in spec.raw:
swagger_version = spec.raw["swagger"]
# Check if it's Swagger 2.x (2.0, 2.1, etc.)
try:
# Handle both string and numeric versions
version_str = str(swagger_version)
# Ensure version_str is not empty and has at least one part
if version_str and version_str.split(".")[0] == "2":
return "skipped", f"Swagger {version_str} (not supported, cicerone requires OpenAPI 3.x)", None
except (IndexError, ValueError):
pass # If we can't parse version, continue with normal processing

return "success", "", None
except Exception as e:
return False, f"{type(e).__name__}: {str(e)}", e
return "failed", f"{type(e).__name__}: {str(e)}", e


def test_all_schemas(
schema_files: List[pathlib.Path], base_dir: pathlib.Path, verbose: bool = False, fail_fast: bool = False
) -> Tuple[int, int, List[Tuple[pathlib.Path, str, Exception | None]]]:
) -> Tuple[int, int, int, List[Tuple[pathlib.Path, str, Exception | None]], List[Tuple[pathlib.Path, str]]]:
"""Test parsing all schema files.

Returns:
Tuple of (success_count, failure_count, failures_list)
Tuple of (success_count, skipped_count, failure_count, failures_list, skipped_list)
"""
print(f"\nTesting {len(schema_files)} schemas...")
successes = 0
skipped = []
failures = []

for i, schema_path in enumerate(schema_files, 1):
if verbose or i % 100 == 0:
print(f"Progress: {i}/{len(schema_files)} ({successes} successful, {len(failures)} failed)")
print(
f"Progress: {i}/{len(schema_files)} ({successes} successful, "
f"{len(skipped)} skipped, {len(failures)} failed)"
)

success, error, exception = test_schema_file(schema_path)
if success:
status, error, exception = test_schema_file(schema_path)
if status == "success":
successes += 1
if verbose:
print(f" ✓ {schema_path.relative_to(base_dir)}")
else:
elif status == "skipped":
skipped.append((schema_path, error))
if verbose:
print(f" ⊘ {schema_path.relative_to(base_dir)}: {error}")
else: # failed
failures.append((schema_path, error, exception))
if verbose:
print(f" ✗ {schema_path.relative_to(base_dir)}: {error}")
Expand All @@ -102,7 +126,7 @@ def test_all_schemas(
print(f"\nSchema location: {schema_path}")
break

return successes, len(failures), failures
return successes, len(skipped), len(failures), failures, skipped


def main() -> int:
Expand Down Expand Up @@ -140,18 +164,38 @@ def main() -> int:
schema_files = schema_files[: args.limit]

# Test all schemas
successes, failures_count, failures = test_all_schemas(
successes, skipped_count, failures_count, failures, skipped = test_all_schemas(
schema_files, repo_dir, verbose=args.verbose, fail_fast=args.fail_fast
)

# Print summary
print("\n" + "=" * 80)
print("SUMMARY")
print("=" * 80)
print(f"Total schemas tested: {len(schema_files)}")
print(f"Total schemas found: {len(schema_files)}")
print(f"Successful: {successes}")
print(f"Skipped (version incompatible): {skipped_count}")
print(f"Failed: {failures_count}")
print(f"Success rate: {successes / len(schema_files) * 100:.2f}%")

# Calculate success rate excluding skipped schemas
testable_count = len(schema_files) - skipped_count
if testable_count > 0:
success_rate = successes / testable_count * 100
print(f"Success rate: {success_rate:.2f}% ({successes}/{testable_count} testable schemas)")
else:
# Output 0.00% for consistency with badge extraction
print("Success rate: 0.00% (no testable schemas)")

if skipped:
print(f"\n{len(skipped)} schemas skipped due to version incompatibility:")
for schema_path, reason in skipped[:5]: # Show first 5 skipped
rel_path = schema_path.relative_to(repo_dir)
print(f" - {rel_path}")
if args.verbose:
print(f" Reason: {reason}")

if len(skipped) > 5:
print(f" ... and {len(skipped) - 5} more")

if failures:
print(f"\n{len(failures)} schemas failed to parse:")
Expand Down
Loading