diff --git a/code_utils/README.md b/code_utils/README.md index 5299d4f76..da9afe9ff 100644 --- a/code_utils/README.md +++ b/code_utils/README.md @@ -23,8 +23,8 @@ uv sync --extra dev # Run the code extractor uv run code-util extract -# Run compiler checks on extracted code -uv run code-util check +# Run compiler checks on extracted code, (can only be done on lang at a time) +uv run code-util check -l programming-language # Get help in the cli uv run code-util --help @@ -34,14 +34,21 @@ uv run code-util --help ## Structure - `main.py` - Entrypoint for the CLI. -- `code_finder.py` - CodeFinder class -- `code_checker.py` - CodeChecker class. -- `docker/` - docker files are different scripts and config the container uses. +- `code_finder/` - module for code relating to the CodeFinder class a.k.a the functionality to scan through markdown documents. +- `code_checker/` - module for code relating to the CodeChecker class a.k.a the functionality to build containers and run commands in them. +- `code_checker/docker/` - docker files, scripts and config that the containers use. - `temp/` - Generated code snippets (gitignored). - `files_with_code.txt` - List of files containing code (gitignored). ## Dependancies -- [Click](https://click.palletsprojects.com/en/stable/) - Framework for building CLIs +- [Click](https://click.palletsprojects.com/en/stable/) - Framework for building CLIs. - [Docker SDK For Python](https://docker-py.readthedocs.io/en/stable/) - Library for interacting with the Docker API. +- [Pytest](https://docs.pytest.org/en/stable/index.html) - Python Unit testing Framework. +- [Pyfakefs](https://pytest-pyfakefs.readthedocs.io/en/latest/) - Python utility for faking a filesystem during testing. + +## Notes + + - Hard coded to only deal with python, javascript(typescript) and C# + - Javascript container uses a private npm package. Please set PAT_TOKEN and CODAT_EMAIL env vars in order to build. diff --git a/code_utils/code_checker/__init__.py b/code_utils/code_checker/__init__.py new file mode 100644 index 000000000..1a2ba6d7a --- /dev/null +++ b/code_utils/code_checker/__init__.py @@ -0,0 +1,7 @@ +"""Code checker package for validating code snippets from Codat documentation.""" + +from .code_checker import CodeChecker +from .docker_operator import DockerOperator +from .codat_code_checker_config_models import CodeCheckerConfig, LanguageDockerConfig, DEFAULT_CONFIG + +__all__ = ['CodeChecker', 'DockerOperator', 'CodeCheckerConfig', 'LanguageDockerConfig', 'DEFAULT_CONFIG'] diff --git a/code_utils/code_checker/codat_code_checker_config_models.py b/code_utils/code_checker/codat_code_checker_config_models.py new file mode 100644 index 000000000..650fccf55 --- /dev/null +++ b/code_utils/code_checker/codat_code_checker_config_models.py @@ -0,0 +1,58 @@ +from dataclasses import dataclass +from typing import Dict +from pathlib import Path + + +@dataclass +class LanguageDockerConfig: + """Configuration for a specific programming language's Docker setup.""" + docker_directory: str + validation_command: str + working_directory: str = "" + + def get_docker_path(self, base_dir: Path) -> Path: + """Get the full path to the Docker directory for this language.""" + return base_dir / self.docker_directory + + +@dataclass +class CodeCheckerConfig: + """Complete configuration for the CodeChecker Docker utility.""" + python: LanguageDockerConfig + javascript: LanguageDockerConfig + csharp: LanguageDockerConfig + container_name: str = "code-snippets:latest" + + def get_language_config(self, language: str) -> LanguageDockerConfig: + """Get Docker configuration for a specific language.""" + language_map = { + 'python': self.python, + 'javascript': self.javascript, + 'csharp': self.csharp + } + return language_map.get(language.lower()) + + def get_all_languages(self) -> list[str]: + """Get list of all supported language names.""" + return ['python', 'javascript', 'csharp'] + + +# Default configuration instance based on current CodeChecker implementation +DEFAULT_CONFIG = CodeCheckerConfig( + python=LanguageDockerConfig( + docker_directory="docker/python", + validation_command="bash -c 'cd snippets && pyright .'", + working_directory="python/snippets" + ), + javascript=LanguageDockerConfig( + docker_directory="docker/javascript", + validation_command="tsc --noEmit", + working_directory="javascript" + ), + csharp=LanguageDockerConfig( + docker_directory="docker/csharp", + validation_command="bash -c 'cd /workspace/code-snippets/csharp && ./validate-csharp-snippets.sh'", + working_directory="/workspace/code-snippets/csharp" + ), + container_name="code-snippets" +) diff --git a/code_utils/code_checker/code_checker.py b/code_utils/code_checker/code_checker.py new file mode 100644 index 000000000..b4b52ab86 --- /dev/null +++ b/code_utils/code_checker/code_checker.py @@ -0,0 +1,128 @@ +""" +Code Checker for validating code snippets for a specified programming language. +This module builds and runs a Docker container to validate complete code snippets. +""" + +import sys +from pathlib import Path +from typing import Dict, Optional + +from .codat_code_checker_config_models import CodeCheckerConfig, DEFAULT_CONFIG +from .docker_operator import DockerOperator + + +class CodeChecker: + """ + A class for validating code snippets by building and running validation commands + in a Docker container for a specified programming language environment. + """ + + def __init__(self, target_language: str, config: Optional[CodeCheckerConfig] = None, base_dir: Optional[Path] = None, docker_operator: Optional[DockerOperator] = None): + """ + Initialize the CodeChecker with dependency injection. + + Args: + target_language: Specific language to check (required). + config: Configuration object for Docker settings. Defaults to DEFAULT_CONFIG. + base_dir: Base directory path. Defaults to the directory containing this file. + docker_operator: DockerOperator instance. If None, creates DockerOperator(config, base_dir). + """ + self.config = config or DEFAULT_CONFIG + self.base_dir = base_dir or Path(__file__).parent + self.docker_wrapper = docker_operator or DockerOperator(self.config, self.base_dir) + self.target_language = target_language.lower() + + def _build_docker_images(self) -> Dict[str, Dict[str, any]]: + """ + Build Docker image for the target language. + + Returns: + Dictionary with build results for the target language + """ + build_results = {} + + print(f"🔨 Building Docker images for: {self.target_language}...") + print("=" * 60) + + success, message = self.docker_wrapper.build_language_image(self.target_language) + build_results[self.target_language] = { + 'success': success, + 'message': message + } + + if not success: + print(f"❌ Failed to build {self.target_language} image: {message}") + + print("=" * 60) + + return build_results + + def _validate_language_snippets(self, language: str) -> Dict[str, any]: + """ + Validate code snippets for a specific language using the DockerOperator. + + Args: + language: The programming language to validate + + Returns: + Dictionary with validation results + """ + success, output = self.docker_wrapper.validate_language_snippets(language) + return { + 'success': success, + 'output': output + } + + def check_complete_snippets(self) -> Dict[str, Dict[str, any]]: + """ + Build Docker image and validate complete code snippets for the target language. + + Returns: + Dictionary with validation results for the target language: + { + 'build': {language: {'success': bool, 'message': str}}, + 'validation': {language: {'success': bool, 'output': str}} + } + """ + print(f"🚀 Starting code snippet validation for: {self.target_language}...") + print("=" * 60) + + # Step 1: Build Docker images for target languages + build_results = self._build_docker_images() + + # Check if all builds succeeded + all_builds_successful = all(result['success'] for result in build_results.values()) + + if not all_builds_successful: + failed_builds = [lang for lang, result in build_results.items() if not result['success']] + print(f"❌ Docker build failed for: {', '.join(failed_builds)}") + return { + 'build': build_results, + 'validation': {} + } + + print("=" * 60) + + # Step 2: Validate snippets for target languages + validation_results = {} + + validation_results[self.target_language] = self._validate_language_snippets(self.target_language) + + # Summary + print("=" * 60) + print("📊 Validation Summary:") + + validation_result = validation_results[self.target_language] + + if validation_result['success']: + print(f"🎉 {self.target_language.title()} validation passed!") + else: + print(f"❌ {self.target_language.title()} validation failed!") + + return { + 'build': build_results, + 'validation': validation_results + } + + + diff --git a/code_utils/code_checker/docker/.dockerignore b/code_utils/code_checker/docker/.dockerignore new file mode 100644 index 000000000..bf6adb2bd --- /dev/null +++ b/code_utils/code_checker/docker/.dockerignore @@ -0,0 +1,64 @@ +# Exclude unnecessary files from Docker build context +**/__pycache__/ +**/*.pyc +**/*.pyo +**/*.pyd +**/.Python +**/env/ +**/venv/ +**/.venv/ +**/pip-log.txt +**/pip-delete-this-directory.txt + +# Node modules and TypeScript compilation outputs +**/node_modules/ +**/npm-debug.log* +**/yarn-debug.log* +**/yarn-error.log* +**/.npm +**/.yarn-integrity +**/dist/ +**/build/ + +# .NET build outputs +**/bin/ +**/obj/ +**/*.user +**/*.suo +**/*.cache +**/packages/ + +# Version control +**/.git/ +**/.gitignore +**/.gitattributes + +# IDE and editor files +**/.vscode/ +**/.idea/ +**/*.swp +**/*.swo +*~ + +# OS generated files +**/.DS_Store +**/.DS_Store? +**/._* +**/.Spotlight-V100 +**/.Trashes +**/ehthumbs.db +**/Thumbs.db + +# Documentation and temp files +**/temp/incomplete/ +**/*.md +**/*.txt +**/output.txt +**/link-results.json + +# Only include the complete code snippets and dependency files +!temp/*/complete/ +!docker/requirements.txt +!docker/package.json +!docker/tsconfig.json +!docker/CodatSnippets.csproj diff --git a/code_utils/code_checker/docker/csharp/CodatSnippets.csproj b/code_utils/code_checker/docker/csharp/CodatSnippets.csproj new file mode 100644 index 000000000..cfc944640 --- /dev/null +++ b/code_utils/code_checker/docker/csharp/CodatSnippets.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + Exe + enable + enable + + + + + + + + + + + + + + diff --git a/code_utils/code_checker/docker/csharp/Dockerfile b/code_utils/code_checker/docker/csharp/Dockerfile new file mode 100644 index 000000000..51c8a408e --- /dev/null +++ b/code_utils/code_checker/docker/csharp/Dockerfile @@ -0,0 +1,59 @@ +# C#/.NET-specific Dockerfile for Codat code snippets validation +FROM ubuntu:22.04 + +# Set environment variables +ENV DEBIAN_FRONTEND=noninteractive +ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 +ENV DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 + +# Update package list and install common dependencies +RUN apt-get update && apt-get install -y \ + curl \ + wget \ + apt-transport-https \ + software-properties-common \ + gnupg \ + lsb-release \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install .NET 8.0 SDK +RUN wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb \ + && dpkg -i packages-microsoft-prod.deb \ + && rm packages-microsoft-prod.deb \ + && apt-get update \ + && apt-get install -y dotnet-sdk-8.0 \ + && rm -rf /var/lib/apt/lists/* + +# Create workspace directory +WORKDIR /workspace/code-snippets/csharp + +# Copy C# project file and restore packages +COPY code_checker/docker/csharp/CodatSnippets.csproj ./CodatSnippets.csproj +RUN dotnet restore + +# Create directory for code snippets and copy them from temp directory +RUN mkdir -p ./snippets/ + +# Copy C# code snippets from temp directory (similar to other languages) +COPY temp/csharp/complete/ ./snippets/ + +# Copy C# validation script and fix line endings +COPY code_checker/docker/csharp/validate-csharp-snippets.sh ./validate-csharp-snippets.sh +RUN sed -i 's/\r$//' validate-csharp-snippets.sh && chmod +x validate-csharp-snippets.sh + +# Verify .NET installation and show packages +RUN echo "=== C#/.NET Environment Information ===" && \ + echo ".NET version:" && dotnet --version && \ + echo "" && \ + echo "=== C# Codat Packages ===" && \ + dotnet list package | grep -i codat || echo "Codat packages will be available after restore" && \ + echo "" && \ + echo "=== C# Snippets Count ===" && \ + find ./snippets -name "*.cs" 2>/dev/null | wc -l | xargs echo "C# snippets found:" || echo "No snippets directory found yet" + +# Set working directory +WORKDIR /workspace/code-snippets/csharp + +# Default command +CMD ["/bin/bash"] diff --git a/code_utils/code_checker/docker/csharp/validate-csharp-snippets.sh b/code_utils/code_checker/docker/csharp/validate-csharp-snippets.sh new file mode 100644 index 000000000..ba401e23d --- /dev/null +++ b/code_utils/code_checker/docker/csharp/validate-csharp-snippets.sh @@ -0,0 +1,136 @@ +#!/bin/bash + +SNIPPETS_PATH=${1:-"/workspace/code-snippets/csharp/snippets"} +TEMP_DIR="/tmp/csharp-validation" +TOTAL_ERRORS=0 +VALIDATED_COUNT=0 + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${BLUE}🔍 Validating C# snippets individually...${NC}" + +# Template for individual snippet project +create_project_file() { + local project_file="$1" + cat > "$project_file" << 'EOF' + + + net8.0 + Exe + enable + enable + + + + + + + + + + + + + +EOF +} + +# Clean up any existing temp directory +if [ -d "$TEMP_DIR" ]; then + rm -rf "$TEMP_DIR" +fi + +# Find all .cs files +if [ ! -d "$SNIPPETS_PATH" ]; then + echo -e "${YELLOW}⚠️ Snippets directory not found: $SNIPPETS_PATH${NC}" + exit 0 +fi + +# Count .cs files +CS_FILES_COUNT=$(find "$SNIPPETS_PATH" -name "*.cs" -type f | wc -l) + +if [ "$CS_FILES_COUNT" -eq 0 ]; then + echo -e "${YELLOW}⚠️ No C# files found in $SNIPPETS_PATH${NC}" + exit 0 +fi + +echo -e "${GREEN}📁 Found $CS_FILES_COUNT C# snippet files${NC}" + +# Process each .cs file +find "$SNIPPETS_PATH" -name "*.cs" -type f | sort | while read -r file; do + VALIDATED_COUNT=$((VALIDATED_COUNT + 1)) + SNIPPET_NAME=$(basename "$file" .cs) + PROJECT_DIR="$TEMP_DIR/$SNIPPET_NAME" + + echo -e "${CYAN}🔄 [$VALIDATED_COUNT/$CS_FILES_COUNT] Validating: $SNIPPET_NAME${NC}" + + # Create project directory + mkdir -p "$PROJECT_DIR" + + # Create project file + PROJECT_FILE="$PROJECT_DIR/$SNIPPET_NAME.csproj" + create_project_file "$PROJECT_FILE" + + # Copy snippet as Program.cs + PROGRAM_FILE="$PROJECT_DIR/Program.cs" + cp "$file" "$PROGRAM_FILE" + + # Restore packages for this project + RESTORE_OUTPUT=$(dotnet restore "$PROJECT_FILE" -v q 2>&1) + RESTORE_EXIT_CODE=$? + + if [ $RESTORE_EXIT_CODE -ne 0 ]; then + echo -e " ${RED}❌ $SNIPPET_NAME - RESTORE FAILED${NC}" + echo -e " ${RED} Restore output:${NC}" + echo "$RESTORE_OUTPUT" | sed 's/^/ /' | while read -r line; do + echo -e " ${RED}$line${NC}" + done + TOTAL_ERRORS=$((TOTAL_ERRORS + 1)) + echo "$TOTAL_ERRORS" > "$TEMP_DIR/error_count" + continue + fi + + # Build the project (capture both stdout and stderr) + BUILD_OUTPUT=$(dotnet build "$PROJECT_FILE" --no-restore -v q 2>&1) + BUILD_EXIT_CODE=$? + + if [ $BUILD_EXIT_CODE -eq 0 ]; then + echo -e " ${GREEN}✅ $SNIPPET_NAME - OK${NC}" + else + echo -e " ${RED}❌ $SNIPPET_NAME - BUILD FAILED${NC}" + echo -e " ${RED} Build output:${NC}" + echo "$BUILD_OUTPUT" | sed 's/^/ /' | while read -r line; do + echo -e " ${RED}$line${NC}" + done + TOTAL_ERRORS=$((TOTAL_ERRORS + 1)) + # Write error count to temp file since subshell can't modify parent variables + echo "$TOTAL_ERRORS" > "$TEMP_DIR/error_count" + fi +done + +# Read final error count +if [ -f "$TEMP_DIR/error_count" ]; then + TOTAL_ERRORS=$(cat "$TEMP_DIR/error_count") +else + TOTAL_ERRORS=0 +fi + +# Clean up temp directory +if [ -d "$TEMP_DIR" ]; then + rm -rf "$TEMP_DIR" +fi + +echo "" +if [ "$TOTAL_ERRORS" -eq 0 ]; then + echo -e "${GREEN}🎉 All $CS_FILES_COUNT C# snippets validated successfully!${NC}" + exit 0 +else + echo -e "${RED}❌ Validation completed with $TOTAL_ERRORS errors out of $CS_FILES_COUNT snippets${NC}" + exit 1 +fi diff --git a/code_utils/code_checker/docker/javascript/.npmrc b/code_utils/code_checker/docker/javascript/.npmrc new file mode 100644 index 000000000..e12fbb5eb --- /dev/null +++ b/code_utils/code_checker/docker/javascript/.npmrc @@ -0,0 +1,12 @@ +; begin auth token +//pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/registry/:username=codat +//pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/registry/:_password=${PAT_TOKEN} +//pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/registry/:email=${CODAT_EMAIL} +//pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/:username=codat +//pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/:_password=${PAT_TOKEN} +//pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/:email=${CODAT_EMAIL} +; end auth token + +registry=https://pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/registry/ +always-auth=true +save-exact=true \ No newline at end of file diff --git a/code_utils/code_checker/docker/javascript/Dockerfile b/code_utils/code_checker/docker/javascript/Dockerfile new file mode 100644 index 000000000..3088ffd93 --- /dev/null +++ b/code_utils/code_checker/docker/javascript/Dockerfile @@ -0,0 +1,101 @@ +# JavaScript/TypeScript-specific Dockerfile for Codat code snippets validation +FROM ubuntu:22.04 + +# Accept PAT token and email as build arguments +ARG PAT_TOKEN +ARG CODAT_EMAIL +ENV PAT_TOKEN=${PAT_TOKEN} +ENV CODAT_EMAIL=${CODAT_EMAIL} + +# Set environment variables +ENV DEBIAN_FRONTEND=noninteractive + +# Update package list and install common dependencies +RUN apt-get update && apt-get install -y \ + curl \ + wget \ + apt-transport-https \ + software-properties-common \ + gnupg \ + lsb-release \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install Node.js 18.x and npm +RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ + && apt-get install -y nodejs + +# Copy package.json, .npmrc, and TypeScript configuration +COPY code_checker/docker/javascript/package.json ./package.json +COPY code_checker/docker/javascript/.npmrc ./.npmrc +COPY code_checker/docker/javascript/tsconfig.json ./tsconfig.json + +# Manually replace PAT_TOKEN and CODAT_EMAIL placeholders in .npmrc (avoiding need for vsts-npm-auth) +RUN sed -i "s/\${PAT_TOKEN}/$PAT_TOKEN/g" .npmrc && \ + sed -i "s/\${CODAT_EMAIL}/$CODAT_EMAIL/g" .npmrc + +# Configure npm globally to use Azure DevOps registry with authentication +RUN npm config set registry https://pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/registry/ && \ + npm config set //pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/registry/:username codat && \ + npm config set //pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/registry/:_password $PAT_TOKEN && \ + npm config set //pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/registry/:email $CODAT_EMAIL && \ + npm config set //pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/:username codat && \ + npm config set //pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/:_password $PAT_TOKEN && \ + npm config set //pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/:email $CODAT_EMAIL + +# Debug: Show the configured registry +RUN echo "=== NPM Registry Configuration ===" && \ + npm config get registry && \ + echo "Global npm config:" && \ + npm config list && \ + echo "Contents of .npmrc:" && \ + cat .npmrc && \ + echo "======================================" && \ + echo "" + +# Install TypeScript globally (now npm is properly configured) +RUN npm install -g typescript ts-node @types/node + +# Create workspace directory +WORKDIR /workspace/code-snippets/javascript + + + +# Install npm dependencies +RUN npm install + +# Create directories for source code and snippets +RUN mkdir -p src snippets + +# Copy JavaScript/TypeScript code snippets from temp directory +COPY temp/javascript/complete/ ./snippets/ + +# Debug: Check what files were actually copied +RUN echo "=== DEBUG: Files in snippets directory ===" && \ + ls -la ./snippets/ && \ + echo "=== DEBUG: TypeScript files count ===" && \ + find ./snippets -name "*.ts" | wc -l && \ + echo "=== DEBUG: All files in snippets ===" && \ + find ./snippets -type f && \ + echo "=== DEBUG: Current working directory ===" && \ + pwd && \ + echo "=== DEBUG: Contents of tsconfig.json ===" && \ + cat ./tsconfig.json + +# Verify Node.js and TypeScript installation +RUN echo "=== JavaScript/TypeScript Environment Information ===" && \ + echo "Node.js version:" && node --version && \ + echo "npm version:" && npm --version && \ + echo "TypeScript version:" && tsc --version && \ + echo "" && \ + echo "=== TypeScript Codat Packages ===" && \ + npm list --depth=0 | grep codat || echo "Codat packages installed" && \ + echo "" && \ + echo "=== TypeScript Snippets Count ===" && \ + find ./snippets -name "*.ts" -o -name "*.js" 2>/dev/null | wc -l | xargs echo "TypeScript/JavaScript snippets found:" || echo "No snippets directory found yet" + +# Set working directory +WORKDIR /workspace/code-snippets/javascript + +# Default command +CMD ["/bin/bash"] diff --git a/code_utils/code_checker/docker/javascript/package.json b/code_utils/code_checker/docker/javascript/package.json new file mode 100644 index 000000000..04993fbce --- /dev/null +++ b/code_utils/code_checker/docker/javascript/package.json @@ -0,0 +1,27 @@ +{ + "name": "@codat/code-checker", + "version": "1.0.0", + "description": "Codat SDK code snippets environment", + "main": "index.js", + "private": true, + "scripts": { + "build": "tsc", + "dev": "ts-node", + "start": "node dist/index.js" + }, + "dependencies": { + "@codat/platform": "^*", + "@codat/bank-feeds": "^*", + "@codat/lending": "^*", + "@codat/sync-for-commerce": "^*", + "@codat/sync-for-expenses": "^*", + "@codat/sync-for-payables": "^*", + "@codat/sync-for-payroll": "^*", + "axios": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0", + "ts-node": "^10.0.0" + } +} diff --git a/code_utils/code_checker/docker/javascript/tsconfig.json b/code_utils/code_checker/docker/javascript/tsconfig.json new file mode 100644 index 000000000..32b25369f --- /dev/null +++ b/code_utils/code_checker/docker/javascript/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true + }, + "include": ["src/**/*", "snippets/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/code_utils/code_checker/docker/python/Dockerfile b/code_utils/code_checker/docker/python/Dockerfile new file mode 100644 index 000000000..2c21394c4 --- /dev/null +++ b/code_utils/code_checker/docker/python/Dockerfile @@ -0,0 +1,58 @@ +# Python-specific Dockerfile for Codat code snippets validation +FROM ubuntu:22.04 + +# Set environment variables +ENV DEBIAN_FRONTEND=noninteractive + +# Update package list and install common dependencies +RUN apt-get update && apt-get install -y \ + curl \ + wget \ + apt-transport-https \ + software-properties-common \ + gnupg \ + lsb-release \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install Python 3.11 and pip +RUN apt-get update && apt-get install -y \ + python3.11 \ + python3.11-dev \ + python3-pip \ + python3.11-venv \ + && update-alternatives --install /usr/bin/python python /usr/bin/python3.11 1 \ + && update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 \ + && rm -rf /var/lib/apt/lists/* + +# Create workspace directory +WORKDIR /workspace/code-snippets/python + +# Copy Python dependencies and install packages +COPY code_checker/docker/python/requirements.txt ./requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +# Create directory for code snippets and copy them from temp directory +RUN mkdir -p ./snippets/ + +# Copy Python code snippets from temp directory (similar to JavaScript approach) +COPY temp/python/complete/ ./snippets/ + +# Verify Python installation and show packages +RUN echo "=== Python Environment Information ===" && \ + echo "Python version:" && python --version && \ + echo "" && \ + echo "=== Python Packages Verified ===" && \ + python -c "import requests; print('✓ requests')" && \ + python -c "import codat_platform; print('✓ codat-platform')" || echo "codat-platform installed but may need configuration" && \ + echo "" && \ + pip list | grep -E "(codat|pyright|requests)" && \ + echo "" && \ + echo "=== Python Snippets Count ===" && \ + find ./snippets -name "*.py" 2>/dev/null | wc -l | xargs echo "Python snippets found:" || echo "No snippets directory found yet" + +# Set working directory +WORKDIR /workspace/code-snippets/python + +# Default command +CMD ["/bin/bash"] diff --git a/code_utils/code_checker/docker/python/requirements.txt b/code_utils/code_checker/docker/python/requirements.txt new file mode 100644 index 000000000..0699644d0 --- /dev/null +++ b/code_utils/code_checker/docker/python/requirements.txt @@ -0,0 +1,20 @@ +# Core Codat Python SDKs +codat-platform +codat-bankfeeds +codat-common +codat-banking +codat-commerce +codat-assess +codat-accounting +codat-sync-for-expenses +codat-sync-for-payroll +codat-sync-for-payables +codat-files +codat-lending + +# Development and utility packages +pyright +requests +python-dateutil +httpx +pydantic diff --git a/code_utils/code_checker/docker_operator.py b/code_utils/code_checker/docker_operator.py new file mode 100644 index 000000000..c41153ebf --- /dev/null +++ b/code_utils/code_checker/docker_operator.py @@ -0,0 +1,245 @@ +""" +Docker Wrapper for code validation across multiple programming languages. +This module provides a configurable Docker interface for building and running validation containers. +""" + +import sys +import os +from pathlib import Path +from typing import Tuple, Optional +import docker +from docker.errors import DockerException, BuildError, APIError, ImageNotFound + +from .codat_code_checker_config_models import CodeCheckerConfig, LanguageDockerConfig + + +class DockerOperator: + """ + A configurable wrapper around Docker functionality for code validation. + This class uses dependency injection via a configuration object to support + multiple languages and Docker setups. + """ + + def __init__(self, config: CodeCheckerConfig, base_dir: Optional[Path] = None): + """ + Initialize the DockerOperator with configuration. + + Args: + config: Configuration object containing Docker settings for each language + base_dir: Base directory path. Defaults to the directory containing this file. + """ + self.config = config + self.base_dir = base_dir or Path(__file__).parent + + try: + self.docker_client = docker.from_env() + except DockerException as e: + raise RuntimeError(f"Failed to connect to Docker: {str(e)}. Make sure Docker is running.") + + def build_language_image(self, language: str) -> Tuple[bool, str]: + """ + Build a Docker image for a specific language. + + Args: + language: The programming language ('python', 'javascript', 'csharp') + + Returns: + Tuple of (success, message) + """ + lang_config = self.config.get_language_config(language) + if not lang_config: + return False, f"Unsupported language: {language}" + + docker_path = lang_config.get_docker_path(self.base_dir) + dockerfile_path = docker_path / "Dockerfile" + + if not dockerfile_path.exists(): + return False, f"Dockerfile not found: {dockerfile_path}" + + # For build context, use the parent directory so we can access temp files + build_context = self.base_dir.parent + + container_name = f"{self.config.container_name}-{language}" + + print(f"🔨 Building Docker image for {language.capitalize()}...") + print(f"Docker directory: {docker_path}") + print(f"Container name: {container_name}") + print() + sys.stdout.flush() + + try: + # Get PAT_TOKEN and CODAT_EMAIL from environment for Azure DevOps authentication + build_args = {} + pat_token = os.getenv('PAT_TOKEN') + codat_email = os.getenv('CODAT_EMAIL') + + if pat_token: + build_args['PAT_TOKEN'] = pat_token + print(f"Using PAT_TOKEN from environment for Azure DevOps authentication") + else: + print(f"⚠️ Warning: PAT_TOKEN not found in environment. Azure DevOps packages may not be accessible.") + + if codat_email: + build_args['CODAT_EMAIL'] = codat_email + print(f"Using CODAT_EMAIL from environment: {codat_email}") + else: + print(f"⚠️ Warning: CODAT_EMAIL not found in environment. Azure DevOps authentication may not work properly.") + + # Build the image with streaming output + # Use relative dockerfile path from build context (convert to forward slashes for Docker) + dockerfile_relative = docker_path.relative_to(build_context) / "Dockerfile" + dockerfile_relative_str = str(dockerfile_relative).replace("\\", "/") + build_logs = self.docker_client.api.build( + path=str(build_context), + dockerfile=dockerfile_relative_str, + tag=container_name, + buildargs=build_args, + rm=True, + forcerm=True, + decode=True + ) + + # Stream build logs in real-time + image_id = None + build_failed = False + error_details = [] + + for log_entry in build_logs: + if 'stream' in log_entry: + output = log_entry['stream'].rstrip('\n') + if output: + print(output) + sys.stdout.flush() + # Check for npm/authentication errors in stream output + if ('npm error' in output.lower() or 'authentication' in output.lower() or + 'E401' in output or 'failed' in output.lower()): + build_failed = True + error_details.append(output) + + elif 'aux' in log_entry and 'ID' in log_entry['aux']: + image_id = log_entry['aux']['ID'] + + elif 'error' in log_entry: + error_msg = log_entry['error'] + print(f"ERROR: {error_msg}") + sys.stdout.flush() + build_failed = True + error_details.append(error_msg) + + elif 'errorDetail' in log_entry: + error_detail = log_entry['errorDetail'] + print(f"ERROR DETAIL: {error_detail}") + sys.stdout.flush() + build_failed = True + error_details.append(str(error_detail)) + + # If we detected build errors during streaming, return failure immediately + if build_failed and error_details: + error_summary = "; ".join(error_details[-3:]) # Last 3 errors + return False, f"Docker build failed: {error_summary}" + + print() + + # Verify the image was actually created successfully + try: + created_image = self.docker_client.images.get(container_name) + print(f"✅ {language.capitalize()} Docker image built successfully!") + return True, f"Docker build completed successfully. Image ID: {created_image.short_id}" + except ImageNotFound: + print(f"❌ {language.capitalize()} Docker build reported success but image was not created!") + return False, f"Docker build completed but image '{container_name}' was not found. This may indicate build errors were not properly reported." + + except BuildError as e: + print() + print(f"❌ {language.capitalize()} Docker build failed!") + error_msg = f"Docker build failed: {str(e)}" + + if hasattr(e, 'build_log') and e.build_log: + print("\nBuild log details:") + for log_entry in e.build_log: + if isinstance(log_entry, dict): + if 'stream' in log_entry: + print(log_entry['stream'].rstrip('\n')) + elif 'error' in log_entry: + print(f"ERROR: {log_entry['error']}") + + return False, error_msg + + except APIError as e: + print(f"❌ Docker API error: {str(e)}") + return False, f"Docker API error: {str(e)}" + except Exception as e: + print(f"❌ Unexpected error during build: {str(e)}") + return False, f"Unexpected error during build: {str(e)}" + + def validate_language_snippets(self, language: str) -> Tuple[bool, str]: + """ + Run validation for a specific language using its configured command. + + Args: + language: The programming language to validate + + Returns: + Tuple of (success, output/error message) + """ + lang_config = self.config.get_language_config(language) + if not lang_config: + return False, f"Unsupported language: {language}" + + container_name = f"{self.config.container_name}-{language}" + + print(f"🔍 Validating {language.capitalize()} snippets...") + print("-" * 60) + + # Check if Docker image exists locally before trying to run it + try: + self.docker_client.images.get(container_name) + except ImageNotFound: + error_msg = f"Docker image '{container_name}' not found locally." + if language.lower() == 'javascript': + error_msg += " JavaScript validation requires Azure DevOps credentials (PAT_TOKEN and CODAT_EMAIL environment variables) to build the Docker image." + else: + error_msg += f" Please ensure the {language} Docker image was built successfully." + print(f" ❌ {error_msg}") + return False, error_msg + + container = None + try: + # Run the container in detached mode so we can get logs + container = self.docker_client.containers.run( + container_name, + lang_config.validation_command, + detach=True, + stdout=True, + stderr=True + ) + + # Wait for completion and get logs + result = container.wait() + logs = container.logs(stdout=True, stderr=True).decode('utf-8') + + # Print the output + if logs.strip(): + print(logs) + + print("-" * 60) + + # Check exit status + exit_code = result['StatusCode'] + if exit_code == 0: + print(f" ✅ {language.capitalize()} validation passed!") + return True, "Validation passed" + else: + print(f" ❌ {language.capitalize()} validation failed (exit code {exit_code})") + return False, f"Validation failed with exit code {exit_code}" + + except Exception as e: + print(f" ❌ {language.capitalize()} validation failed with error: {str(e)}") + return False, f"Validation failed with error: {str(e)}" + finally: + # Clean up the container + if container: + try: + container.remove() + except: + pass # Container might already be removed \ No newline at end of file diff --git a/code_utils/code_finder.py b/code_utils/code_finder.py deleted file mode 100644 index c7b1b6ba5..000000000 --- a/code_utils/code_finder.py +++ /dev/null @@ -1,235 +0,0 @@ -import os -import sys -import re -import shutil -from pathlib import Path - - -class CodeFinder: - - all_languages = {'python', 'javascript', 'csharp', 'go'} - - def __init__(self, output_file_name: str = "files_with_code.txt", - target_languages: set = None, deprecated_languages: set = None): - self.matching_files = [] - - # Set default values for language sets - self.target_languages = target_languages if target_languages is not None else {'python', 'javascript', 'csharp'} - self.deprecated_languages = deprecated_languages if deprecated_languages is not None else {'go'} - - self.script_dir = Path(__file__).resolve().parent - self.temp_dir = self.script_dir / 'temp' - self.project_root = self.script_dir.parent - self.docs_path = self.project_root / 'docs' - self.output_file = self.script_dir / output_file_name - - def has_code_snippets(self, file_path:str, target_languages:set): - """ - Check if a markdown file contains code snippets with specified languages. - - Args: - file_path (str): Path to the markdown file - target_languages (set): Set of programming languages to look for - - Returns: - bool: True if file contains code snippets with target languages - """ - try: - with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: - content = f.read() - - # Look for code blocks that start with ``` followed by language name - # Pattern matches: ```python, ```javascript, ```csharp, ```go - pattern = r'```(' + '|'.join(CodeFinder.all_languages) + r')\b' - - matches = re.findall(pattern, content, re.IGNORECASE) - return len(matches) > 0 - - except Exception as e: - print(f"Error reading file {file_path}: {e}") - return False - - def find_files_with_code(self): - """Find markdown files in docs directory that contain code snippets with specified languages.""" - - # Check if docs directory exists - if not self.docs_path.exists(): - print(f"Error: docs directory not found at {self.docs_path}") - sys.exit(1) - - print(f"Searching for markdown files with code snippets in: {self.docs_path}") - print(f"Looking for languages: {', '.join(sorted(self.target_languages))}") - - # Find all markdown files recursively using pathlib - markdown_files = list(self.docs_path.rglob('*.md')) + list(self.docs_path.rglob('*.mdx')) - - for file_path in markdown_files: - # Get relative path from docs directory for output (using forward slashes) - rel_path = file_path.relative_to(self.docs_path).as_posix() - - # Check if file contains target code snippets - if self.has_code_snippets(file_path, self.target_languages): - self.matching_files.append(rel_path) - print(f"Found: {rel_path}") - - # Sort the paths for better readability - self.matching_files.sort() - - # Write to output file - try: - with open(self.output_file, 'w', encoding='utf-8') as f: - - for path in self.matching_files: - f.write(path + '\n') - - print(f"\nSummary:") - print(f"- Total markdown files processed: {len(self.matching_files)}") - print(f"- Files with target code snippets: {len(self.matching_files)}") - print(f"- Results saved to: {self.output_file}") - - except Exception as e: - print(f"Error writing to output file: {e}") - sys.exit(1) - - def has_imports(self, code_content:str, language:str): - """ - Check if code snippet has import statements at the top. - - Args: - code_content (str): The code content to check - language (str): Programming language (python, javascript, csharp) - - Returns: - bool: True if code has imports, False otherwise - """ - lines = code_content.strip().split('\n') - - # Skip empty lines and comments at the top - non_empty_lines = [] - for line in lines: - stripped = line.strip() - if stripped and not self._is_comment_line(stripped, language): - non_empty_lines.append(stripped) - - if not non_empty_lines: - return False - - # Check first few non-empty, non-comment lines for imports - lines_to_check = non_empty_lines[:10] # Check first 10 meaningful lines - - import_patterns = { - 'python': [r'^import\s+', r'^from\s+.+\s+import\s+'], - 'javascript': [r'^import\s+', r'^const\s+.+=\s+require\(', r'^require\('], - 'csharp': [r'^using\s+'] - } - - patterns = import_patterns.get(language, []) - for line in lines_to_check: - for pattern in patterns: - if re.match(pattern, line, re.IGNORECASE): - return True - - return False - - def _is_comment_line(self, line:str, language:str): - """Check if a line is a comment based on language syntax.""" - comment_patterns = { - 'python': [r'^\s*#'], - 'javascript': [r'^\s*//', r'^\s*/\*'], - 'csharp': [r'^\s*//', r'^\s*/\*'] - } - - patterns = comment_patterns.get(language, []) - for pattern in patterns: - if re.match(pattern, line): - return True - return False - - def extract_code(self): - """ - Extract code snippets from matching files and save them to temp directory. - Creates subdirectories for each programming language with complete/incomplete folders. - """ - if not self.matching_files: - print("No matching files found. Run find_files_with_code() first.") - return - - # Language to file extension mapping - lang_extensions = { - 'python': '.py', - 'javascript': '.ts', - 'csharp': '.cs' - } - - - # Create subdirectories for each target language with complete/incomplete folders - for lang in self.target_languages: - lang_dir = self.temp_dir / lang - complete_dir = lang_dir / 'complete' - incomplete_dir = lang_dir / 'incomplete' - - complete_dir.mkdir(parents=True, exist_ok=True) - incomplete_dir.mkdir(parents=True, exist_ok=True) - - print(f"Created temp directory structure at: {self.temp_dir}") - - # Counters for summary - total_snippets = 0 - snippets_by_lang = {lang: {'complete': 0, 'incomplete': 0} for lang in self.target_languages} - - # Process each matching file - for file_path in self.matching_files: - full_file_path = self.docs_path / file_path - - try: - with open(full_file_path, 'r', encoding='utf-8', errors='ignore') as f: - content = f.read() - - # Extract code blocks using regex - # Pattern: ```language followed by code until closing ``` - pattern = r'```(' + '|'.join(self.target_languages) + r')\b\n?(.*?)\n?```' - matches = re.findall(pattern, content, re.DOTALL | re.IGNORECASE) - - for i, (language, code_content) in enumerate(matches): - language = language.lower() - - if language in self.target_languages: - # Clean up the code content - code_content = code_content.strip() - - if code_content: # Only save non-empty snippets - # Check if snippet has imports - has_imports = self.has_imports(code_content, language) - subdirectory = 'complete' if has_imports else 'incomplete' - - # Generate filename based on source file and snippet index - source_name = Path(file_path).stem - source_name = re.sub(r'[^\w\-_]', '_', source_name) # Clean filename - - filename = f"{source_name}_snippet_{i+1}{lang_extensions[language]}" - snippet_path = self.temp_dir / language / subdirectory / filename - - # Save the code snippet - with open(snippet_path, 'w', encoding='utf-8') as snippet_file: - snippet_file.write(code_content) - - total_snippets += 1 - snippets_by_lang[language][subdirectory] += 1 - - print(f"Extracted {language} snippet ({subdirectory}): {Path(language) / subdirectory / filename}") - - except Exception as e: - print(f"Error processing file {file_path}: {e}") - continue - - # Print summary - print(f"\nExtraction Summary:") - print(f"- Total code snippets extracted: {total_snippets}") - for lang, counts in snippets_by_lang.items(): - total_lang_snippets = counts['complete'] + counts['incomplete'] - if total_lang_snippets > 0: - print(f"- {lang.capitalize()} snippets: {total_lang_snippets}") - print(f" - Complete (with imports): {counts['complete']}") - print(f" - Incomplete (no imports): {counts['incomplete']}") - print(f"- Snippets saved to: {self.temp_dir}") - print(f"- Organization: Each language has 'complete' and 'incomplete' subdirectories") \ No newline at end of file diff --git a/code_utils/main.py b/code_utils/main.py index e5885eced..8164f63f1 100644 --- a/code_utils/main.py +++ b/code_utils/main.py @@ -3,7 +3,7 @@ from typing import Optional import click from code_finder import CodeFinder -from code_checker import CodeChecker +from code_checker.code_checker import CodeChecker @click.group() @@ -27,12 +27,13 @@ def extract(language: Optional[str]) -> None: @cli.command() -def check() -> None: - """ - Check and validate extracted code snippets. Will currently only check if - python, javascript and csharp directories are present. - """ - checker = CodeChecker() +@click.option('--language', '-l', + type=click.Choice(['python', 'javascript', 'csharp'], case_sensitive=False), + help='Programming language to check (python, javascript, csharp).', + required=True) +def check(language: str) -> None: + """Check and validate extracted code snippets for a specific language""" + checker = CodeChecker(target_language=language) result = checker.check_complete_snippets() click.echo(result) diff --git a/code_utils/tests/test_code_checker/conftest.py b/code_utils/tests/test_code_checker/conftest.py new file mode 100644 index 000000000..dbe7f0b75 --- /dev/null +++ b/code_utils/tests/test_code_checker/conftest.py @@ -0,0 +1,118 @@ +""" +Configuration and fixtures for code_checker tests. +""" + +import pytest +from pathlib import Path +from typing import Tuple, List + +from code_utils.code_checker.codat_code_checker_config_models import CodeCheckerConfig, DEFAULT_CONFIG + + +class DockerOperatorStub: + """ + A simplified stub for DockerOperator that provides controllable responses + for testing CodeChecker in isolation from Docker daemon. + """ + + def __init__(self, config: CodeCheckerConfig = None, base_dir: Path = None): + self.config = config or DEFAULT_CONFIG + self.base_dir = base_dir or Path(__file__).parent + + # Default responses - can be overridden in tests + self._build_responses = {} # language -> (success, message) + self._validation_responses = {} # language -> (success, output) + + # Default to successful responses + self._default_build_success = True + self._default_build_message = "Build successful" + self._default_validation_success = True + self._default_validation_output = "Validation passed" + + def set_build_response(self, language: str, success: bool, message: str): + """Set specific response for build_language_image calls.""" + self._build_responses[language] = (success, message) + + def set_validation_response(self, language: str, success: bool, output: str): + """Set specific response for validate_language_snippets calls.""" + self._validation_responses[language] = (success, output) + + def set_default_responses(self, build_success: bool = True, validation_success: bool = True): + """Set default responses for all languages.""" + self._default_build_success = build_success + self._default_validation_success = validation_success + + def build_language_image(self, language: str) -> Tuple[bool, str]: + """ + Stub for building Docker images. + Returns configurable success/failure responses. + """ + if language in self._build_responses: + return self._build_responses[language] + + if self._default_build_success: + return True, f"{self._default_build_message} for {language}" + else: + return False, f"Build failed for {language}" + + def validate_language_snippets(self, language: str) -> Tuple[bool, str]: + """ + Stub for validating code snippets. + Returns configurable success/failure responses. + """ + if language in self._validation_responses: + return self._validation_responses[language] + + if self._default_validation_success: + return True, f"{self._default_validation_output} for {language}" + else: + return False, f"Validation failed for {language}" + + + +@pytest.fixture +def docker_operator_stub(): + """ + Fixture providing a DockerOperator stub for testing. + + Usage in tests: + def test_code_checker_with_stub(docker_operator_stub): + # Configure responses + docker_operator_stub.set_build_response('python', True, 'Python build ok') + docker_operator_stub.set_validation_response('python', False, 'Python validation failed') + + # Inject into CodeChecker during initialization + checker = CodeChecker(target_language='python', docker_operator=docker_operator_stub) + + # Run tests + results = checker.check_complete_snippets() + + # Assert results + assert results['build']['python']['success'] is True + assert results['validation']['python']['success'] is False + """ + stub = DockerOperatorStub() + return stub + + +@pytest.fixture +def failing_docker_operator_stub(): + """ + Fixture providing a DockerOperator stub that fails by default. + Useful for testing error handling scenarios. + """ + stub = DockerOperatorStub() + stub.set_default_responses(build_success=False, validation_success=False) + return stub + + +@pytest.fixture +def mock_config(): + """Fixture providing a mock configuration for testing.""" + return DEFAULT_CONFIG + + +@pytest.fixture +def test_base_dir(): + """Fixture providing a test base directory path.""" + return Path(__file__).parent.parent.parent / "code_checker" diff --git a/code_utils/tests/test_code_checker/test_code_checker.py b/code_utils/tests/test_code_checker/test_code_checker.py new file mode 100644 index 000000000..fcd6c1223 --- /dev/null +++ b/code_utils/tests/test_code_checker/test_code_checker.py @@ -0,0 +1,74 @@ +""" +Simple unit tests for CodeChecker using DockerOperator stub. +""" + +import pytest +from code_utils.code_checker.code_checker import CodeChecker + + +class TestCodeChecker: + """Simple unit tests for CodeChecker class.""" + + def test_successful_single_language_validation(self, docker_operator_stub): + """Test successful validation for a single language.""" + # Configure stub for success + docker_operator_stub.set_build_response('python', True, 'Build successful') + docker_operator_stub.set_validation_response('python', True, 'Validation passed') + + # Create CodeChecker with injected stub + checker = CodeChecker(target_language='python', docker_operator=docker_operator_stub) + + # Run validation + results = checker.check_complete_snippets() + + # Assert success + assert results['build']['python']['success'] is True + assert results['validation']['python']['success'] is True + assert 'Build successful' in results['build']['python']['message'] + assert 'Validation passed' in results['validation']['python']['output'] + + def test_build_failure_prevents_validation(self, docker_operator_stub): + """Test that build failure prevents validation from running.""" + # Configure stub for build failure + docker_operator_stub.set_build_response('javascript', False, 'Build failed - missing deps') + + # Create CodeChecker with injected stub + checker = CodeChecker(target_language='javascript', docker_operator=docker_operator_stub) + + # Run validation + results = checker.check_complete_snippets() + + # Assert build failed and no validation ran + assert results['build']['javascript']['success'] is False + assert 'Build failed' in results['build']['javascript']['message'] + assert len(results['validation']) == 0 # No validation due to build failure + + def test_validation_failure_after_successful_build(self, docker_operator_stub): + """Test validation failure after successful build.""" + # Configure stub for build success but validation failure + docker_operator_stub.set_build_response('csharp', True, 'Build OK') + docker_operator_stub.set_validation_response('csharp', False, 'Compilation errors found') + + # Create CodeChecker with injected stub + checker = CodeChecker(target_language='csharp', docker_operator=docker_operator_stub) + + # Run validation + results = checker.check_complete_snippets() + + # Assert build succeeded but validation failed + assert results['build']['csharp']['success'] is True + assert results['validation']['csharp']['success'] is False + assert 'Compilation errors found' in results['validation']['csharp']['output'] + + def test_all_failing_scenario(self, failing_docker_operator_stub): + """Test scenario where all operations fail.""" + # Create CodeChecker with injected failing stub + checker = CodeChecker(target_language='python', docker_operator=failing_docker_operator_stub) + + # Run validation + results = checker.check_complete_snippets() + + # Assert build failed and no validation ran + assert results['build']['python']['success'] is False + assert len(results['validation']) == 0 + diff --git a/code_utils/uv.lock b/code_utils/uv.lock index 0c81a0f54..41d2a6b33 100644 --- a/code_utils/uv.lock +++ b/code_utils/uv.lock @@ -225,15 +225,13 @@ dependencies = [ { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "docker" }, - { name = "pyfakefs" }, - { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, ] [package.optional-dependencies] dev = [ { name = "black", version = "24.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "black", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pyfakefs" }, { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "ruff" }, @@ -244,9 +242,8 @@ requires-dist = [ { name = "black", marker = "extra == 'dev'" }, { name = "click", specifier = ">=8.1.8" }, { name = "docker", specifier = ">=7.1.0" }, - { name = "pyfakefs", specifier = ">=5.9.3" }, - { name = "pytest", specifier = ">=8.3.5" }, - { name = "pytest", marker = "extra == 'dev'" }, + { name = "pyfakefs", marker = "extra == 'dev'", specifier = ">=5.9.3" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.5" }, { name = "ruff", marker = "extra == 'dev'" }, ] provides-extras = ["dev"]