From b27061a886d52a98497bbc4aeda9fdd53ef39050 Mon Sep 17 00:00:00 2001
From: Peter Simpson
Date: Fri, 3 Oct 2025 13:28:07 +0100
Subject: [PATCH 1/3] refactoring the code checker functionality
---
code_utils/README.md | 19 +-
code_utils/code_checker/__init__.py | 7 +
.../codat_code_checker_config_models.py | 58 +++++
code_utils/code_checker/code_checker.py | 128 +++++++++
code_utils/code_checker/docker/.dockerignore | 64 +++++
.../docker/csharp/CodatSnippets.csproj | 21 ++
.../code_checker/docker/csharp/Dockerfile | 59 +++++
.../docker/csharp/validate-csharp-snippets.sh | 136 ++++++++++
.../code_checker/docker/javascript/.npmrc | 12 +
.../code_checker/docker/javascript/Dockerfile | 101 ++++++++
.../docker/javascript/package.json | 26 ++
.../docker/javascript/tsconfig.json | 15 ++
.../code_checker/docker/python/Dockerfile | 58 +++++
.../docker/python/requirements.txt | 20 ++
code_utils/code_checker/docker_operator.py | 245 ++++++++++++++++++
code_utils/code_finder.py | 235 -----------------
code_utils/main.py | 15 +-
code_utils/uv.lock | 9 +-
18 files changed, 974 insertions(+), 254 deletions(-)
create mode 100644 code_utils/code_checker/__init__.py
create mode 100644 code_utils/code_checker/codat_code_checker_config_models.py
create mode 100644 code_utils/code_checker/code_checker.py
create mode 100644 code_utils/code_checker/docker/.dockerignore
create mode 100644 code_utils/code_checker/docker/csharp/CodatSnippets.csproj
create mode 100644 code_utils/code_checker/docker/csharp/Dockerfile
create mode 100644 code_utils/code_checker/docker/csharp/validate-csharp-snippets.sh
create mode 100644 code_utils/code_checker/docker/javascript/.npmrc
create mode 100644 code_utils/code_checker/docker/javascript/Dockerfile
create mode 100644 code_utils/code_checker/docker/javascript/package.json
create mode 100644 code_utils/code_checker/docker/javascript/tsconfig.json
create mode 100644 code_utils/code_checker/docker/python/Dockerfile
create mode 100644 code_utils/code_checker/docker/python/requirements.txt
create mode 100644 code_utils/code_checker/docker_operator.py
delete mode 100644 code_utils/code_finder.py
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..cada18b98
--- /dev/null
+++ b/code_utils/code_checker/docker/javascript/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "codat-snippets",
+ "version": "1.0.0",
+ "description": "Codat SDK code snippets environment",
+ "main": "index.js",
+ "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/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"]
From 9e7e4adadafb1ef5901f893780a2a2a34c51e45b Mon Sep 17 00:00:00 2001
From: Peter Simpson
Date: Fri, 3 Oct 2025 13:38:28 +0100
Subject: [PATCH 2/3] code checker unit tests
---
.../tests/test_code_checker/conftest.py | 118 ++++++++++++++++++
.../test_code_checker/test_code_checker.py | 74 +++++++++++
2 files changed, 192 insertions(+)
create mode 100644 code_utils/tests/test_code_checker/conftest.py
create mode 100644 code_utils/tests/test_code_checker/test_code_checker.py
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
+
From 58eb8756e6d1f869a7ca9986721b3667bc3e8913 Mon Sep 17 00:00:00 2001
From: Peter Simpson
Date: Mon, 6 Oct 2025 12:25:02 +0100
Subject: [PATCH 3/3] adding private bit and namespace change to packagejson2
---
code_utils/code_checker/docker/javascript/package.json | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/code_utils/code_checker/docker/javascript/package.json b/code_utils/code_checker/docker/javascript/package.json
index cada18b98..04993fbce 100644
--- a/code_utils/code_checker/docker/javascript/package.json
+++ b/code_utils/code_checker/docker/javascript/package.json
@@ -1,8 +1,9 @@
{
- "name": "codat-snippets",
+ "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",