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
42 changes: 42 additions & 0 deletions .github/workflows/lint-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Lint on Pull Request

# Controls when the action will run.
on:
# Triggers the workflow on pull request events
pull_request:
branches: [ main, release-* ]

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

# Add permissions for the workflow
permissions:
contents: read
pull-requests: read

jobs:
lint:
# The type of runner that the job will run on
runs-on: ubuntu-latest

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# 1. Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- name: Checkout Repository
uses: actions/checkout@v4

# 2. Sets up Python environment for the make command
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11' # Use a recent Python 3 version

# 3. Install system dependencies
- name: Install System Dependencies
run: |
sudo apt-get update
sudo apt-get install -y git curl

# 4. Run the make lint command
- name: Run Lint
run: make lint
53 changes: 53 additions & 0 deletions .github/workflows/test-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Test on Pull Request

# Controls when the action will run.
on:
# Triggers the workflow on pull request events
pull_request:
branches: [ main, release-* ]

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

# Add permissions for the workflow
permissions:
contents: read
pull-requests: read

jobs:
test:
# The type of runner that the job will run on
runs-on: ubuntu-latest

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# 1. Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- name: Checkout Repository
uses: actions/checkout@v4

# 2. Sets up Python environment for the make command
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11' # Use a recent Python 3 version

# 3. Install system dependencies
- name: Install System Dependencies
run: |
sudo apt-get update
sudo apt-get install -y git curl

# 4. Run the make test command
- name: Run Tests
run: make test

# 5. Optional: Upload test results as artifacts (if you have test reports)
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
api-docs/
*.log
retention-days: 7
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Byte-compiled / optimized / DLL files
__pycache__/
49 changes: 49 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ FORCE_DOWNLOAD ?= false
deps: ensure-gen-api-docs
@which python3 > /dev/null || (echo "Python3 not found. Attempting to install..." && (command -v apt-get >/dev/null 2>&1 && sudo apt-get update && sudo apt-get install -y python3) || (command -v yum >/dev/null 2>&1 && sudo yum install -y python3) || (command -v brew >/dev/null 2>&1 && brew install python3) || (echo "Automatic install failed. Please install Python3 manually." && exit 1))
@python3 -c "import yaml" 2>/dev/null || (echo "PyYAML not found. Installing..." && python3 -m pip install --user pyyaml)
@python3 -c "import flake8" 2>/dev/null || (echo "Flake8 not found. Installing..." && python3 -m pip install --user flake8)
@which curl > /dev/null || (echo "Curl not found. Attempting to install..." && (command -v apt-get >/dev/null 2>&1 && sudo apt-get update && sudo apt-get install -y curl) || (command -v yum >/dev/null 2>&1 && sudo yum install -y curl) || (command -v brew >/dev/null 2>&1 && brew install curl) || (echo "Automatic install failed. Please install Curl manually." && exit 1))
@which git > /dev/null || (echo "Git not found. Attempting to install..." && (command -v apt-get >/dev/null 2>&1 && sudo apt-get update && sudo apt-get install -y git) || (command -v yum >/dev/null 2>&1 && sudo yum install -y git) || (command -v brew >/dev/null 2>&1 && brew install git) || (echo "Automatic install failed. Please install Git manually." && exit 1))

Expand Down Expand Up @@ -50,3 +51,51 @@ gen-api-docs: setup
.PHONY: gen-api-docs-core
gen-api-docs-core: setup-core gen-api-docs remove-core-crds
echo "API docs generated successfully"

# Test targets
.PHONY: test
#test: test-crd-import test-helm-template-removal test-integration
test: test-crd-import test-helm-template-removal test-clean
@echo "✅ All tests passed!"

.PHONY: lint
lint: deps
@echo "Running Python lint tests..."
@python3 -m flake8 cmd/ tests/ --max-line-length=120 --ignore=E501,W503 --exclude=__pycache__

.PHONY: test-crd-import
test-crd-import: deps
@echo "Running CRD import tests..."
@python3 tests/run_tests.py --pattern test_crd_import.py

