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
110 changes: 110 additions & 0 deletions .github/PRE_COMMIT_HOOK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Pre-commit Hook Configuration

This repository uses a pre-commit hook to ensure code quality and prevent broken commits.

## What the pre-commit hook does

The pre-commit hook automatically runs the following checks before allowing any commit:

1. **Code Formatting** (`make format`)

- Formats C++ files using `clang-format`
- Formats Python files using `ruff` (or `black` as fallback)
- Automatically stages formatted files if changes are made

2. **Code Linting** (`make lint`)

- Lints C++ files using `clang-tidy`
- Lints Python files using `ruff` (or `flake8` as fallback)

3. **Tests** (`make test:all`)
- Runs all C++ tests using Google Test
- Runs all Python tests using pytest

## Installation

The pre-commit hook is already installed in this repository. If you're setting up a new clone, the hook should work automatically.

## Manual Testing

You can manually test the pre-commit hook by running:

```bash
./.git/hooks/pre-commit
```

## Bypassing the hook (not recommended)

If you absolutely need to bypass the pre-commit hook (e.g., for a work-in-progress commit), you can use:

```bash
git commit --no-verify -m "your commit message"
```

**Warning:** This is not recommended as it can introduce broken code into the repository.

## Troubleshooting

### Hook fails on formatting

- The hook automatically fixes formatting issues and stages the corrected files
- If formatting fails, check that you have the required tools installed:
- C++: `clang-format`
- Python: `ruff` or `black`

### Hook fails on linting

- Fix the linting issues reported by running `make lint` to see details
- Common issues include:
- Unused variables
- Code style violations
- Missing includes

### Hook fails on tests

- Fix the failing tests by running `make test:all` to see details
- Ensure all new code has corresponding tests
- Verify that existing functionality wasn't broken

### Installing required tools

For macOS (using Homebrew):

```bash
# C++ tools
brew install clang-format llvm

# Python tools (if not using requirements.txt)
pip install ruff pytest

# Google Test (for C++ tests)
brew install googletest
```

For Ubuntu/Debian:

```bash
# C++ tools
sudo apt-get install clang-format clang-tidy

# Python tools (if not using requirements.txt)
pip install ruff pytest

# Google Test (for C++ tests)
sudo apt-get install libgtest-dev
```

## Configuration

The pre-commit hook uses the existing Makefile targets. To modify the behavior:

1. **Formatting rules**: Modify the `format-cpp` and `format-python` targets in `Makefile`
2. **Linting rules**: Modify the `lint-cpp` and `lint-python` targets in `Makefile`
3. **Test configuration**: Modify the `test:all` target in `Makefile`

## Benefits

- **Consistent code style**: All code follows the same formatting standards
- **Early error detection**: Linting catches potential issues before they reach the repository
- **Reliable codebase**: Tests ensure that new changes don't break existing functionality
- **Team productivity**: Reduces time spent on code review for style and basic issues
108 changes: 104 additions & 4 deletions .github/workflows/python-package-conda.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
name: Python Lint & Test
---
name: Presubmit (Python)

on: [push]
on: # yamllint disable-line rule:truthy
pull_request:
branches: ["main"]

jobs:
build-python:
Expand All @@ -11,6 +14,9 @@ jobs:

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch full history for git diff

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
Expand All @@ -22,11 +28,105 @@ jobs:
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Detect changed Python files
id: detect-changes
run: |
# Get list of changed files
if [ "${{ github.event_name }}" = "pull_request" ]; then
# For PR, compare with base branch
CHANGED_FILES=$(git diff --name-only \
${{ github.event.pull_request.base.sha }}..HEAD)
else
# For push, compare with previous commit
if [ "${{ github.event.before }}" = \
"0000000000000000000000000000000000000000" ]; then
# First commit in repo
CHANGED_FILES=$(git diff --name-only --diff-filter=A HEAD)
else
CHANGED_FILES=$(git diff --name-only \
${{ github.event.before }}..HEAD)
fi
fi

echo "All changed files:"
echo "$CHANGED_FILES"

# Filter for Python files only
CHANGED_PYTHON_FILES=$(echo "$CHANGED_FILES" | \
grep '\.py$' || true)

# Extract unique problem directories from changed Python files
CHANGED_PROBLEMS=$(echo "$CHANGED_PYTHON_FILES" | \
grep -E '^problems/[^/]+/' | cut -d'/' -f2 | \
sort -u | tr '\n' ' ' || true)

# Check if requirements.txt or Python-related config files changed
PYTHON_CONFIG_CHANGES=$(echo "$CHANGED_FILES" | \
grep -E '(requirements\.txt|setup\.py|pyproject\.toml)$' || true)

