diff --git a/.github/compatibility.json b/.github/compatibility.json new file mode 100644 index 0000000..d7fc723 --- /dev/null +++ b/.github/compatibility.json @@ -0,0 +1 @@ +{"schemaVersion": 1, "label": "compatibility", "message": "0.00%", "color": "brightgreen"} diff --git a/.github/workflows/test-openapi-directory.yml b/.github/workflows/test-openapi-directory.yml index 9f80999..8929d56 100644 --- a/.github/workflows/test-openapi-directory.yml +++ b/.github/workflows/test-openapi-directory.yml @@ -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 @@ -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 diff --git a/README.md b/README.md index d666744..d9532b6 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/test_openapi_directory.py b/test_openapi_directory.py index 4c0f943..7a8632e 100644 --- a/test_openapi_directory.py +++ b/test_openapi_directory.py @@ -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}") @@ -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: @@ -140,7 +164,7 @@ 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 ) @@ -148,10 +172,30 @@ def main() -> int: 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:")