.PHONY: test-helm-template-removal
test-helm-template-removal: deps
@echo "Running Helm template removal tests..."
@python3 tests/run_tests.py --pattern test_helm_template_removal.py

.PHONY: test-integration
test-integration: deps
@echo "Running integration tests..."
@python3 tests/run_tests.py --pattern test_integration.py

.PHONY: test-verbose
test-verbose: deps
@echo "Running all tests with verbose output..."
@python3 tests/run_tests.py --verbose

.PHONY: test-clean
test-clean:
@echo "Cleaning up test artifacts..."
@find . -name "*.pyc" -delete
@find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
@rm -rf api-docs

# Development targets
.PHONY: dev-test
dev-test: test-clean test
@echo "Development test cycle completed"

.PHONY: validate
validate: test gen-api-docs
@echo "✅ Validation completed - all tests passed and API docs generated successfully"

22 changes: 15 additions & 7 deletions cmd/gen-api-docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
import os
import re
import yaml
from collections import defaultdict

# List of folder names to ignore during file search
IGNORED_FOLDERS = ['vendor', '.github', '.git', 'hack']
IGNORED_FOLDERS = ['vendor', '.github', '.git', 'hack', 'tests']

# Global search directory
search_dir = '.'


def is_primitive(go_type):
primitives = {'string', 'int', 'int32', 'int64', 'float32', 'float64', 'bool', 'byte', 'rune', 'uint', 'uint32', 'uint64', 'map', '[]byte', 'interface{}'}
# Also treat slices and maps of primitives as primitives
Expand All @@ -27,6 +27,7 @@ def is_primitive(go_type):
return True
return False


def find_go_struct(type_name, go_files):
# Look for a struct definition with the exact type name
struct_regex = re.compile(r'type ' + re.escape(type_name) + r' struct {([^}]*)}', re.MULTILINE | re.DOTALL)
Expand All @@ -52,6 +53,7 @@ def find_go_struct(type_name, go_files):
return struct_body, file_path, content
return None, None, None


def parse_go_struct(type_name, go_files, parsed_types):
if type_name in parsed_types:
return {'kind': type_name, 'fields': [{'name': '...', 'type': '', 'description': 'Recursive reference to ' + type_name, 'validations': []}]}
Expand Down Expand Up @@ -104,6 +106,7 @@ def parse_go_struct(type_name, go_files, parsed_types):
fields.append(field_info)
return {'kind': type_name, 'description': struct_comment, 'fields': fields}


def parse_go_file(file_path, go_files, parsed_types=None):
if parsed_types is None:
parsed_types = set()
Expand All @@ -118,6 +121,7 @@ def parse_go_file(file_path, go_files, parsed_types=None):
spec_struct_name = kind + 'Spec'
return parse_go_struct(spec_struct_name, go_files, parsed_types)


def parse_crd_file(file_path):
with open(file_path, 'r') as f:
content = f.read()
Expand All @@ -135,7 +139,6 @@ def parse_crd_file(file_path):
content = re.sub(r'\{\{-?[^}]+-?\}\}', '', content)
# Remove lines that are only whitespace after template removal
content = '\n'.join(line for line in content.split('\n') if line.strip())

try:
crd = yaml.safe_load(content)
except yaml.YAMLError as e:
Expand Down Expand Up @@ -190,7 +193,7 @@ def parse_schema_fields(schema):
field_schema = properties[field_name]
schema = field_schema.get('properties', {})
if 'description' in field_schema:
crd_info['description'+field_name] = field_schema['description']
crd_info['description' + field_name] = field_schema['description']
crd_info[field_name] = parse_schema_fields(schema)
except (KeyError, IndexError) as e:
print(f"Exception details: {e}")
Expand All @@ -199,12 +202,13 @@ def parse_schema_fields(schema):
pass
return crd_info


def render_fields(fields, go_files, depth=0):
md = ''
indent = ''

if depth > 0:
indent = " " * ((depth - 1) * 4) +"└>" + " " * 2
indent = " " * ((depth - 1) * 4) + "└>" + " " * 2