echo "Changed Python files:"
echo "$CHANGED_PYTHON_FILES"
echo "Changed problems: $CHANGED_PROBLEMS"
echo "Python config changes: $PYTHON_CONFIG_CHANGES"

# Set outputs
echo "changed_problems=$CHANGED_PROBLEMS" >> $GITHUB_OUTPUT
echo "has_python_changes=$([ -n "$CHANGED_PYTHON_FILES" ] && \
echo 'true' || echo 'false')" >> $GITHUB_OUTPUT
echo "has_python_config_changes=$([ -n "$PYTHON_CONFIG_CHANGES" ] && \
echo 'true' || echo 'false')" >> $GITHUB_OUTPUT
echo "should_run_all_tests=$([ -n "$PYTHON_CONFIG_CHANGES" ] && \
echo 'true' || echo 'false')" >> $GITHUB_OUTPUT

- name: Format Python code
if: steps.detect-changes.outputs.has_python_changes == 'true'
run: make format-python

- name: Lint Python code
if: steps.detect-changes.outputs.has_python_changes == 'true'
run: make lint-python

- name: Run Python tests
run: make test-py:all
- name: Run Python tests for changed problems
if: >
steps.detect-changes.outputs.changed_problems != '' &&
steps.detect-changes.outputs.should_run_all_tests == 'false'
run: |
PROBLEMS="${{ steps.detect-changes.outputs.changed_problems }}"
echo "Running tests for changed problems: $PROBLEMS"

for problem in $PROBLEMS; do
echo "Testing problem: $problem"
# Convert snake_case to kebab-case for make target
kebab_case=$(echo "$problem" | sed 's/_/-/g')
make test-py:$kebab_case || exit 1
done

- name: Run all Python tests (dependencies changed)
if: steps.detect-changes.outputs.should_run_all_tests == 'true'
run: |
echo "Running all tests due to Python configuration changes"
make test-py:all

- name: Summary
if: always()
run: |
echo "## Test Summary" >> $GITHUB_STEP_SUMMARY

