diff --git a/.claude/skills/rubycritic/SKILL.md b/.claude/skills/rubycritic/SKILL.md index d69ce27..1c30472 100644 --- a/.claude/skills/rubycritic/SKILL.md +++ b/.claude/skills/rubycritic/SKILL.md @@ -3,72 +3,156 @@ name: rubycritic description: Integrate RubyCritic to analyze Ruby code quality and maintain high standards throughout development. Use when working on Ruby projects to check code smells, complexity, and duplication. Triggers include creating/editing Ruby files, refactoring code, reviewing code quality, or when user requests code analysis or quality checks. --- -# RubyCritic Code Quality Integration + +Maintain high code quality standards in Ruby projects by integrating RubyCritic analysis into the development workflow. Automatically detect code smells, complexity issues, and duplication, providing actionable guidance for improvements. + -This skill integrates RubyCritic to maintain high code quality standards in Ruby projects during Claude Code sessions. - -## Quick Start - -When working on Ruby code, periodically run RubyCritic to check code quality: + +Run quality check on Ruby files: ```bash scripts/check_quality.sh [path/to/ruby/files] ``` -If no path is provided, it analyzes the current directory. +If no path is provided, analyzes the current directory. The script automatically installs RubyCritic if missing. -## Workflow Integration +**Immediate feedback**: -### When to Run RubyCritic +- Overall score (0-100) +- File ratings (A-F) +- Specific code smells detected + -Run quality checks: + + +**Proactive analysis**: Run RubyCritic automatically after significant code changes: - After creating new Ruby files or classes -- After significant refactoring +- After implementing complex methods (>10 lines) +- After refactoring existing code +- Before marking tasks as complete - Before committing code -- When user explicitly requests code quality analysis -- After implementing complex methods or logic -### Interpreting Results +**Integration pattern**: -RubyCritic provides: +1. Make code changes +2. Run `scripts/check_quality.sh [changed_files]` +3. Review output for issues +4. Address critical smells (if any) +5. Re-run to verify improvements +6. Proceed with next task -- **Overall Score**: Project-wide quality rating (0-100) -- **File Ratings**: A-F letter grades per file -- **Code Smells**: Specific issues detected by Reek -- **Complexity**: Flog scores indicating method complexity -- **Duplication**: Flay scores showing code duplication +**When to skip**: Simple variable renames, comment changes, or minor formatting adjustments don't require quality checks. + -### Quality Thresholds + +**Overall Score**: -Aim for: +- 95+ (excellent) - Maintain this standard +- 90-94 (good) - Minor improvements possible +- 80-89 (acceptable) - Consider refactoring +- Below 80 - Prioritize improvements -- **Overall Score**: 95+ (excellent), 90+ (good), 80+ (acceptable) -- **File Ratings**: A or B ratings for all files -- **No Critical Smells**: Address any high-priority issues immediately +**File Ratings**: -### Responding to Issues +- A/B - Acceptable quality +- C - Needs attention +- D/F - Requires refactoring -When RubyCritic identifies problems: +**Issue Types**: -1. **Review the console output** for specific issues -2. **Prioritize critical smells** (complexity, duplication, unclear naming) -3. **Refactor incrementally** - fix issues one at a time -4. **Re-run analysis** after each fix to verify improvement -5. **Explain changes** to the user if quality improves significantly +- **Code Smells** (Reek) - Design and readability issues +- **Complexity** (Flog) - Overly complex methods +- **Duplication** (Flay) - Repeated code patterns + -## Installation Handling + +**Priority order**: -The check_quality.sh script automatically: +1. **Critical smells** - Long parameter lists, high complexity, feature envy +2. **Duplication** - Extract shared methods or modules +3. **Minor smells** - Unused parameters, duplicate method calls +4. **Style issues** - Naming, organization -- Detects if RubyCritic is installed -- Installs it if missing (with user awareness) -- Uses bundler if Gemfile is present -- Falls back to system gem installation +**Incremental fixing**: + +- Fix one issue at a time +- Run analysis after each fix +- Verify score improves +- Explain significant improvements to user + +**When scores drop**: + +- Identify which file/method caused the drop +- Review recent changes in that area +- Fix immediately before continuing +- Don't accumulate technical debt + + + +**Common errors and solutions**: + +**"RubyCritic not found"**: Script auto-installs, but if it fails: + +- Check Ruby is installed: `ruby --version` +- Manually install: `gem install rubycritic` +- Or add to Gemfile: `gem 'rubycritic', require: false` + +**"No files to analyze"**: Verify path contains `.rb` files + +- Check path is correct +- Use explicit path: `scripts/check_quality.sh app/models` + +**"Bundler error"**: Gemfile.lock conflict + +- Run `bundle install` first +- Or use system gem: `gem install rubycritic && rubycritic [path]` + +**Analysis hangs**: Large codebase -## Configuration +- Analyze specific directories instead of entire project +- Use `--no-browser` flag to skip HTML generation +- Consider `.rubycritic.yml` to exclude paths -RubyCritic respects `.rubycritic.yml` if present in the project. For custom configuration, create this file with options like: +For additional error scenarios, see [references/error-handling.md](references/error-handling.md) + + + + +**Pre-commit quality checks**: Automatically run RubyCritic before commits: + +```bash +# .git/hooks/pre-commit +#!/bin/bash +# Get staged Ruby files +RUBY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.rb$') + +if [ -n "$RUBY_FILES" ]; then + echo "Running RubyCritic on staged files..." + scripts/check_quality.sh $RUBY_FILES + + if [ $? -ne 0 ]; then + echo "Quality check failed. Fix issues or use --no-verify to skip." + exit 1 + fi +fi +``` + +**CI integration**: Add to GitHub Actions, GitLab CI, or other CI systems: + +```yaml +# .github/workflows/quality.yml +- name: Run RubyCritic + run: | + gem install rubycritic + rubycritic --format json --minimum-score 90 +``` + +For complete git hooks setup and CI examples, see [references/git-hooks.md](references/git-hooks.md) + + + +**Basic configuration** (`.rubycritic.yml`): ```yaml minimum_score: 95 @@ -80,21 +164,102 @@ paths: no_browser: true ``` -## Output Formats +**Common options**: + +- `minimum_score`: Fail if score below this threshold +- `formats`: Output formats (console, html, json) +- `paths`: Directories to analyze +- `no_browser`: Don't auto-open HTML report + +For advanced configuration and custom thresholds, see [references/configuration.md](references/configuration.md) + + + +**Quick quality check during development**: + +```bash +# Check recently modified files +scripts/check_quality.sh $(git diff --name-only | grep '\.rb$') +``` + +**Generate detailed HTML report**: + +```bash +bundle exec rubycritic --format html app/ +# Opens browser with detailed analysis +``` + +**Compare with main branch** (CI mode): + +```bash +rubycritic --mode-ci --branch main app/ +# Shows only changes from main branch +``` + +**Check specific file types**: + +```bash +scripts/check_quality.sh app/models/*.rb +scripts/check_quality.sh app/services/**/*.rb +``` + + -The script uses console format by default for inline feedback. For detailed reports: + +For detailed examples of common code smells and how to fix them, see [references/code_smells.md](references/code_smells.md) + +**Quick reference**: + +- **Control Parameter** - Replace boolean params with separate methods +- **Feature Envy** - Move method to the class it uses most +- **Long Parameter List** - Use parameter objects or hashes +- **High Complexity** - Extract methods, use early returns +- **Duplication** - Extract to shared methods or modules + + + +**Automatic installation**: The `check_quality.sh` script handles installation automatically: + +- Detects if RubyCritic is installed +- Uses bundler if Gemfile present +- Falls back to system gem installation +- Adds to Gemfile development group if needed + +**Manual installation**: + +With Bundler: + +```ruby +# Gemfile +group :development do + gem 'rubycritic', require: false +end +``` + +System-wide: + +```bash +gem install rubycritic +``` -- HTML report: `rubycritic --format html [paths]` -- JSON output: `rubycritic --format json [paths]` + -## Best Practices + +RubyCritic is successfully integrated when: -1. **Run early and often** - Catch issues before they multiply -2. **Address issues immediately** - Don't let technical debt accumulate -3. **Explain to users** - When fixing quality issues, briefly explain what was improved -4. **Set baselines** - On new projects, establish quality standards early -5. **CI mode** - For comparing branches: `--mode-ci --branch main` +- Quality checks run automatically after significant code changes +- Overall score maintained at 90+ (or project-defined threshold) +- Critical code smells addressed immediately +- Quality improvements explained to user when significant +- No quality regressions introduced by changes +- Files maintain A or B ratings + -## Bundled Resources + +**Detailed references**: -- **scripts/check_quality.sh**: Automated quality check with installation handling +- [references/code_smells.md](references/code_smells.md) - Common smells and fixes with examples +- [references/configuration.md](references/configuration.md) - Advanced RubyCritic configuration +- [references/git-hooks.md](references/git-hooks.md) - Pre-commit hooks and CI integration +- [references/error-handling.md](references/error-handling.md) - Troubleshooting common errors + diff --git a/.claude/skills/rubycritic/references/configuration.md b/.claude/skills/rubycritic/references/configuration.md new file mode 100644 index 0000000..99f7616 --- /dev/null +++ b/.claude/skills/rubycritic/references/configuration.md @@ -0,0 +1,396 @@ +# RubyCritic Configuration Reference + +This guide covers advanced RubyCritic configuration options for customizing analysis behavior, output formats, and quality thresholds. + +## Configuration File + +Create `.rubycritic.yml` in your project root: + +```yaml +# .rubycritic.yml + +# Minimum acceptable score (0-100) +minimum_score: 95 + +# Output formats (can specify multiple) +formats: + - console # Terminal output + - html # HTML report in tmp/rubycritic + - json # JSON output for CI/tooling + +# Paths to analyze +paths: + - 'app/' + - 'lib/' + - 'spec/' + +# Paths to exclude +exclude_paths: + - 'db/migrate/**/*' + - 'config/**/*' + - 'vendor/**/*' + +# Don't auto-open browser for HTML reports +no_browser: true + +# Suppress output (useful for CI) +suppress_ratings: false + +# CI mode options +mode: default # or 'ci' for CI mode + +# Branch to compare against (CI mode) +branch: main + +# Deduplicate similar smells +deduplicate_symlinks: true +``` + +## Common Configuration Patterns + +### Strict Quality Standards + +For new projects or teams prioritizing code quality: + +```yaml +minimum_score: 95 +formats: + - console + - html +paths: + - 'app/' + - 'lib/' +no_browser: true +suppress_ratings: false +``` + +### CI/CD Integration + +For continuous integration environments: + +```yaml +minimum_score: 90 +formats: + - json + - console +mode: ci +branch: main +no_browser: true +suppress_ratings: true +exclude_paths: + - 'db/migrate/**/*' + - 'spec/**/*' +``` + +### Legacy Codebase + +For existing projects with technical debt: + +```yaml +minimum_score: 70 # Lower threshold +formats: + - html + - console +paths: + - 'app/models' + - 'app/services' +exclude_paths: + - 'app/controllers/**/*' # Exclude problematic areas temporarily + - 'lib/legacy/**/*' +no_browser: false # Open reports for review +``` + +### Development Mode + +For active development with fast feedback: + +```yaml +minimum_score: 85 +formats: + - console +no_browser: true +suppress_ratings: false +deduplicate_symlinks: true +``` + +## Command-Line Options + +Override configuration file with CLI options: + +```bash +# Set minimum score +rubycritic --minimum-score 90 app/ + +# Specify format +rubycritic --format html app/ + +# CI mode +rubycritic --mode-ci --branch main app/ + +# Suppress browser opening +rubycritic --no-browser app/ + +# Multiple formats +rubycritic --format console --format json app/ + +# Custom paths +rubycritic app/models app/services + +# Help +rubycritic --help +``` + +## Output Formats + +### Console Format + +Terminal-friendly output with immediate feedback: + +```bash +rubycritic --format console app/ +``` + +Output includes: +- Overall score +- File-by-file ratings +- List of code smells +- Complexity metrics + +### HTML Format + +Detailed browser-based report: + +```bash +rubycritic --format html app/ +``` + +Features: +- Interactive file browser +- Visual complexity graphs +- Clickable code smells +- Historical trends (if run multiple times) +- Saved to `tmp/rubycritic/index.html` + +### JSON Format + +Machine-readable output for tooling integration: + +```bash +rubycritic --format json app/ > quality_report.json +``` + +Use cases: +- CI/CD pipeline parsing +- Custom reporting tools +- Quality metrics tracking +- Integration with dashboards + +## CI Mode + +Compare changes against a base branch: + +```bash +rubycritic --mode-ci --branch main app/ +``` + +**Benefits**: +- Only analyzes changed files +- Shows quality delta +- Faster on large codebases +- Focuses on new issues + +**Configuration**: +```yaml +mode: ci +branch: main +minimum_score: 90 +``` + +## Score Calculation + +RubyCritic calculates scores based on: + +1. **Reek** - Code smell detection (40% weight) +2. **Flog** - Complexity analysis (30% weight) +3. **Flay** - Duplication detection (30% weight) + +### Score Ranges + +- **90-100**: Excellent - exemplary code quality +- **80-89**: Good - minor improvements recommended +- **70-79**: Fair - some technical debt present +- **60-69**: Poor - significant refactoring needed +- **0-59**: Critical - major quality issues + +### Custom Thresholds + +Set thresholds based on project maturity: + +**New projects**: 95+ +**Active development**: 90+ +**Established projects**: 85+ +**Legacy codebases**: 70+ (with improvement plan) + +## Excluding Paths + +### Temporary Exclusions + +Exclude paths during active development: + +```yaml +exclude_paths: + - 'app/controllers/legacy_controller.rb' + - 'lib/deprecated/**/*' +``` + +### Permanent Exclusions + +Exclude paths that shouldn't be analyzed: + +```yaml +exclude_paths: + - 'db/migrate/**/*' # Migrations + - 'db/schema.rb' # Auto-generated + - 'config/**/*' # Configuration + - 'vendor/**/*' # Third-party code + - 'bin/**/*' # Scripts + - 'spec/fixtures/**/*' # Test fixtures + - 'spec/support/shared/**/*' # Test helpers +``` + +## Integration with Other Tools + +### RuboCop Integration + +RubyCritic complements RuboCop: + +- **RuboCop**: Style and syntax enforcement +- **RubyCritic**: Complexity and design analysis + +Run both: +```bash +rubocop app/ && rubycritic app/ +``` + +### SimpleCov Integration + +Combine with test coverage: + +```bash +# Run tests with coverage +bundle exec rspec + +# Then analyze code quality +rubycritic app/ +``` + +### CI Pipeline Integration + +```yaml +# .github/workflows/quality.yml +name: Code Quality + +on: [push, pull_request] + +jobs: + quality: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + bundler-cache: true + + - name: Run RubyCritic + run: | + gem install rubycritic + rubycritic --format json --minimum-score 90 app/ lib/ + + - name: Upload Report + uses: actions/upload-artifact@v2 + with: + name: rubycritic-report + path: tmp/rubycritic/ +``` + +## Performance Optimization + +For large codebases: + +### Analyze Specific Directories + +```bash +# Only analyze changed areas +rubycritic app/models app/services +``` + +### Use CI Mode + +```bash +# Only analyze changed files +rubycritic --mode-ci --branch main +``` + +### Exclude Non-Critical Paths + +```yaml +paths: + - 'app/models' + - 'app/services' + - 'lib/core' +# Exclude specs, migrations, config +``` + +### Disable Browser Opening + +```yaml +no_browser: true +``` + +## Troubleshooting Configuration + +### Configuration Not Loading + +Verify file location and syntax: +```bash +# Check if file exists +ls -la .rubycritic.yml + +# Validate YAML syntax +ruby -e "require 'yaml'; YAML.load_file('.rubycritic.yml')" +``` + +### Scores Too Low + +Adjust thresholds based on codebase maturity: +```yaml +minimum_score: 80 # More lenient +``` + +### Too Many Exclusions + +Review exclusions periodically: +```bash +# List excluded paths +grep -A 10 "exclude_paths:" .rubycritic.yml +``` + +### Performance Issues + +Reduce analysis scope: +```yaml +paths: + - 'app/models' # Start small + - 'app/services' +``` + +## Best Practices + +1. **Version control**: Commit `.rubycritic.yml` to repository +2. **Team alignment**: Agree on minimum score thresholds +3. **Gradual improvement**: Start with lower threshold, increase over time +4. **Focused analysis**: Analyze specific paths for faster feedback +5. **CI integration**: Run on every PR to prevent regressions +6. **Historical tracking**: Keep HTML reports for trend analysis +7. **Regular review**: Adjust configuration as project evolves diff --git a/.claude/skills/rubycritic/references/error-handling.md b/.claude/skills/rubycritic/references/error-handling.md new file mode 100644 index 0000000..d79f043 --- /dev/null +++ b/.claude/skills/rubycritic/references/error-handling.md @@ -0,0 +1,653 @@ +# RubyCritic Error Handling and Troubleshooting + +This guide covers common errors, edge cases, and troubleshooting strategies when using RubyCritic. + +## Installation Errors + +### "RubyCritic not found" + +**Symptom**: Command `rubycritic` not recognized + +**Causes**: +- RubyCritic not installed +- Not in PATH (when using Bundler) +- Wrong Ruby version active + +**Solutions**: + +```bash +# Check if installed +which rubycritic + +# Install system-wide +gem install rubycritic + +# Or add to Gemfile +# Gemfile +group :development do + gem 'rubycritic', require: false +end + +# Then install +bundle install + +# Use with Bundler +bundle exec rubycritic app/ +``` + +### "Gem::InstallError: You don't have write permissions" + +**Symptom**: Permission denied when installing gem + +**Solutions**: + +```bash +# Option 1: Use bundler (recommended) +bundle install + +# Option 2: Install to user directory +gem install --user-install rubycritic + +# Option 3: Use rbenv/rvm (recommended for development) +rbenv install 3.2.0 +rbenv global 3.2.0 +gem install rubycritic +``` + +### "Bundler::GemNotFound: Could not find gem 'rubycritic'" + +**Symptom**: Gem not in Gemfile.lock + +**Solutions**: + +```bash +# Add to Gemfile +echo "gem 'rubycritic', require: false, group: :development" >> Gemfile + +# Update bundle +bundle install + +# Verify installation +bundle exec rubycritic --version +``` + +### "LoadError: cannot load such file" + +**Symptom**: Missing dependencies for RubyCritic + +**Solutions**: + +```bash +# Update bundler +bundle update + +# Re-install RubyCritic +gem uninstall rubycritic +gem install rubycritic + +# Check Ruby version compatibility +ruby --version # Should be 2.7+ +``` + +## Analysis Errors + +### "No files to critique" + +**Symptom**: RubyCritic finds no Ruby files to analyze + +**Causes**: +- Wrong path specified +- No `.rb` files in path +- Path excluded in configuration + +**Solutions**: + +```bash +# Verify path contains Ruby files +ls -la app/*.rb + +# Check explicit file extension +rubycritic app/**/*.rb + +# Verify configuration exclusions +cat .rubycritic.yml | grep -A 5 exclude_paths + +# Use absolute paths +rubycritic $(pwd)/app/ +``` + +### "Analysis timed out" + +**Symptom**: RubyCritic hangs or takes extremely long + +**Causes**: +- Very large codebase +- Infinite loops in analyzed code +- Resource constraints + +**Solutions**: + +```bash +# Analyze smaller directories +rubycritic app/models/ + +# Use CI mode for faster analysis +rubycritic --mode-ci --branch main app/ + +# Increase timeout (if available in your version) +# Or split into multiple runs +rubycritic app/models/ +rubycritic app/services/ +rubycritic app/controllers/ + +# Check for problematic files +# Analyze one directory at a time to identify issues +``` + +### "Invalid multibyte char (UTF-8)" + +**Symptom**: Encoding errors when analyzing files + +**Causes**: +- Files with invalid UTF-8 encoding +- Mixed encodings in codebase + +**Solutions**: + +```bash +# Find files with encoding issues +find . -name "*.rb" -exec file {} \; | grep -v "UTF-8" + +# Fix encoding in problematic files +# Add magic comment at top of file: +# frozen_string_literal: true +# encoding: UTF-8 + +# Or convert file encoding +iconv -f ISO-8859-1 -t UTF-8 problematic_file.rb -o fixed_file.rb +``` + +### "SyntaxError: unexpected token" + +**Symptom**: RubyCritic fails on valid Ruby syntax + +**Causes**: +- Unsupported Ruby version features +- RubyCritic version too old +- Actually invalid syntax in code + +**Solutions**: + +```bash +# Check RubyCritic version +rubycritic --version + +# Update RubyCritic +gem update rubycritic + +# Verify syntax is actually valid +ruby -c app/models/user.rb + +# Check Ruby version compatibility +ruby --version +``` + +## Configuration Errors + +### "Invalid YAML in .rubycritic.yml" + +**Symptom**: Configuration file not parsed correctly + +**Solutions**: + +```bash +# Validate YAML syntax +ruby -e "require 'yaml'; YAML.load_file('.rubycritic.yml')" + +# Common YAML issues: +# - Wrong indentation (use spaces, not tabs) +# - Missing quotes around special characters +# - Incorrect list syntax + +# Example of common error: +# ❌ Wrong +paths: +- app/ # Missing space after hyphen + +# ✅ Correct +paths: + - 'app/' +``` + +### "Unrecognized option" + +**Symptom**: Command-line option not recognized + +**Causes**: +- Typo in option name +- Option not available in RubyCritic version +- Incorrect option format + +**Solutions**: + +```bash +# Check available options +rubycritic --help + +# Verify correct format +rubycritic --minimum-score 90 # Correct +rubycritic --minimum_score 90 # Wrong (underscore) + +# Check version supports option +rubycritic --version +``` + +### "Configuration file not found" + +**Symptom**: RubyCritic doesn't load `.rubycritic.yml` + +**Solutions**: + +```bash +# Verify file exists in project root +ls -la .rubycritic.yml + +# Check file permissions +chmod 644 .rubycritic.yml + +# Use absolute path in config +pwd # Get current directory +# Ensure running RubyCritic from project root + +# Debug: Run with explicit paths +rubycritic --format console app/ +``` + +## Output Errors + +### "Permission denied writing to tmp/" + +**Symptom**: Cannot write HTML report + +**Solutions**: + +```bash +# Check tmp directory permissions +ls -la tmp/ + +# Create directory if missing +mkdir -p tmp/rubycritic + +# Fix permissions +chmod 755 tmp/ + +# Or use different output directory +rubycritic --path ./reports app/ +``` + +### "Browser failed to open" + +**Symptom**: HTML report generated but browser doesn't open + +**Solutions**: + +```bash +# Use --no-browser flag +rubycritic --no-browser app/ + +# Manually open report +open tmp/rubycritic/index.html # macOS +xdg-open tmp/rubycritic/index.html # Linux + +# Add to configuration +# .rubycritic.yml +no_browser: true +``` + +### "JSON output malformed" + +**Symptom**: JSON format produces invalid JSON + +**Solutions**: + +```bash +# Validate JSON output +rubycritic --format json app/ | jq . + +# If jq not installed +rubycritic --format json app/ > output.json +ruby -e "require 'json'; JSON.parse(File.read('output.json'))" + +# Use console format for debugging +rubycritic --format console app/ +``` + +## Score Calculation Issues + +### "Score unexpectedly low" + +**Symptom**: Quality score lower than expected + +**Investigation**: + +```bash +# Get detailed breakdown +rubycritic --format html app/ +# Open HTML report to see which files/metrics are low + +# Check individual analyzers +# Look for: +# - High Flog scores (complexity) +# - Many Reek smells +# - Flay duplications + +# Focus on worst files first +rubycritic --format console app/ | grep "F:" +``` + +**Common causes**: +- Long methods (>10 lines) +- High cyclomatic complexity +- Many parameters (>3) +- Duplicate code +- Feature envy (using other classes' methods) + +### "Score changes between runs" + +**Symptom**: Inconsistent scores for same code + +**Causes**: +- Different file sets analyzed +- Configuration changes +- RubyCritic version differences + +**Solutions**: + +```bash +# Use consistent paths +rubycritic app/ lib/ # Always specify same paths + +# Lock configuration +# Commit .rubycritic.yml to version control + +# Use same version +# Add to Gemfile with version constraint +gem 'rubycritic', '~> 4.7', require: false +``` + +### "Cannot determine score from output" + +**Symptom**: Parsing tools can't extract score + +**Solutions**: + +```bash +# Use JSON format for machine parsing +rubycritic --format json app/ > quality.json + +# Parse JSON for score +ruby -e " + require 'json' + data = JSON.parse(File.read('quality.json')) + puts data['score'] +" + +# Or use grep with console format +rubycritic --format console app/ | grep -oP 'Score: \K\d+' +``` + +## Integration Errors + +### "Git hook not executing" + +**Symptom**: Pre-commit/pre-push hook not running + +**Solutions**: + +```bash +# Check if hook exists +ls -la .git/hooks/pre-commit + +# Verify executable permissions +chmod +x .git/hooks/pre-commit + +# Test hook manually +.git/hooks/pre-commit + +# Check shebang line +head -1 .git/hooks/pre-commit +# Should be: #!/bin/bash + +# Verify git config allows hooks +git config --get core.hooksPath +``` + +### "CI build failing but local passes" + +**Symptom**: Quality check passes locally but fails in CI + +**Causes**: +- Different RubyCritic versions +- Different file sets analyzed +- Configuration not in version control + +**Solutions**: + +```bash +# Pin RubyCritic version in Gemfile +gem 'rubycritic', '~> 4.7.0', require: false + +# Ensure configuration is committed +git add .rubycritic.yml +git commit -m "Add RubyCritic config" + +# Use same command locally and in CI +bundle exec rubycritic app/ lib/ + +# Debug: Compare versions +rubycritic --version # Local +# vs CI output + +# Check for ignored files in .gitignore +# That might exist locally but not in CI +``` + +### "Bundler can't find rubycritic in CI" + +**Symptom**: CI can't install or run RubyCritic + +**Solutions**: + +```yaml +# Ensure proper setup in CI +steps: + - name: Install dependencies + run: bundle install + + - name: Run RubyCritic + run: bundle exec rubycritic app/ + +# Or install separately +steps: + - name: Install RubyCritic + run: gem install rubycritic + + - name: Run analysis + run: rubycritic app/ +``` + +## Performance Issues + +### "Analysis takes too long" + +**Symptom**: RubyCritic runs for minutes/hours + +**Solutions**: + +```bash +# Profile which directories are slow +time rubycritic app/models/ +time rubycritic app/services/ +time rubycritic app/controllers/ + +# Exclude large/generated files +# .rubycritic.yml +exclude_paths: + - 'app/assets/**/*' + - 'db/**/*' + - 'spec/**/*' + +# Use CI mode on branches +rubycritic --mode-ci --branch main app/ + +# Analyze only changed files in git hook +git diff --cached --name-only | grep '\.rb$' | xargs rubycritic +``` + +### "Running out of memory" + +**Symptom**: RubyCritic crashes with memory errors + +**Solutions**: + +```bash +# Increase Ruby memory limit +RUBY_GC_HEAP_GROWTH_FACTOR=1.1 rubycritic app/ + +# Analyze in smaller batches +for dir in app/*/; do + rubycritic "$dir" +done + +# Reduce scope +# Only analyze app/ not spec/ +rubycritic app/ + +# Use CI mode (more efficient) +rubycritic --mode-ci --branch main app/ +``` + +## Debugging Strategies + +### Enable Verbose Output + +```bash +# Run with verbose mode (if available) +rubycritic --verbose app/ + +# Check RubyCritic logs +# Location varies by OS +cat tmp/rubycritic.log +``` + +### Isolate Problematic Files + +```bash +# Binary search approach +# Split files in half, find which half has issues + +# Test individual files +for file in app/models/*.rb; do + echo "Analyzing $file" + rubycritic "$file" || echo "Failed on $file" +done +``` + +### Check Dependencies + +```bash +# Verify all RubyCritic dependencies +bundle exec gem dependency rubycritic + +# Update dependencies +bundle update rubycritic + +# Clean and reinstall +gem uninstall rubycritic +bundle install +``` + +### Compare with Fresh Environment + +```bash +# Create new gemset/environment +# Test if issue persists + +# Docker test +docker run -it ruby:3.2 bash +gem install rubycritic +# Test analysis +``` + +## Getting Help + +### Gather Debug Information + +When reporting issues: + +```bash +# Collect version info +ruby --version +rubycritic --version +bundle --version + +# Configuration +cat .rubycritic.yml + +# Command used +echo "rubycritic app/" + +# Error output +rubycritic app/ 2>&1 | tee error.log + +# System info +uname -a +``` + +### Resources + +- **GitHub Issues**: https://github.com/whitesmith/rubycritic/issues +- **Documentation**: https://github.com/whitesmith/rubycritic +- **Stack Overflow**: Tag `rubycritic` + +## Prevention Best Practices + +1. **Pin versions**: Lock RubyCritic version in Gemfile +2. **Test configuration**: Validate `.rubycritic.yml` syntax +3. **Incremental adoption**: Start with small directories +4. **Monitor performance**: Track analysis time +5. **Document exceptions**: Comment why files are excluded +6. **Version control config**: Commit configuration files +7. **CI validation**: Test hooks in CI environment +8. **Regular updates**: Keep RubyCritic updated +9. **Team training**: Document common issues +10. **Fallback plans**: Have manual quality review process + +## Quick Reference + +### Emergency Fixes + +```bash +# Skip quality check temporarily +git commit --no-verify + +# Force run with basic settings +rubycritic --format console --no-browser app/ + +# Minimal analysis +rubycritic app/models/user.rb + +# Reset configuration +rm .rubycritic.yml +rubycritic app/ +``` + +### Health Check + +```bash +# Verify setup is working +rubycritic --version && echo "✓ Installed" +[ -f .rubycritic.yml ] && echo "✓ Configured" +ruby -c .git/hooks/pre-commit && echo "✓ Hook valid" +bundle exec rubycritic --help && echo "✓ Bundler OK" +``` diff --git a/.claude/skills/rubycritic/references/git-hooks.md b/.claude/skills/rubycritic/references/git-hooks.md new file mode 100644 index 0000000..fb49657 --- /dev/null +++ b/.claude/skills/rubycritic/references/git-hooks.md @@ -0,0 +1,618 @@ +# Git Hooks and CI Integration + +This guide covers integrating RubyCritic into git workflows and continuous integration pipelines to maintain code quality automatically. + +## Pre-Commit Hook + +Automatically run RubyCritic on staged files before commits. + +### Basic Pre-Commit Hook + +Create `.git/hooks/pre-commit`: + +```bash +#!/bin/bash + +# Get staged Ruby files +RUBY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.rb$') + +if [ -z "$RUBY_FILES" ]; then + # No Ruby files staged, skip check + exit 0 +fi + +echo "Running RubyCritic on staged files..." +echo "$RUBY_FILES" +echo "" + +# Run RubyCritic on staged files +if [ -f "scripts/check_quality.sh" ]; then + scripts/check_quality.sh $RUBY_FILES +else + bundle exec rubycritic --format console --no-browser $RUBY_FILES +fi + +RESULT=$? + +if [ $RESULT -ne 0 ]; then + echo "" + echo "❌ Quality check failed!" + echo "Fix the issues above or use 'git commit --no-verify' to skip this check." + exit 1 +fi + +echo "✅ Quality check passed!" +exit 0 +``` + +Make it executable: +```bash +chmod +x .git/hooks/pre-commit +``` + +### Pre-Commit Hook with Threshold + +Only fail on severe quality issues: + +```bash +#!/bin/bash + +RUBY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.rb$') + +if [ -z "$RUBY_FILES" ]; then + exit 0 +fi + +echo "Running RubyCritic on staged files..." + +# Run and capture output +OUTPUT=$(bundle exec rubycritic --format console --no-browser $RUBY_FILES 2>&1) +echo "$OUTPUT" + +# Extract score from output +SCORE=$(echo "$OUTPUT" | grep -oP 'Score: \K\d+' | head -1) + +if [ -z "$SCORE" ]; then + echo "⚠️ Could not determine quality score" + exit 0 # Don't block commit if we can't get score +fi + +MINIMUM_SCORE=85 + +if [ "$SCORE" -lt "$MINIMUM_SCORE" ]; then + echo "" + echo "❌ Quality score $SCORE is below minimum $MINIMUM_SCORE" + echo "Please improve code quality or use --no-verify to skip." + exit 1 +fi + +echo "✅ Quality score: $SCORE (minimum: $MINIMUM_SCORE)" +exit 0 +``` + +### Pre-Commit Hook with Selective Analysis + +Only analyze files in critical directories: + +```bash +#!/bin/bash + +RUBY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.rb$') + +if [ -z "$RUBY_FILES" ]; then + exit 0 +fi + +# Filter for critical paths only +CRITICAL_FILES=$(echo "$RUBY_FILES" | grep -E '^(app/models|app/services|lib)/') + +if [ -z "$CRITICAL_FILES" ]; then + echo "No critical files changed, skipping quality check" + exit 0 +fi + +echo "Running RubyCritic on critical files:" +echo "$CRITICAL_FILES" +echo "" + +bundle exec rubycritic --format console --no-browser $CRITICAL_FILES + +if [ $? -ne 0 ]; then + echo "❌ Quality check failed!" + exit 1 +fi + +echo "✅ Quality check passed!" +exit 0 +``` + +## Pre-Push Hook + +Run comprehensive analysis before pushing to remote: + +Create `.git/hooks/pre-push`: + +```bash +#!/bin/bash + +echo "Running comprehensive quality check before push..." +echo "" + +# Get all commits being pushed +while read local_ref local_sha remote_ref remote_sha +do + if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then + # Branch is being deleted, skip + continue + fi + + if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then + # New branch, compare with main + RANGE="main..$local_sha" + else + # Existing branch, compare with remote + RANGE="$remote_sha..$local_sha" + fi + + # Get changed Ruby files + RUBY_FILES=$(git diff --name-only $RANGE | grep '\.rb$') + + if [ -n "$RUBY_FILES" ]; then + echo "Analyzing changed files:" + echo "$RUBY_FILES" + echo "" + + bundle exec rubycritic --format console --no-browser $RUBY_FILES + + if [ $? -ne 0 ]; then + echo "" + echo "❌ Quality check failed!" + echo "Fix issues or use 'git push --no-verify' to skip." + exit 1 + fi + fi +done + +echo "✅ All quality checks passed!" +exit 0 +``` + +Make it executable: +```bash +chmod +x .git/hooks/pre-push +``` + +## Commit Message Hook + +Add quality score to commit message automatically: + +Create `.git/hooks/prepare-commit-msg`: + +```bash +#!/bin/bash + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 + +# Skip if amending or using a message from another source +if [ "$COMMIT_SOURCE" = "message" ] || [ "$COMMIT_SOURCE" = "merge" ]; then + exit 0 +fi + +# Get staged Ruby files +RUBY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.rb$') + +if [ -z "$RUBY_FILES" ]; then + exit 0 +fi + +# Run RubyCritic and capture score +OUTPUT=$(bundle exec rubycritic --format console --no-browser $RUBY_FILES 2>&1) +SCORE=$(echo "$OUTPUT" | grep -oP 'Score: \K\d+' | head -1) + +if [ -n "$SCORE" ]; then + # Append quality score to commit message + echo "" >> "$COMMIT_MSG_FILE" + echo "Code Quality Score: $SCORE" >> "$COMMIT_MSG_FILE" +fi + +exit 0 +``` + +Make it executable: +```bash +chmod +x .git/hooks/prepare-commit-msg +``` + +## GitHub Actions Integration + +### Basic Workflow + +Create `.github/workflows/code-quality.yml`: + +```yaml +name: Code Quality + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + rubycritic: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 # Full history for CI mode + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + bundler-cache: true + + - name: Install RubyCritic + run: gem install rubycritic + + - name: Run RubyCritic + run: | + rubycritic --format console --format json \ + --minimum-score 90 \ + --no-browser \ + app/ lib/ + + - name: Upload Report + if: always() + uses: actions/upload-artifact@v3 + with: + name: rubycritic-report + path: tmp/rubycritic/ + retention-days: 30 +``` + +### PR-Focused Workflow + +Only analyze changed files in pull requests: + +```yaml +name: PR Quality Check + +on: + pull_request: + branches: [ main ] + +jobs: + quality-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + bundler-cache: true + + - name: Install RubyCritic + run: gem install rubycritic + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v35 + with: + files: | + **/*.rb + + - name: Run RubyCritic on changed files + if: steps.changed-files.outputs.any_changed == 'true' + run: | + echo "Changed Ruby files:" + echo "${{ steps.changed-files.outputs.all_changed_files }}" + + rubycritic --format console \ + --minimum-score 90 \ + --no-browser \ + ${{ steps.changed-files.outputs.all_changed_files }} + + - name: Comment PR with Results + if: failure() + uses: actions/github-script@v6 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '⚠️ Code quality check failed. Please review the RubyCritic output above.' + }) +``` + +### Workflow with Coverage and Quality + +Combine with SimpleCov for comprehensive analysis: + +```yaml +name: Tests and Quality + +on: [push, pull_request] + +jobs: + test-and-quality: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + bundler-cache: true + + - name: Run tests with coverage + run: bundle exec rspec + env: + COVERAGE: true + + - name: Install RubyCritic + run: gem install rubycritic + + - name: Run RubyCritic + run: | + rubycritic --format console --format html \ + --minimum-score 90 \ + app/ lib/ + + - name: Upload Coverage Report + uses: actions/upload-artifact@v3 + with: + name: coverage-report + path: coverage/ + + - name: Upload Quality Report + uses: actions/upload-artifact@v3 + with: + name: quality-report + path: tmp/rubycritic/ +``` + +## GitLab CI Integration + +Create `.gitlab-ci.yml`: + +```yaml +stages: + - test + - quality + +quality: + stage: quality + image: ruby:3.2 + before_script: + - gem install rubycritic + script: + - rubycritic --format console --format json --minimum-score 90 app/ lib/ + artifacts: + paths: + - tmp/rubycritic/ + expire_in: 1 week + only: + - merge_requests + - main +``` + +### GitLab CI with Changed Files Only + +```yaml +quality:mr: + stage: quality + image: ruby:3.2 + before_script: + - gem install rubycritic + script: + - | + git fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME + CHANGED_FILES=$(git diff --name-only origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD | grep '\.rb$' || true) + + if [ -n "$CHANGED_FILES" ]; then + echo "Analyzing changed files:" + echo "$CHANGED_FILES" + rubycritic --format console --minimum-score 90 $CHANGED_FILES + else + echo "No Ruby files changed" + fi + only: + - merge_requests +``` + +## CircleCI Integration + +Create `.circleci/config.yml`: + +```yaml +version: 2.1 + +jobs: + quality: + docker: + - image: cimg/ruby:3.2 + steps: + - checkout + - run: + name: Install dependencies + command: bundle install + - run: + name: Install RubyCritic + command: gem install rubycritic + - run: + name: Run RubyCritic + command: | + rubycritic --format console --format json \ + --minimum-score 90 \ + --no-browser \ + app/ lib/ + - store_artifacts: + path: tmp/rubycritic + destination: quality-report + +workflows: + version: 2 + build-and-quality: + jobs: + - quality +``` + +## Shared Git Hooks Setup + +Use a shared hooks directory for team consistency: + +### Setup Script + +Create `bin/setup-git-hooks`: + +```bash +#!/bin/bash + +HOOKS_DIR=".git-hooks" +GIT_HOOKS_DIR=".git/hooks" + +# Create symlinks for all hooks +for hook in "$HOOKS_DIR"/*; do + hook_name=$(basename "$hook") + ln -sf "../../$HOOKS_DIR/$hook_name" "$GIT_HOOKS_DIR/$hook_name" + chmod +x "$HOOKS_DIR/$hook_name" + echo "Installed $hook_name hook" +done + +echo "✅ Git hooks installed successfully!" +``` + +Make executable: +```bash +chmod +x bin/setup-git-hooks +``` + +### Team Workflow + +1. Create `.git-hooks/` directory (tracked in git) +2. Add hooks to `.git-hooks/` +3. Run `bin/setup-git-hooks` during project setup +4. Document in README: + +```markdown +## Setup + +1. Clone repository +2. Run `bin/setup-git-hooks` to install quality checks +3. Install dependencies: `bundle install` +``` + +## Bypassing Hooks + +When necessary to bypass quality checks: + +```bash +# Skip pre-commit hook +git commit --no-verify -m "Message" + +# Skip pre-push hook +git push --no-verify +``` + +**Use sparingly** - only when: +- Emergency hotfixes +- Work-in-progress commits +- Non-code changes (docs, config) + +## Best Practices + +1. **Fast feedback**: Use pre-commit for quick checks +2. **Comprehensive analysis**: Use pre-push for thorough checks +3. **CI as gatekeeper**: Always run in CI, even if hooks are skipped +4. **Team adoption**: Make hooks easy to install (`bin/setup-git-hooks`) +5. **Flexible thresholds**: Different scores for different branches +6. **Clear messaging**: Explain why checks fail and how to fix +7. **Escape hatch**: Document when `--no-verify` is acceptable +8. **Artifact storage**: Keep reports for historical analysis +9. **PR comments**: Auto-comment on PRs with quality issues +10. **Regular review**: Adjust thresholds as codebase improves + +## Troubleshooting + +### Hooks Not Running + +```bash +# Check if hooks are executable +ls -la .git/hooks/pre-commit + +# Make executable +chmod +x .git/hooks/pre-commit + +# Verify hook content +cat .git/hooks/pre-commit +``` + +### Hooks Running on Non-Ruby Commits + +Add file type check: +```bash +RUBY_FILES=$(git diff --cached --name-only | grep '\.rb$') +if [ -z "$RUBY_FILES" ]; then + exit 0 # No Ruby files, skip +fi +``` + +### CI Failing but Local Hooks Pass + +Ensure same RubyCritic version: +```yaml +# In CI, use Bundler version +- run: bundle exec rubycritic ... + +# Locally, also use Bundler +bundle exec rubycritic ... +``` + +### Performance Issues in Hooks + +Analyze only changed files: +```bash +# Instead of analyzing entire app/ +RUBY_FILES=$(git diff --cached --name-only | grep '\.rb$') +rubycritic $RUBY_FILES +``` + +## Example Repository Setup + +Complete setup for a Rails application: + +``` +project/ +├── .git-hooks/ +│ ├── pre-commit # Quality check on staged files +│ └── pre-push # Comprehensive analysis +├── .github/ +│ └── workflows/ +│ └── quality.yml # CI quality checks +├── .rubycritic.yml # Configuration +├── bin/ +│ └── setup-git-hooks # Installation script +└── scripts/ + └── check_quality.sh # Shared quality check script +``` + +This provides: +- Local pre-commit checks (fast) +- Pre-push comprehensive analysis +- CI validation +- Team-wide consistency +- Easy onboarding diff --git a/.claude/skills/rubycritic/scripts/check_quality.sh b/.claude/skills/rubycritic/scripts/check_quality.sh index f0280b5..3a86543 100755 --- a/.claude/skills/rubycritic/scripts/check_quality.sh +++ b/.claude/skills/rubycritic/scripts/check_quality.sh @@ -22,31 +22,31 @@ check_rubycritic_installed() { if command_exists rubycritic; then return 0 fi - + # Check if it's available via bundler if [ -f "Gemfile" ] && command_exists bundle; then if bundle exec rubycritic --version >/dev/null 2>&1; then return 0 fi fi - + return 1 } # Function to install RubyCritic install_rubycritic() { echo -e "${YELLOW}RubyCritic is not installed. Installing now...${NC}" - + # Check if we're in a bundler project if [ -f "Gemfile" ]; then echo -e "${BLUE}Detected Gemfile. Checking if rubycritic is in Gemfile...${NC}" - + if grep -q "rubycritic" Gemfile; then echo -e "${BLUE}RubyCritic found in Gemfile. Running bundle install...${NC}" bundle install else echo -e "${YELLOW}RubyCritic not in Gemfile. Adding it to development group...${NC}" - + # Add to Gemfile if grep -q "group :development do" Gemfile; then # Insert after the development group line @@ -58,14 +58,14 @@ install_rubycritic() { echo " gem 'rubycritic', require: false" >> Gemfile echo "end" >> Gemfile fi - + bundle install fi else echo -e "${BLUE}No Gemfile found. Installing RubyCritic as a system gem...${NC}" gem install rubycritic fi - + echo -e "${GREEN}✓ RubyCritic installed successfully!${NC}" echo "" } @@ -73,25 +73,25 @@ install_rubycritic() { # Function to run RubyCritic run_rubycritic() { local target_path="${1:-.}" - + echo -e "${BLUE}Running RubyCritic analysis on: ${target_path}${NC}" echo "" - + # Determine how to run RubyCritic local cmd="rubycritic" if [ -f "Gemfile" ] && command_exists bundle; then cmd="bundle exec rubycritic" fi - + # Run RubyCritic with console format and no browser # Use --no-browser to prevent opening HTML report # Use --format console for immediate feedback $cmd --format console --no-browser "$target_path" - + local exit_code=$? - + echo "" - + if [ $exit_code -eq 0 ]; then echo -e "${GREEN}✓ Quality check complete!${NC}" echo -e "${BLUE}Tip: For detailed HTML report, run: $cmd --format html $target_path${NC}" @@ -99,7 +99,7 @@ run_rubycritic() { echo -e "${YELLOW}⚠ Quality check completed with warnings${NC}" echo -e "${BLUE}Review the output above for issues to address${NC}" fi - + return $exit_code } @@ -109,19 +109,19 @@ main() { echo -e "${BLUE} RubyCritic Code Quality Analyzer${NC}" echo -e "${BLUE}═══════════════════════════════════════════════${NC}" echo "" - + # Check for Ruby if ! command_exists ruby; then echo -e "${RED}Error: Ruby is not installed${NC}" echo "Please install Ruby before running this script" exit 1 fi - + # Check and install RubyCritic if needed if ! check_rubycritic_installed; then install_rubycritic fi - + # Run the analysis run_rubycritic "$@" } diff --git a/.github/workflows/rubycritic.yml b/.github/workflows/rubycritic.yml new file mode 100644 index 0000000..1a92df9 --- /dev/null +++ b/.github/workflows/rubycritic.yml @@ -0,0 +1,262 @@ +name: RubyCritic Code Quality + +on: + pull_request: + branches: [ main ] + paths: + - 'pdf_converter/**/*.rb' + - 'pdf_converter/Gemfile' + - 'pdf_converter/Gemfile.lock' + +permissions: + contents: read + pull-requests: write + +jobs: + rubycritic: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: pdf_converter + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for accurate comparison + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + bundler-cache: true + working-directory: pdf_converter + + - name: Run RubyCritic on PR branch + run: | + echo "=== Running RubyCritic ===" + + # Run RubyCritic in CI mode to compare against base branch + set +e # Don't exit on error, but capture it + bundle exec rubycritic \ + --mode-ci \ + --branch origin/${{ github.base_ref }} \ + --format json \ + --format console \ + --no-browser \ + app/ lib/ + + RC_EXIT_CODE=$? + echo "RubyCritic exit code: $RC_EXIT_CODE" + set -e + + echo "=== Checking for report files ===" + echo "Contents of tmp directory:" + ls -la tmp/ || echo "tmp directory does not exist" + + echo "Contents of tmp/rubycritic directory:" + ls -la tmp/rubycritic/ || echo "tmp/rubycritic directory does not exist" + + # Save the report for analysis (copy to workspace root for github-script access) + if [ -f tmp/rubycritic/report.json ]; then + echo "=== Found report.json, copying to workspace root ===" + cp tmp/rubycritic/report.json ../rubycritic_report.json + echo "Report copied successfully" + echo "Report score:" + cat tmp/rubycritic/report.json | jq '.score' || echo "Could not parse score with jq" + else + echo "WARNING: CI mode report not found. Running full analysis..." + echo "This might happen when CI mode has no changed files to analyze." + + # Fallback: Run without CI mode to get a full report + bundle exec rubycritic \ + --format json \ + --format console \ + --no-browser \ + app/ lib/ || echo "RubyCritic failed" + + echo "Contents of tmp/rubycritic after full run:" + ls -la tmp/rubycritic/ || echo "Still no tmp/rubycritic directory" + + if [ -f tmp/rubycritic/report.json ]; then + echo "=== Found report.json after full analysis ===" + cp tmp/rubycritic/report.json ../rubycritic_report.json + echo "Report copied successfully" + else + echo "ERROR: Still no report found. Skipping quality analysis." + echo "Creating empty marker file to prevent errors in next step" + echo '{"score": 0, "analyzed_modules": [], "error": "No report generated"}' > ../rubycritic_report.json + fi + fi + + - name: Analyze results and comment on PR + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + + // Read RubyCritic JSON report + let report; + try { + const reportPath = path.join(process.env.GITHUB_WORKSPACE, 'rubycritic_report.json'); + console.log('Looking for report at:', reportPath); + + if (!fs.existsSync(reportPath)) { + console.log('No RubyCritic report found at:', reportPath); + console.log('Workspace contents:', fs.readdirSync(process.env.GITHUB_WORKSPACE)); + return; + } + + const reportContent = fs.readFileSync(reportPath, 'utf8'); + report = JSON.parse(reportContent); + console.log('Report loaded successfully. Score:', report.score); + } catch (error) { + console.error('Error reading report:', error); + return; + } + + // Check for error in report + if (report.error) { + console.log('Report generation error:', report.error); + const errorComment = `## 🔍 RubyCritic Code Quality Report\n\n` + + `⚠️ **Unable to generate quality report**\n\n` + + `RubyCritic could not analyze the code. This might happen when:\n` + + `- No Ruby files were changed in this PR\n` + + `- RubyCritic encountered an error during analysis\n\n` + + `Please check the [workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.`; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: errorComment + }); + return; + } + + // Extract metrics from report + const score = report.score || 0; + const analyzedFiles = report.analysed_modules || []; + + // Calculate quality breakdown + const filesByGrade = {}; + analyzedFiles.forEach(file => { + const grade = file.rating || 'N/A'; + filesByGrade[grade] = (filesByGrade[grade] || 0) + 1; + }); + + // Find files with issues (grade C or lower) + const filesWithIssues = analyzedFiles + .filter(file => ['C', 'D', 'F'].includes(file.rating)) + .sort((a, b) => (a.score || 0) - (b.score || 0)) + .slice(0, 10); // Top 10 worst files + + // Build comment body + let commentBody = `## 🔍 RubyCritic Code Quality Report\n\n`; + + // Score summary with emoji + let scoreEmoji = '✅'; + let scoreStatus = 'Excellent'; + if (score < 60) { + scoreEmoji = '🔴'; + scoreStatus = 'Critical'; + } else if (score < 70) { + scoreEmoji = '🟠'; + scoreStatus = 'Poor'; + } else if (score < 80) { + scoreEmoji = '🟡'; + scoreStatus = 'Fair'; + } else if (score < 90) { + scoreEmoji = '🟢'; + scoreStatus = 'Good'; + } + + commentBody += `### Overall Score: ${scoreEmoji} **${score.toFixed(1)}/100** (${scoreStatus})\n\n`; + + // Quality distribution + commentBody += `### Quality Distribution\n\n`; + commentBody += `| Grade | Files | Description |\n`; + commentBody += `|-------|-------|-------------|\n`; + commentBody += `| A | ${filesByGrade['A'] || 0} | Excellent quality |\n`; + commentBody += `| B | ${filesByGrade['B'] || 0} | Good quality |\n`; + commentBody += `| C | ${filesByGrade['C'] || 0} | Fair quality |\n`; + commentBody += `| D | ${filesByGrade['D'] || 0} | Poor quality |\n`; + commentBody += `| F | ${filesByGrade['F'] || 0} | Critical issues |\n\n`; + + // Files needing attention + if (filesWithIssues.length > 0) { + commentBody += `### 📋 Files Needing Attention\n\n`; + commentBody += `| File | Score | Grade | Issues |\n`; + commentBody += `|------|-------|-------|--------|\n`; + + filesWithIssues.forEach(file => { + const fileName = file.path.replace('pdf_converter/', ''); + const fileScore = (file.score || 0).toFixed(1); + const grade = file.rating || 'N/A'; + const smellsCount = (file.smells || []).length; + commentBody += `| \`${fileName}\` | ${fileScore} | ${grade} | ${smellsCount} |\n`; + }); + commentBody += `\n`; + } else { + commentBody += `### ✨ Great Job!\n\nAll analyzed files have good quality ratings (B or better).\n\n`; + } + + // Recommendations + commentBody += `### 💡 Recommendations\n\n`; + if (score >= 90) { + commentBody += `- ✅ Code quality is excellent! Keep up the good work.\n`; + } else if (score >= 80) { + commentBody += `- 🔹 Consider refactoring files with grade C or lower\n`; + commentBody += `- 🔹 Focus on reducing complexity in lower-scored files\n`; + } else if (score >= 70) { + commentBody += `- ⚠️ Several files need refactoring\n`; + commentBody += `- ⚠️ Review code smells and complexity issues\n`; + commentBody += `- ⚠️ Consider breaking down large classes/methods\n`; + } else { + commentBody += `- 🔴 Significant refactoring needed\n`; + commentBody += `- 🔴 Review all files with grade D or F\n`; + commentBody += `- 🔴 Focus on reducing complexity and code smells\n`; + commentBody += `- 🔴 Consider pair programming or code review sessions\n`; + } + + commentBody += `\n---\n`; + commentBody += `📊 Full report available in the [RubyCritic HTML output](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})\n`; + + // Find existing comment + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existingComment = comments.find(comment => + comment.user.login === 'github-actions[bot]' && + comment.body.includes('RubyCritic Code Quality Report') + ); + + // Post or update comment + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: commentBody + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: commentBody + }); + } + + - name: Upload RubyCritic HTML Report + uses: actions/upload-artifact@v4 + if: always() + with: + name: rubycritic-report + path: pdf_converter/tmp/rubycritic/ + retention-days: 30 diff --git a/pdf_converter/.rubocop.yml b/pdf_converter/.rubocop.yml index 5456688..8dd8c4e 100644 --- a/pdf_converter/.rubocop.yml +++ b/pdf_converter/.rubocop.yml @@ -48,4 +48,8 @@ Lint/UnusedMethodArgument: # Disable FetchEnvVar for ENV variable checks in specs and app code Style/FetchEnvVar: - Enabled: false \ No newline at end of file + Enabled: false + +# Allow "error" as exception variable name (more descriptive than "e") +Naming/RescuedExceptionsVariableName: + PreferredName: error \ No newline at end of file diff --git a/pdf_converter/app.rb b/pdf_converter/app.rb index 857051a..15e9011 100644 --- a/pdf_converter/app.rb +++ b/pdf_converter/app.rb @@ -44,44 +44,27 @@ def process_pdf_conversion(request_body, start_time, response_builder) puts "Authentication successful for unique_id: #{unique_id}" # Download PDF - download_result = PdfDownloader.new.download(request_body['source']) - return handle_failure(download_result, response_builder, 'PDF download', output_dir) unless download_result[:success] - - pdf_content = download_result[:content] - puts "PDF downloaded successfully, size: #{pdf_content.bytesize} bytes" + pdf_content = download_pdf(request_body['source'], response_builder, output_dir) + return pdf_content if pdf_content.is_a?(Hash) && pdf_content[:statusCode] # Convert PDF to images - conversion_result = PdfConverter.new.convert_to_images( - pdf_content: pdf_content, - output_dir: output_dir, - unique_id: unique_id, - dpi: ENV['CONVERSION_DPI']&.to_i || 300 - ) - unless conversion_result[:success] - return handle_failure(conversion_result, response_builder, 'PDF conversion', - output_dir) - end - - images = conversion_result[:images] - page_count = images.size - puts "PDF converted successfully: #{page_count} pages" + conversion_result = convert_pdf_to_images(pdf_content, output_dir, unique_id, response_builder) + return conversion_result if conversion_result.is_a?(Hash) && conversion_result[:statusCode] # Upload images as zip file - upload_result = ImageUploader.new.upload_images_from_files(request_body['destination'], images, unique_id) - return handle_failure(upload_result, response_builder, 'Zip upload', output_dir) unless upload_result[:success] - - zip_url = upload_result[:zip_url] - puts "Zip file uploaded successfully: #{zip_url}" + zip_url = upload_images_as_zip(request_body['destination'], conversion_result[:images], unique_id, response_builder, + output_dir) + return zip_url if zip_url.is_a?(Hash) && zip_url[:statusCode] # Send webhook notification - notify_webhook(request_body['webhook'], unique_id, zip_url, page_count, start_time) + notify_webhook(request_body['webhook'], unique_id, zip_url, conversion_result[:images].size, start_time) # Clean up and return success FileUtils.rm_rf(output_dir) response_builder.success_response( unique_id: unique_id, zip_url: zip_url, - page_count: page_count, + page_count: conversion_result[:images].size, metadata: conversion_result[:metadata] ) end @@ -141,6 +124,61 @@ def send_webhook(webhook_url, unique_id, zip_url, page_count, start_time) # Don't fail the request if webhook fails, just log it end +# Downloads PDF from source URL +# +# @param source_url [String] Source URL for PDF +# @param response_builder [ResponseBuilder] Response builder instance +# @param output_dir [String] Output directory for cleanup on failure +# @return [String, Hash] PDF content or error response +def download_pdf(source_url, response_builder, output_dir) + download_result = PdfDownloader.new.download(source_url) + return handle_failure(download_result, response_builder, 'PDF download', output_dir) unless download_result[:success] + + pdf_content = download_result[:content] + puts "PDF downloaded successfully, size: #{pdf_content.bytesize} bytes" + pdf_content +end + +# Converts PDF content to images +# +# @param pdf_content [String] PDF file content +# @param output_dir [String] Output directory for images +# @param unique_id [String] Unique identifier +# @param response_builder [ResponseBuilder] Response builder instance +# @return [Hash] Conversion result or error response +def convert_pdf_to_images(pdf_content, output_dir, unique_id, response_builder) + conversion_result = PdfConverter.new.convert_to_images( + pdf_content: pdf_content, + output_dir: output_dir, + unique_id: unique_id, + dpi: ENV['CONVERSION_DPI']&.to_i || 300 + ) + unless conversion_result[:success] + return handle_failure(conversion_result, response_builder, 'PDF conversion', + output_dir) + end + + puts "PDF converted successfully: #{conversion_result[:images].size} pages" + conversion_result +end + +# Uploads converted images as a zip file +# +# @param destination_url [String] Destination URL for zip file +# @param images [Array] Array of image paths +# @param unique_id [String] Unique identifier +# @param response_builder [ResponseBuilder] Response builder instance +# @param output_dir [String] Output directory for cleanup on failure +# @return [String, Hash] Zip URL or error response +def upload_images_as_zip(destination_url, images, unique_id, response_builder, output_dir) + upload_result = ImageUploader.new.upload_images_from_files(destination_url, images, unique_id) + return handle_failure(upload_result, response_builder, 'Zip upload', output_dir) unless upload_result[:success] + + zip_url = upload_result[:zip_url] + puts "Zip file uploaded successfully: #{zip_url}" + zip_url +end + def authenticate_request(event) # Initialize authenticator (cached after first initialization in Lambda) @authenticator ||= JwtAuthenticator.new(ENV['JWT_SECRET_NAME'] || 'pdf-converter/jwt-secret') @@ -150,12 +188,12 @@ def authenticate_request(event) # Authenticate the request @authenticator.authenticate(headers) -rescue JwtAuthenticator::AuthenticationError => e +rescue JwtAuthenticator::AuthenticationError => error # Handle secrets manager errors - puts "ERROR: Authentication service error: #{e.message}" + puts "ERROR: Authentication service error: #{error.message}" { authenticated: false, error: 'Authentication service unavailable' } -rescue StandardError => e +rescue StandardError => error # Handle any other unexpected errors - puts "ERROR: Unexpected authentication error: #{e.message}" + puts "ERROR: Unexpected authentication error: #{error.message}" { authenticated: false, error: 'Authentication service error' } end diff --git a/pdf_converter/app/image_uploader.rb b/pdf_converter/app/image_uploader.rb index cbccf82..dd46d47 100644 --- a/pdf_converter/app/image_uploader.rb +++ b/pdf_converter/app/image_uploader.rb @@ -42,13 +42,13 @@ def upload(url, content, content_type = 'image/png') etag: etag, size: content.bytesize } - rescue ArgumentError => e - error_result(e.message) + rescue ArgumentError => error + error_result(error.message) rescue URI::InvalidURIError error_result('Invalid URL format') - rescue StandardError => e + rescue StandardError => error # Provide better error message for 403 errors - error_message = e.message + error_message = error.message if error_message.include?('403') error_result('Access denied - URL may be expired or invalid') else @@ -62,32 +62,11 @@ def upload(url, content, content_type = 'image/png') # @param content_type [String] The content type for all images # @return [Array] Array of result hashes for each upload def upload_batch(urls, images, content_type = 'image/png') - raise ArgumentError, 'Number of URLs must match number of images' unless urls.size == images.size - + validate_batch_inputs(urls, images) log_info("Starting batch upload of #{urls.size} images") - results = [] - Async do - barrier = Async::Barrier.new - semaphore = Async::Semaphore.new(THREAD_POOL_SIZE, parent: barrier) - - urls.zip(images).each_with_index do |(url, content), index| - semaphore.async do - result = upload(url, content, content_type) - result[:index] = index - results << result - end - end - - # Wait for all uploads to complete - barrier.wait - end - - # Sort results by index to maintain order - results.sort_by! { |result| result[:index] } - - successful = results.count { |result| result[:success] } - log_info("Batch upload completed: #{successful}/#{results.size} successful") + results = perform_concurrent_uploads(urls, images, content_type) + log_batch_completion(results) results end @@ -115,10 +94,10 @@ def upload_images_from_files(destination_url, image_paths, unique_id) zip_url: UrlUtils.strip_query_params([destination_url]).first, etag: upload_result[:etag] } - rescue StandardError => e + rescue StandardError => error { success: false, - error: "Zip upload error: #{e.message}" + error: "Zip upload error: #{error.message}" } end @@ -129,6 +108,37 @@ def validate_inputs(url, content) raise ArgumentError, 'Content cannot be nil or empty' if content.nil? || content.empty? end + def validate_batch_inputs(urls, images) + raise ArgumentError, 'Number of URLs must match number of images' unless urls.size == images.size + end + + def perform_concurrent_uploads(urls, images, content_type) + results = [] + + Async do + barrier = Async::Barrier.new + semaphore = Async::Semaphore.new(THREAD_POOL_SIZE, parent: barrier) + + urls.zip(images).each_with_index do |(url, content), index| + semaphore.async do + result = upload(url, content, content_type) + result[:index] = index + results << result + end + end + + barrier.wait + end + + results.sort_by! { |result| result[:index] } + results + end + + def log_batch_completion(results) + successful = results.count { |result| result[:success] } + log_info("Batch upload completed: #{successful}/#{results.size} successful") + end + # Uploads content with retry logic for transient failures # @param uri [URI] The URI to upload to # @param content [String] The content to upload @@ -138,8 +148,8 @@ def upload_with_retry(uri, content, content_type) RetryHandler.with_retry(logger: @logger) do perform_upload(uri, content, content_type) end - rescue RetryHandler::RetryError => e - raise StandardError, e.message + rescue RetryHandler::RetryError => error + raise StandardError, error.message end def perform_upload(uri, content, content_type) diff --git a/pdf_converter/app/jwt_authenticator.rb b/pdf_converter/app/jwt_authenticator.rb index d3c0a29..829103a 100644 --- a/pdf_converter/app/jwt_authenticator.rb +++ b/pdf_converter/app/jwt_authenticator.rb @@ -76,8 +76,8 @@ def validate_token(token) { valid: false, error: 'Invalid signature' } rescue JWT::DecodeError { valid: false, error: 'Malformed token' } - rescue StandardError => e - { valid: false, error: "Token validation error: #{e.message}" } + rescue StandardError => error + { valid: false, error: "Token validation error: #{error.message}" } end end @@ -100,10 +100,10 @@ def retrieve_secret log_debug('Successfully retrieved JWT secret from Secrets Manager') rescue Aws::SecretsManager::Errors::ResourceNotFoundException handle_secret_error("Secret '#{@secret_name}' not found") - rescue Aws::SecretsManager::Errors::ServiceError => e - handle_secret_error("AWS service error - #{e.message}") - rescue StandardError => e - handle_secret_error(e.message) + rescue Aws::SecretsManager::Errors::ServiceError => error + handle_secret_error("AWS service error - #{error.message}") + rescue StandardError => error + handle_secret_error(error.message) end # Builds the AWS Secrets Manager client configuration diff --git a/pdf_converter/app/pdf_converter.rb b/pdf_converter/app/pdf_converter.rb index b0effdb..0a0d35a 100644 --- a/pdf_converter/app/pdf_converter.rb +++ b/pdf_converter/app/pdf_converter.rb @@ -41,11 +41,12 @@ def convert_to_images(pdf_content:, output_dir:, unique_id:, **options) return validation_error if validation_error # Convert all pages to images - images = convert_all_pages(temp_pdf_path, page_count, output_dir, unique_id, conversion_dpi) + context = { output_dir: output_dir, unique_id: unique_id, dpi: conversion_dpi } + images = convert_all_pages(temp_pdf_path, page_count, context) success_result(images, page_count, conversion_dpi) - rescue StandardError => e - error_result("PDF conversion failed: #{e.message}") + rescue StandardError => error + error_result("PDF conversion failed: #{error.message}") ensure cleanup_temp_file(temp_pdf) end @@ -62,8 +63,8 @@ def get_page_count(pdf_content) temp_pdf.close temp_pdf.unlink count - rescue StandardError => e - log_error("Failed to get page count: #{e.message}") + rescue StandardError => error + log_error("Failed to get page count: #{error.message}") 0 end @@ -82,16 +83,14 @@ def validate_page_count(page_count) # Converts all pages of a PDF to PNG images # @param pdf_path [String] Path to the temporary PDF file # @param page_count [Integer] Total number of pages - # @param output_dir [String] Directory to save images - # @param unique_id [String] Unique identifier for naming - # @param dpi [Integer] DPI for conversion + # @param context [Hash] Conversion context with :output_dir, :unique_id, :dpi # @return [Array] Array of image file paths - def convert_all_pages(pdf_path, page_count, output_dir, unique_id, dpi) + def convert_all_pages(pdf_path, page_count, context) images = [] - log_info("Starting conversion of #{page_count} pages at #{dpi} DPI") + log_info("Starting conversion of #{page_count} pages at #{context[:dpi]} DPI") (0...page_count).each do |page_index| - image_path = convert_page(pdf_path, page_index, output_dir, unique_id, dpi) + image_path = convert_page(pdf_path, page_index, context) images << image_path page_number = page_index + 1 log_info("Converted page #{page_number}/#{page_count}") @@ -149,26 +148,26 @@ def get_page_count_from_file(pdf_path) # Each page is loaded vertically, so total height / page height = page count first_page = Vips::Image.pdfload(pdf_path, n: 1, dpi: 1) (image.height / first_page.height).to_i - rescue StandardError => e - log_error("Failed to load PDF for page count: #{e.message}") + rescue StandardError => error + log_error("Failed to load PDF for page count: #{error.message}") 0 end - def convert_page(pdf_path, page_index, output_dir, unique_id, dpi) + def convert_page(pdf_path, page_index, context) # Page number for filename (1-indexed) page_number = page_index + 1 - output_filename = "#{unique_id}_page_#{page_number}.png" - output_path = File.join(output_dir, output_filename) + output_filename = "#{context[:unique_id]}_page_#{page_number}.png" + output_path = File.join(context[:output_dir], output_filename) # Load specific page from PDF - image = Vips::Image.pdfload(pdf_path, page: page_index, n: 1, dpi: dpi) + image = Vips::Image.pdfload(pdf_path, page: page_index, n: 1, dpi: context[:dpi]) # Convert to PNG with compression image.pngsave(output_path, compression: @compression) output_path - rescue StandardError => e - log_error("Failed to convert page #{page_number}: #{e.message}") + rescue StandardError => error + log_error("Failed to convert page #{page_number}: #{error.message}") raise end diff --git a/pdf_converter/app/pdf_downloader.rb b/pdf_converter/app/pdf_downloader.rb index 3c6987f..98d1125 100644 --- a/pdf_converter/app/pdf_downloader.rb +++ b/pdf_converter/app/pdf_downloader.rb @@ -41,8 +41,8 @@ def download(url) } rescue URI::InvalidURIError error_result('Invalid URL format') - rescue StandardError => e - error_result("Download failed: #{e.message}") + rescue StandardError => error + error_result("Download failed: #{error.message}") end # Validates that the content is a valid PDF @@ -64,8 +64,8 @@ def download_with_retry(uri) RetryHandler.with_retry(logger: @logger) do fetch_with_redirects(uri) end - rescue RetryHandler::RetryError => e - raise StandardError, e.message + rescue RetryHandler::RetryError => error + raise StandardError, error.message end def validate_url(url) diff --git a/pdf_converter/app/webhook_notifier.rb b/pdf_converter/app/webhook_notifier.rb index 6703a43..73bd7ca 100644 --- a/pdf_converter/app/webhook_notifier.rb +++ b/pdf_converter/app/webhook_notifier.rb @@ -39,8 +39,8 @@ def notify(webhook_url:, unique_id:, status:, images:, page_count:, processing_t error_msg = "Webhook returned HTTP #{response.code}: #{response.body}" { error: error_msg } end - rescue StandardError => e - { error: "Webhook error: #{e.message}" } + rescue StandardError => error + { error: "Webhook error: #{error.message}" } end private diff --git a/pdf_converter/lib/retry_handler.rb b/pdf_converter/lib/retry_handler.rb index 398254a..f4f8282 100644 --- a/pdf_converter/lib/retry_handler.rb +++ b/pdf_converter/lib/retry_handler.rb @@ -44,20 +44,20 @@ def self.with_retry(max_attempts: DEFAULT_MAX_ATTEMPTS, delay_base: DEFAULT_RETR while attempt <= max_attempts begin return yield(attempt) - rescue *NON_RETRYABLE_EXCEPTIONS => e + rescue *NON_RETRYABLE_EXCEPTIONS => error # Don't retry non-retryable errors, fail immediately - raise e - rescue StandardError => e - last_error = e + raise error + rescue StandardError => error + last_error = error # Check if we should retry this error - raise e unless retryable_error?(e) + raise error unless retryable_error?(error) # Check if we have attempts remaining - raise RetryError, "#{e.message} after #{max_attempts} attempts" if attempt >= max_attempts + raise RetryError, "#{error.message} after #{max_attempts} attempts" if attempt >= max_attempts # Log the retry attempt - log_retry(logger, attempt, e.message) + log_retry(logger, attempt, error.message) # Wait before retrying with exponential backoff wait_before_retry(attempt, delay_base) diff --git a/pdf_converter/spec/app/pdf_converter_spec.rb b/pdf_converter/spec/app/pdf_converter_spec.rb index a97d8cb..b38025a 100644 --- a/pdf_converter/spec/app/pdf_converter_spec.rb +++ b/pdf_converter/spec/app/pdf_converter_spec.rb @@ -463,14 +463,16 @@ def height context 'with single page' do it 'converts one page' do temp_pdf = Tempfile.new(['test', '.pdf']) - images = converter.send(:convert_all_pages, temp_pdf.path, 1, output_dir, unique_id, 300) + context = { output_dir: output_dir, unique_id: unique_id, dpi: 300 } + images = converter.send(:convert_all_pages, temp_pdf.path, 1, context) expect(images.size).to eq(1) temp_pdf.close! end it 'does not trigger garbage collection' do temp_pdf = Tempfile.new(['test', '.pdf']) - converter.send(:convert_all_pages, temp_pdf.path, 1, output_dir, unique_id, 300) + context = { output_dir: output_dir, unique_id: unique_id, dpi: 300 } + converter.send(:convert_all_pages, temp_pdf.path, 1, context) expect(GC).not_to have_received(:start) temp_pdf.close! end @@ -479,7 +481,8 @@ def height context 'with 10 pages' do it 'triggers garbage collection after 10th page' do temp_pdf = Tempfile.new(['test', '.pdf']) - converter.send(:convert_all_pages, temp_pdf.path, 10, output_dir, unique_id, 300) + context = { output_dir: output_dir, unique_id: unique_id, dpi: 300 } + converter.send(:convert_all_pages, temp_pdf.path, 10, context) expect(GC).to have_received(:start).once temp_pdf.close! end @@ -488,7 +491,8 @@ def height context 'with 25 pages' do it 'triggers garbage collection twice' do temp_pdf = Tempfile.new(['test', '.pdf']) - converter.send(:convert_all_pages, temp_pdf.path, 25, output_dir, unique_id, 300) + context = { output_dir: output_dir, unique_id: unique_id, dpi: 300 } + converter.send(:convert_all_pages, temp_pdf.path, 25, context) expect(GC).to have_received(:start).twice temp_pdf.close! end @@ -600,24 +604,28 @@ def height context 'with successful conversion' do it 'returns output path' do - path = converter.send(:convert_page, temp_pdf.path, 0, output_dir, unique_id, 300) + context = { output_dir: output_dir, unique_id: unique_id, dpi: 300 } + path = converter.send(:convert_page, temp_pdf.path, 0, context) expect(path).to include(output_dir) expect(path).to end_with('.png') end it 'uses 1-indexed page numbers in filename' do - path = converter.send(:convert_page, temp_pdf.path, 0, output_dir, unique_id, 300) + context = { output_dir: output_dir, unique_id: unique_id, dpi: 300 } + path = converter.send(:convert_page, temp_pdf.path, 0, context) expect(path).to include('page_1.png') end it 'loads correct page from PDF' do - converter.send(:convert_page, temp_pdf.path, 2, output_dir, unique_id, 300) + context = { output_dir: output_dir, unique_id: unique_id, dpi: 300 } + converter.send(:convert_page, temp_pdf.path, 2, context) expect(Vips::Image).to have_received(:pdfload) .with(temp_pdf.path, page: 2, n: 1, dpi: 300) end it 'saves with configured compression' do - converter.send(:convert_page, temp_pdf.path, 0, output_dir, unique_id, 300) + context = { output_dir: output_dir, unique_id: unique_id, dpi: 300 } + converter.send(:convert_page, temp_pdf.path, 0, context) expect(vips_image).to have_received(:pngsave) .with(anything, compression: 6) end