for field in fields:
if isinstance(field, str):
Expand All @@ -218,10 +222,11 @@ def render_fields(fields, go_files, depth=0):
description = field.get('description', 'No description provided.').replace('\n', ' ')
md += f"| {indent} **{field.get('name', 'N/A')}** | `{field.get('type', 'N/A')}` | {description} | {validations} |\n"
if 'inline' in field and len(field['inline']) > 0:
md += render_fields(field['inline']['fields'], go_files, depth+1)
md += render_fields(field['inline']['fields'], go_files, depth + 1)

return md


def generate_markdown(crd_info, output_dir, go_files):
kind = crd_info['kind']
file_path = os.path.join(output_dir, f"{kind.lower()}_api.md")
Expand All @@ -239,6 +244,7 @@ def generate_markdown(crd_info, output_dir, go_files):
f.write(render_fields(crd_info.get('status', []), go_files))
return f"{kind.lower()}_api.md"


def collect_go_type_files():
go_type_files = []
for root, _, files in os.walk(search_dir):
Expand All @@ -249,6 +255,7 @@ def collect_go_type_files():
go_type_files.append(os.path.join(root, file))
return go_type_files


def main():
import sys
global search_dir
Expand Down Expand Up @@ -314,5 +321,6 @@ def main():
f.write("---\n\n")
print(f"API documentation generated successfully in the '{api_docs_dir}' directory.")


if __name__ == "__main__":
main()
main()
98 changes: 98 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# gen-api-docs Tests

This directory contains comprehensive tests for the `gen-api-docs.py` script.

## Test Files

### `test_crd_import.py`
Tests for CRD (Custom Resource Definition) import functionality:
- Valid CRD parsing
- Invalid YAML handling
- Missing CRD fields
- Array properties
- Validation rules extraction
- Schema parsing

### `test_helm_template_removal.py`
Tests for Helm template code removal:
- Conditional template removal (`{{- if }}`)
- Mixed content handling
- Empty line cleanup

### `test_integration.py`
Integration tests for the complete workflow:
- CRD priority over `_types.go`
- Multiple resource processing
- Ignored folder handling
- Markdown generation format
- Complete end-to-end workflows

## Test Data

### `test_data/sample_crd.yaml`
A sample CRD file with various field types and validations for testing.

### `test_data/sample_types.go`
A sample `_types.go` file with various struct definitions, validations, and embedded types.

### `test_data/sample_helm_crd.yaml`
A sample CRD file with Helm templates for testing template removal functionality.

## Running Tests

### Run all tests:
```bash
make test
```

### Run individual tests:
```bash
make test-crd-import
make test-helm-template-removal
make test-integration
```

### Run with verbose output:
```bash
make test-verbose
```

## Test Coverage

The tests cover:

1. **CRD Import Validation**:
- Valid CRD parsing with all field types
- Invalid YAML handling
- Missing or malformed CRD structures
- Array and object property handling
- Validation rule extraction (minimum, maximum, pattern, enum)

3. **Helm Template Removal Validation**:
- All Helm template syntax patterns
- Conditional blocks (`{{- if }}`, `{{- else }}`, `{{- end }}`)
- Mixed template and YAML content
- Empty line cleanup after template removal

4. **Integration Testing**:
- Complete workflow validation
- CRD vs `_types.go` priority handling
- Multiple resource processing
- Ignored folder functionality
- Markdown generation format validation

## Expected Test Results

All tests should pass and validate that:
- CRD files are properly parsed and their schemas extracted
- Helm templates are completely removed while preserving valid YAML
- The integration workflow works correctly end-to-end
- Generated markdown has the correct format and content

## Troubleshooting

If tests fail:
1. Check that the `cmd/gen-api-docs.py` file is accessible
2. Ensure all required Python packages are installed (`yaml`, `unittest`)
3. Verify that test data files are present in `test_data/`
4. Check that the test directory structure matches the expected layout
Loading
Loading