if [ "${{ steps.detect-changes.outputs.has_python_changes }}" = \
"false" ] && \
[ "${{ steps.detect-changes.outputs.has_python_config_changes }}" \
= "false" ]; then
echo "- No Python files changed - skipped all tasks" >> \
$GITHUB_STEP_SUMMARY
elif [ "${{ steps.detect-changes.outputs.should_run_all_tests }}" = \
"true" ]; then
echo "- Ran all tests due to config changes" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.detect-changes.outputs.changed_problems }}" != \
"" ]; then
problems="${{ steps.detect-changes.outputs.changed_problems }}"
echo "- Ran targeted tests for: $problems" >> \
$GITHUB_STEP_SUMMARY
else
echo "- No tests needed (no problem files changed)" >> \
$GITHUB_STEP_SUMMARY
fi
46 changes: 36 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -87,32 +87,48 @@ test\:%:
test-cpp\:%:
@if [ "$*" = "all" ]; then \
echo "Running all C++ tests..."; \
overall_result=0; \
for dir in $(PROBLEM_DIRS); do \
if [ -f $$dir/*_test.cc ]; then \
echo "$(call color_blue,Testing C++ in $$dir...)"; \
$(CXX) $(CXXFLAGS) $(DEBUG_LDFLAGS) $(TEST_LDFLAGS) -I$(COMMON_DIR) -I$$dir -o $$dir/test_runner $$dir/*.cc; \
if [ $$? -eq 0 ]; then \
cd $$dir && ./test_runner --gtest_brief=1 --gtest_color=yes 2>/dev/null | tail -n +2; \
cd $$dir && ./test_runner --gtest_brief=1 --gtest_color=yes; \
test_result=$$?; \
if [ $$test_result -ne 0 ]; then \
overall_result=$$test_result; \
fi; \
rm -f $$dir/test_runner; \
cd - > /dev/null; \
else \
echo "$(call color_red,Failed to compile tests in $$dir)"; \
overall_result=1; \
fi; \
if [ "$$dir" != "$(lastword $(PROBLEM_DIRS))" ]; then \
echo ""; \
fi; \
fi; \
done; \
make clean SILENT=true; \
exit $$overall_result; \
else \
snake_case_name=$(call kebab_to_snake,$*); \
if [ -f $(PROBLEMS_DIR)/$$snake_case_name/$${snake_case_name}_test.cc ]; then \
echo "$(call color_blue,Running C++ tests for $*...)"; \
$(CXX) $(CXXFLAGS) $(DEBUG_LDFLAGS) $(TEST_LDFLAGS) -I$(COMMON_DIR) -I$(PROBLEMS_DIR)/$$snake_case_name -o $(PROBLEMS_DIR)/$$snake_case_name/test_runner $(PROBLEMS_DIR)/$$snake_case_name/*.cc; \
if [ $$? -eq 0 ]; then \
cd $(PROBLEMS_DIR)/$$snake_case_name && ./test_runner --gtest_color=yes 2>/dev/null | tail -n +2; \
cd $(PROBLEMS_DIR)/$$snake_case_name && ./test_runner --gtest_color=yes; \
test_result=$$?; \
rm -f $(PROBLEMS_DIR)/$$snake_case_name/test_runner; \
cd - > /dev/null; \
exit $$test_result; \
else \
echo "$(call color_red,Failed to compile tests for $*)"; \
exit 1; \
fi; \
else \
echo "$(call color_red,No C++ test file found for $*.)"; \
exit 1; \
fi; \
fi
@make clean SILENT=true
Expand All @@ -121,19 +137,29 @@ test-cpp\:%:
test-py\:%:
@if [ "$*" = "all" ]; then \
echo "Running all Python tests..."; \
overall_result=0; \
for dir in $(PROBLEM_DIRS); do \
if [ -f $$dir/*_test.py ]; then \
cd $$dir && $(PYTEST) *_test.py --color=yes 2>/dev/null | tail -n +7; \
echo "$(call color_blue,Testing Python in $$dir...)"; \
cd $$dir && $(PYTEST) *_test.py --color=yes -v; \
test_result=$$?; \
if [ $$test_result -ne 0 ]; then \
overall_result=$$test_result; \
fi; \
cd - > /dev/null; \
echo ""; \
fi; \
done; \
make clean SILENT=true; \
exit $$overall_result; \
else \
snake_case_name=$(call kebab_to_snake,$*); \
if [ -f $(PROBLEMS_DIR)/$$snake_case_name/$${snake_case_name}_test.py ]; then \
echo "$(call color_blue,Running Python tests for $*...)"; \
cd $(PROBLEMS_DIR)/$$snake_case_name && $(PYTEST) $${snake_case_name}_test.py -v --color=yes 2>/dev/null | tail -n +8; \
cd $(PROBLEMS_DIR)/$$snake_case_name && $(PYTEST) $${snake_case_name}_test.py -v --color=yes; \
else \
echo "$(call color_red,No Python test file found for $*.)"; \
exit 1; \
fi; \
fi
@make clean SILENT=true
Expand All @@ -151,7 +177,7 @@ format:
.PHONY: format-cpp
format-cpp:
@echo "Formatting C++ files..."
@if command -v clang-format >/dev/null 2>&1; then \
@if command -v clang-format >/dev/null; then \
find $(COMMON_DIR) $(PROBLEMS_DIR) -name '*.cc' -o -name '*.h' | xargs clang-format -i; \
echo "$(call color_green,C++ files formatted.)"; \
else \
Expand All @@ -161,10 +187,10 @@ format-cpp:
.PHONY: format-python
format-python:
@echo "Formatting Python files..."
@if command -v ruff >/dev/null 2>&1; then \
@if command -v ruff >/dev/null; then \
ruff format $(PROBLEMS_DIR); \
echo "$(call color_green,Python files formatted with ruff.)"; \
elif command -v black >/dev/null 2>&1; then \
elif command -v black >/dev/null; then \
find $(PROBLEMS_DIR) -name '*.py' | xargs black; \
echo "$(call color_green,Python files formatted with black.)"; \
else \
Expand All @@ -180,7 +206,7 @@ lint:
.PHONY: lint-cpp
lint-cpp:
@echo "Linting C++ files..."
@if command -v clang-tidy >/dev/null 2>&1; then \
@if command -v clang-tidy >/dev/null; then \
find $(PROBLEMS_DIR) $(COMMON_DIR) -type f \( -name '*.cc' -o -name '*.h' \) | while read -r file; do \
clang-tidy "$$file" -- $(CXXFLAGS) -x c++ || true; \
done; \
Expand All @@ -192,10 +218,10 @@ lint-cpp:
.PHONY: lint-python
lint-python:
@echo "Linting Python files..."
@if command -v ruff >/dev/null 2>&1; then \
@if command -v ruff >/dev/null; then \
ruff check $(PROBLEMS_DIR); \
echo "$(call color_green,Python linting complete with ruff.)"; \
elif command -v flake8 >/dev/null 2>&1; then \
elif command -v flake8 >/dev/null; then \
find $(PROBLEMS_DIR) -name '*.py' | xargs flake8; \
echo "$(call color_green,Python linting complete with flake8.)"; \
else \
Expand Down
Loading