diff --git a/.gitignore b/.gitignore index eaca413..0243daa 100644 --- a/.gitignore +++ b/.gitignore @@ -146,7 +146,7 @@ reports/ docs/_build/ docs/.doctrees/ site/ -SETUP_COMPLETE.md +docs/internal/ lint-usage-guide.md MIGRAT*.md diff --git a/Makefile b/Makefile index ccde181..9ca880b 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,13 @@ -.PHONY: help build test test-unit test-integration test-watch lint install install-deps clean release +# Makefile.common - Shared Makefile targets for all utilities + +include Makefile.common + +.PHONY: test test-unit test-integration coverage coverage-unit coverage-integration +.PHONY: watch watch-unit watch-integration watch-all badge clean help test-shared-lib watch-shared-lib # Default target .DEFAULT_GOAL := help -## help: Display this help message help: @echo "Server Utilities - Makefile Commands" @echo "" @@ -12,7 +16,6 @@ help: @echo "Available targets:" @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' -## build: Build all utilities build: @echo "Building all utilities..." @for dir in */; do \ @@ -23,42 +26,6 @@ build: done @echo "✅ Build complete" -## test: Run all tests (unit + integration) -test: - @echo "Running all tests..." - @for dir in */; do \ - if [ -f "$$dir/Makefile" ]; then \ - echo "Testing $$dir..."; \ - $(MAKE) -C "$$dir" test || exit 1; \ - fi \ - done - @echo "✅ All tests passed" - -## test-unit: Run unit tests only -test-unit: - @echo "Running unit tests..." - @for dir in */; do \ - if [ -f "$$dir/Makefile" ]; then \ - $(MAKE) -C "$$dir" test-unit || exit 1; \ - fi \ - done - @echo "✅ Unit tests passed" - -## test-integration: Run integration tests only -test-integration: - @echo "Running integration tests..." - @for dir in */; do \ - if [ -f "$$dir/Makefile" ]; then \ - $(MAKE) -C "$$dir" test-integration || exit 1; \ - fi \ - done - @echo "✅ Integration tests passed" - -## test-watch: Run tests in watch mode -test-watch: - @echo "Starting test watcher..." - @./tests/watch.sh - ## lint: Run linters (shellcheck, etc.) - Usage: make lint [MODULE=module-name] lint: @if [ -n "$(MODULE)" ]; then \ @@ -85,7 +52,6 @@ lint: echo "✅ Linting complete"; \ fi -## install: Install all utilities to system install: @echo "Installing utilities..." @for dir in */; do \ @@ -96,7 +62,6 @@ install: done @echo "✅ Installation complete" -## install-deps: Install development dependencies install-deps: @echo "Installing development dependencies..." @command -v shellcheck >/dev/null 2>&1 || { \ @@ -123,7 +88,6 @@ install-deps: } @echo "✅ Dependencies installed" -## clean: Clean build artifacts clean: @echo "Cleaning build artifacts..." @for dir in */; do \ @@ -135,7 +99,6 @@ clean: @find . -name "*.tmp" -delete @echo "✅ Clean complete" -## release: Create a new release (requires VERSION variable) release: @if [ -z "$(VERSION)" ]; then \ echo "Error: VERSION not specified"; \ @@ -147,7 +110,6 @@ release: @git push origin "v$(VERSION)" @echo "✅ Release v$(VERSION) created" -## coverage: Generate test coverage report coverage: @echo "Generating coverage reports..." @for dir in */; do \ @@ -157,27 +119,48 @@ coverage: done @echo "✅ Coverage reports generated" -## docs: Generate documentation docs: @echo "Generating documentation..." @echo "Documentation is in README.md files" @echo "✅ Documentation complete" -## format: Format code format: @echo "Formatting code..." @find . -name "*.sh" -not -path "*/\.*" -not -path "*/node_modules/*" -exec shfmt -w {} + @echo "✅ Formatting complete" -## security: Run security checks security: @echo "Running security checks..." @echo "Checking for common security issues..." @find . -name "*.sh" -not -path "*/\.*" -exec grep -Hn "eval" {} + || true @echo "✅ Security check complete" -## update: Update dependencies update: @echo "Updating dependencies..." @git pull origin main @echo "✅ Dependencies updated" + +test-shared-lib: + @echo "Running shared library tests..." + @if [ -d "tests/lib" ]; then \ + for test_file in tests/lib/test_*.sh; do \ + if [ -f "$$test_file" ]; then \ + echo "Running $$(basename $$test_file)..."; \ + bash "$$test_file" || exit 1; \ + fi \ + done; \ + echo "✅ All shared library tests passed"; \ + else \ + echo "⚠️ tests/lib/ directory not found"; \ + exit 1; \ + fi + +watch-shared-lib: + @echo "Starting test watcher for shared libraries..." + @if [ -f "tests/watch.sh" ]; then \ + ./tests/watch.sh shared-lib; \ + else \ + echo "Error: tests/watch.sh not found"; \ + exit 1; \ + fi + diff --git a/Makefile.common b/Makefile.common new file mode 100644 index 0000000..c29fe1f --- /dev/null +++ b/Makefile.common @@ -0,0 +1,36 @@ +test: + @echo "Running all tests..." + @for dir in */; do \ + if [ -f "$$dir/Makefile" ]; then \ + echo "Testing $$dir..."; \ + $(MAKE) -C "$$dir" test || exit 1; \ + fi \ + done + @echo "✅ All tests passed" + +test-unit: + @echo "Running unit tests..." + @for dir in */; do \ + if [ -f "$$dir/Makefile" ]; then \ + $(MAKE) -C "$$dir" test-unit || exit 1; \ + fi \ + done + @echo "✅ Unit tests passed" + +test-integration: + @echo "Running integration tests..." + @for dir in */; do \ + if [ -f "$$dir/Makefile" ]; then \ + $(MAKE) -C "$$dir" test-integration || exit 1; \ + fi \ + done + @echo "✅ Integration tests passed" + +test-watch: + @if [ -f "tests/watch.sh" ]; then \ + echo "Starting test watcher..."; \ + ./tests/watch.sh; \ + else \ + echo "Error: tests/watch.sh not found"; \ + exit 1; \ + fi diff --git a/README.md b/README.md index 21cf606..e9a0a29 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,14 @@ A collection of server utilities for DevOps, SRE, and infrastructure teams. -## 📦 Available Utilities +## Available Utilities | Utility | Description | Distribution | Status | | ------------------------------ |--------------------------------------------------------------------------------| --------------- |-----------| | [server-audit](./server-audit) | Modular server auditing tool for binary version checking and system inspection | Homebrew, Maven | 🛠️ Alpha | | [diff-check](./diff-check) | Git-based file difference checker for configuration drift detection | Homebrew | 🛠️ Alpha | -## 🚀 Quick Start +## Quick Start ### Installation @@ -42,16 +42,22 @@ cd server-utilities make install ``` -## 📚 Documentation +## Documentation Each utility maintains its own comprehensive documentation: - **[server-audit](./server-audit/README.md)** - Server audit tool for checking installed binaries and configurations - **[diff-check](./diff-check/README.md)** - Git-based file difference checker for configuration drift detection -For general contribution guidelines, security policies, and community standards, see the links below. +### Project Documentation -## 🏗️ Architecture +- **[Roadmap](./ROADMAP.md)** - Future features and planned releases +- **[Contributing](./CONTRIBUTING.md)** - Contribution guidelines +- **[Security Policy](./SECURITY.md)** - Security policies and vulnerability reporting + +For general contribution guidelines, security policies, and community standards, see the links above. + +## Architecture This repository follows a monorepo structure where each directory represents an independently versioned and distributable utility: @@ -75,7 +81,7 @@ server-utilities/ 4. **Extensibility**: Pluggable architecture for custom extensions 5. **Zero Trust**: Explicit configuration required, safe defaults -## 🔧 Development +## Development ### Prerequisites @@ -127,7 +133,7 @@ Each utility follows these conventions: - `docs/` - Additional documentation and assets - `lib/` - Core implementation files -## 🤝 Contributing +## Contributing We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details on: @@ -147,13 +153,13 @@ We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING. 6. Push to your fork (`git push origin feature/amazing-feature`) 7. Open a Pull Request -## 🔒 Security +## Security Security is a top priority. If you discover a security vulnerability, please follow our [Security Policy](SECURITY.md). **Do not** open public issues for security vulnerabilities. -## 📋 Requirements +## Requirements ### Minimum Requirements @@ -167,7 +173,7 @@ Security is a top priority. If you discover a security vulnerability, please fol - **Docker**: For containerized deployments - **GitHub CLI**: For automated release workflows -## 📊 Project Status +## Project Status This project is actively maintained and used in production environments. See individual utility documentation for specific stability guarantees. @@ -175,7 +181,7 @@ This project is actively maintained and used in production environments. See ind - **Support**: Community-driven with commercial support available - **Release Cadence**: Monthly feature releases, weekly patches as needed -## 📜 License +## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. @@ -183,11 +189,11 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file All dependencies and their licenses are documented in [THIRD_PARTY_NOTICES](THIRD_PARTY_NOTICES.md). -## 🙏 Acknowledgments +## Acknowledgments - Built with production needs in mind -## 📞 Support & Community +## Support & Community - **Issues**: [GitHub Issues](https://github.com/minademian/server-utilities/issues) - **Discussions**: [GitHub Discussions](https://github.com/minademian/server-utilities/discussions) @@ -201,11 +207,11 @@ All dependencies and their licenses are documented in [THIRD_PARTY_NOTICES](THIR 4. Ask in [Discussions](https://github.com/minademian/server-utilities/discussions) 5. Open a new issue with the appropriate template -## 🗺️ Roadmap +## Roadmap See [ROADMAP.md](ROADMAP.md) for planned features and utilities. -## 🔄 Versioning +## Versioning We use [Semantic Versioning](https://semver.org/) (SemVer) for all utilities: @@ -215,7 +221,7 @@ We use [Semantic Versioning](https://semver.org/) (SemVer) for all utilities: Each utility maintains its own version independently. -## 💼 Commercial Support +## Commercial Support Enterprise support, custom feature development, and SLA guarantees available upon request. diff --git a/ROADMAP.md b/ROADMAP.md index 2135f25..6f71d7b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,93 +6,34 @@ This document outlines the planned features and utilities for the Server Utiliti Build a comprehensive, production-grade toolkit for DevOps, SRE, and infrastructure teams that emphasizes security, reliability, and ease of integration. -## Release Planning - -### Q4 2025 - -#### v1.1.0 - Enhanced Auditing - -- [ ] MySQL/MariaDB version checker -- [ ] PostgreSQL version checker -- [ ] Redis version checker -- [ ] Nginx version checker -- [ ] Enhanced error reporting with structured output -- [ ] Support for JSON output format -- [ ] Performance optimizations for large server fleets - -#### v1.2.0 - Security Enhancements - -- [ ] CVE database integration for version checking -- [ ] Security audit mode with vulnerability scanning -- [ ] Compliance reporting (CIS benchmarks) -- [ ] Certificate expiration checker -- [ ] SSH configuration auditor - -### Q1 2026 - -#### v2.0.0 - Breaking Changes & Major Features +## Current Status -- [ ] Configuration management utility -- [ ] Automated remediation framework -- [ ] Plugin system for custom checkers -- [ ] Web-based dashboard for results visualization -- [ ] API for programmatic access -- [ ] Container image scanning utility +### diff-check: v1.0.0-alpha +Pre-release testing phase. Gathering early feedback and stabilizing core functionality. -**Breaking Changes:** +**Target:** v1.0.0 stable by end of Q4 2025 -- Configuration file format migration (INI → YAML) -- Output format standardization -- Minimum Bash version bump to 4.4+ +**Next milestones:** +- v1.0.0-beta: Feature freeze, broader testing +- v1.0.0-rc.1: Release candidate for final validation +- v1.0.0: First stable release -#### v2.1.0 - Cloud Integration +See [docs/RELEASE_PROCESS.md](docs/internal/RELEASE_PROCESS.md) for detailed release process. -- [ ] AWS EC2 integration -- [ ] Azure VM integration -- [ ] GCP Compute Engine integration -- [ ] Kubernetes cluster auditing -- [ ] Cloud-native deployment patterns - -### Q2 2026 - -#### v2.2.0 - Database Utilities - -- [ ] Database migration utility -- [ ] Schema version checker -- [ ] Backup verification tool -- [ ] Query performance analyzer -- [ ] Connection pool monitor - -#### v2.3.0 - Log Analysis - -- [ ] Log aggregation utility -- [ ] Pattern matching and anomaly detection -- [ ] Log rotation checker -- [ ] Disk space analyzer -- [ ] Alerting integration - -### Q3 2026 - -#### v3.0.0 - Enterprise Features +## Release Planning -- [ ] Multi-tenant support -- [ ] Role-based access control (RBAC) -- [ ] Audit trail and compliance reporting -- [ ] Centralized configuration management -- [ ] REST API with authentication -- [ ] Grafana dashboard integration -- [ ] Slack/Teams notification integration +### Q4 2025 -### Q4 2026 +#### v1.1.0 - Enhanced Auditing -#### v3.1.0 - Advanced Automation +#### `server-audit` +- [ ] Java libraries lister +- [ ] Version checker for Tomcat running on multiple ports +- [ ] PostgreSQL version checker +- [ ] Support for JSON output format -- [ ] Ansible playbook generator -- [ ] Terraform module generator -- [ ] GitOps integration -- [ ] Change management workflow -- [ ] Rollback automation -- [ ] Chaos engineering utilities +#### `diff-check` +- [ ] Alternative diff check when production server has port 22 blocked ## Feature Requests diff --git a/diff-check/Makefile b/diff-check/Makefile index 1ad5495..cad51dd 100644 --- a/diff-check/Makefile +++ b/diff-check/Makefile @@ -1,3 +1,9 @@ +# diff-check/Makefile + +UTILITY_NAME := diff-check + +include ../Makefile.common + .PHONY: test coverage badge help clean all watch watch-unit watch-integration watch-all all: clean coverage badge @@ -67,12 +73,12 @@ watch: watch-unit watch-unit: @echo "Starting test watcher (unit tests)..." - @./tests/watch.sh unit + @../tests/watch.sh unit watch-integration: @echo "Starting test watcher (integration tests)..." - @./tests/watch.sh integration + @../tests/watch.sh integration watch-all: @echo "Starting test watcher (all tests)..." - @./tests/watch.sh all + @../tests/watch.sh all diff --git a/diff-check/README.md b/diff-check/README.md index 519a39d..b6d8d8b 100644 --- a/diff-check/README.md +++ b/diff-check/README.md @@ -5,6 +5,9 @@ A Git repository vs. file system diff analyzer. +**Version**: 1.0.0-rc.1 +**Last Updated**: November 2, 2024 + ## Table of Contents - [Features](#features) @@ -460,6 +463,3 @@ Mina Demian --- -**Version**: 1.0.0-alpha -**Last Updated**: November 2, 2024 - diff --git a/diff-check/diff-check.sh b/diff-check/diff-check.sh index 0300698..3a37829 100755 --- a/diff-check/diff-check.sh +++ b/diff-check/diff-check.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash +set -euo pipefail #=============================================================================== # diff-check - Git-to-deployment diff analyzer #=============================================================================== -# Version: 1.0.0-alpha +# Version: 1.0.0-rc.1 # Author: Mina Demian # License: MIT # URL: https://github.com/minademian/server-utilities @@ -52,19 +53,33 @@ # --branch main --debug --report-file ./diff.log --summary-file ./summary.log #=============================================================================== +# Tracing (opt-in). Set DIFFC_TRACE=1 or DEBUG=true to enable verbose failure diagnostics. +if [[ "${DIFFC_TRACE:-0}" == "1" ]]; then + # merge stderr->stdout for easier capture if you want combined logs in CI + # exec 2>&1 -declare -a MODIFIED=() -declare -a ADDED=() -declare -a DELETED=() -declare -a ARTIFACTS=() + # shellcheck disable=SC2154 + trap 'rc=$?; echo "[FATAL] Exited with code $rc at line ${LINENO} running: ${BASH_COMMAND}" >&2; \ + echo "Stack trace:" >&2; i=0; while caller $i; do ((i++)); done >&2; exit $rc' ERR -PRODUCTION_MODE=false + echo "[INFO] Starting diff-check: $(basename "$0") PID=$$" +else + # Use a subtle startup log (via your info logger) not raw echo + # info_echo "[INFO] Starting diff-check: $(basename "$0") PID=$$" + : +fi + +APP_DIR="" +GIT_URL="" +BRANCH="" +SSH_KEY="" +PRODUCTION_MODE="${PRODUCTION_MODE:-false}" SAFE_MODE=true -DEBUG=false +DEBUG="${DEBUG:-false}" TMP_DIR="/tmp/app-diff-$$" -REPORT_DIR="" +REPORT_DIR="/tmp/reports-app-diff-$$" DRY_RUN=false -SUMMARY=false +SUMMARY=true JSON_FILE="" REPORT_FILE="" SUMMARY_FILE="" @@ -74,13 +89,29 @@ TEST_OUTPUT=false TEST_TYPE="mixed" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + LIB_DIR="$SCRIPT_DIR/lib" -if [[ -f "$LIB_DIR/diff_helpers.sh" ]]; then - # shellcheck source=/dev/null - source "$LIB_DIR/diff_helpers.sh" +SHARED_LIB_DIR="$SCRIPT_DIR/../lib" + +# Source shared libraries first +if [[ -f "$SHARED_LIB_DIR/logging.sh" ]]; then + # shellcheck source=/dev/null + source "$SHARED_LIB_DIR/logging.sh" else - debug_error "[ERROR] Required helper library not found: $LIB_DIR/diff_helpers.sh" >&2 - exit 1 + echo "[ERROR] Required shared library not found: $SHARED_LIB_DIR/logging.sh" >&2 + exit 1 +fi + +# Source local libraries +if [[ -d "$LIB_DIR" ]]; then + if compgen -G "$LIB_DIR"/*.sh > /dev/null; then + for helper_file in "$LIB_DIR"/*.sh; do + if [[ -f "$helper_file" ]]; then + # shellcheck source=/dev/null + source "$helper_file" + fi + done + fi fi main() { @@ -122,8 +153,16 @@ main() { fi ;; --json-file) JSON_FILE="$2"; shift 2 ;; - --debug) DEBUG=true; shift ;; - --production) PRODUCTION_MODE=true; shift ;; + --debug) + DEBUG=true + export DEBUG + shift + ;; + --production) + PRODUCTION_MODE=true + export PRODUCTION_MODE + shift + ;; --test-output) TEST_OUTPUT=true if [[ -n "${2:-}" && "$2" != --* ]]; then @@ -135,441 +174,33 @@ main() { ;; --artifact-file) ARTIFACT_FILE="$2"; shift 2 ;; --help) show_test_output_help; exit 0 ;; - *) info_echo "Unknown option: $1" >&2; exit 1 ;; - esac - done - - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - if [[ -f "$SCRIPT_DIR/lib/test-output.sh" ]]; then - # shellcheck source=/dev/null - source "$SCRIPT_DIR/lib/test-output.sh" - elif [[ -f "$SCRIPT_DIR/../lib/test-output.sh" ]]; then - # shellcheck source=/dev/null - source "$SCRIPT_DIR/../lib/test-output.sh" - debug_echo "Sourced test-output helper from repo root: $SCRIPT_DIR/../lib/test-output.sh" - elif [[ "$TEST_OUTPUT" == "true" ]]; then - debug_error "Test output functionality not available: lib/test-output.sh not found" - exit 1 - fi - - if [[ "$DEBUG" == "true" ]]; then - debug_echo "Debug mode enabled" - debug_echo "Script directory: $SCRIPT_DIR" - debug_echo "Arguments: APP_DIR=${APP_DIR:-}, GIT_URL=${GIT_URL:-}, BRANCH=${BRANCH:-}, REPORT_DIR=${REPORT_DIR:-}, REPORT_FILE=${REPORT_FILE:-}, SUMMARY_FILE=${SUMMARY_FILE:-}, ARTIFACT_FILE=${ARTIFACT_FILE:-}, TEST_OUTPUT=${TEST_OUTPUT}" - debug_echo "Temporary directory: $TMP_DIR" - fi - - if [[ "$PRODUCTION_MODE" == "true" ]]; then - production_echo "Production mode enabled" - production_debug "Script directory: $SCRIPT_DIR" - production_debug "Arguments: APP_DIR=${APP_DIR:-}, GIT_URL=${GIT_URL:-}, BRANCH=${BRANCH:-}, REPORT_DIR=${REPORT_DIR:-}, TEST_OUTPUT=${TEST_OUTPUT}" - production_debug "Temporary directory: $TMP_DIR" - production_debug "PRODUCTION_MODE=${PRODUCTION_MODE}" - fi - - if [[ "$TEST_OUTPUT" != "true" ]]; then - [[ -z "${APP_DIR:-}" ]] && { debug_error "--app-dir is required"; exit 1; } - [[ -z "${GIT_URL:-}" ]] && { debug_error "--git-url is required"; exit 1; } - [[ -z "${BRANCH:-}" ]] && { debug_error "--branch is required"; exit 1; } - else - info_echo "[TEST] Test output mode enabled (type: $TEST_TYPE)" - APP_DIR="/test/app" - GIT_URL="https://test.git" - BRANCH="test-branch" - fi - - if [[ -n "${REPORT_DIR:-}" ]]; then - shopt -s extglob 2>/dev/null || true - REPORT_DIR="${REPORT_DIR%%+(/)}" - shopt -u extglob 2>/dev/null || true - if [[ "$REPORT_DIR" != /* ]]; then - REPORT_DIR="$(cd "$(dirname "$REPORT_DIR")" 2>/dev/null && pwd)/$(basename "$REPORT_DIR")" || REPORT_DIR="$(pwd)/$REPORT_DIR" - fi - debug_echo "Creating report directory: $REPORT_DIR" - if ! mkdir -p "$REPORT_DIR" 2>/dev/null; then - debug_error "Failed to create directory: $REPORT_DIR" - exit 1 - fi - if [[ ! -w "$REPORT_DIR" ]]; then - debug_error "Directory not writable: $REPORT_DIR" - exit 1 - fi - info_echo "Report directory ready: $REPORT_DIR" - info_echo "Directory permissions: $(ls -ld "$REPORT_DIR")" - - if [[ -z "${REPORT_FILE:-}" ]]; then - REPORT_FILE="${REPORT_DIR}/diff_${TIMESTAMP}.log" - fi - if [[ -z "${SUMMARY_FILE:-}" ]]; then - SUMMARY_FILE="${REPORT_DIR}/summary_${TIMESTAMP}.log" - fi - if [[ -z "${ARTIFACT_FILE:-}" ]]; then - ARTIFACT_FILE="${REPORT_DIR}/artifacts_${TIMESTAMP}.txt" - fi - debug_echo "Derived report paths: REPORT_FILE=$REPORT_FILE, SUMMARY_FILE=$SUMMARY_FILE, ARTIFACT_FILE=$ARTIFACT_FILE" - else - info_echo "No report directory specified, skipping report generation" - fi - - if [[ -n "${JSON_FILE:-}" ]]; then - if [[ "$JSON_FILE" != /* ]]; then - JSON_FILE="$(pwd)/$JSON_FILE" - fi - - JSON_DIR="$(dirname "$JSON_FILE")" - debug_echo "Creating JSON output directory: $JSON_DIR" - - if ! mkdir -p "$JSON_DIR" 2>/dev/null; then - debug_error "[ERROR] Failed to create directory: $JSON_DIR" >&2 - exit 1 - fi - - if [[ ! -w "$JSON_DIR" ]]; then - debug_error "[ERROR] Directory not writable: $JSON_DIR" >&2 - exit 1 - fi - - info_echo "JSON output directory ready: $JSON_DIR" - fi - - debug_echo "Creating temp directory: $TMP_DIR" - if ! mkdir -p "$TMP_DIR"; then - debug_error "[ERROR] Failed to create temp directory: $TMP_DIR" >&2 - exit 1 - fi - info_echo "Temp directory created: $TMP_DIR" - - if [[ "$TEST_OUTPUT" != "true" ]]; then - info_echo "Cloning $GIT_URL (branch: $BRANCH)..." - if [[ -n "${SSH_KEY:-}" ]]; then - if [[ ! -f "$SSH_KEY" ]]; then - debug_error "[ERROR] SSH key file not found: $SSH_KEY" >&2 + --*) + debug_error "Unknown option: $1" + debug_error "Use --help for usage information" exit 1 - fi - debug_echo "Using SSH key: $SSH_KEY" - export GIT_SSH_COMMAND="ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new" - debug_echo "Clone command: GIT_SSH_COMMAND='$GIT_SSH_COMMAND' git clone --quiet --depth=1 --branch '$BRANCH' '$GIT_URL' '$TMP_DIR/repo'" - else - debug_echo "Clone command: git clone --quiet --depth=1 --branch '$BRANCH' '$GIT_URL' '$TMP_DIR/repo'" - fi - if ! git clone --quiet --depth=1 --branch "$BRANCH" "$GIT_URL" "$TMP_DIR/repo"; then - production_error "Git clone failed for $GIT_URL (branch $BRANCH)" - debug_error "[ERROR] Git clone failed!" >&2 - if [[ -n "${SSH_KEY:-}" ]]; then - production_error "Suggest testing SSH connection with: ssh -i $SSH_KEY -T git@github.com" - debug_error "[ERROR] Try testing SSH connection: ssh -i $SSH_KEY -T git@github.com" >&2 - fi - exit 1 - fi - unset GIT_SSH_COMMAND - info_echo "Repository cloned successfully" - production_echo "Repository cloned into: $TMP_DIR/repo" - debug_echo "Repo contents: $(find "$TMP_DIR/repo" -type f | head -10)" - else - info_echo "Skipping git clone in test mode" - mkdir -p "$TMP_DIR/repo" - fi - - info_echo "Comparing deployed files in $APP_DIR with Git HEAD..." - if [[ "$TEST_OUTPUT" != "true" ]]; then - if [[ ! -d "$APP_DIR" ]]; then - debug_error "[ERROR] App directory does not exist: $APP_DIR" >&2 - exit 1 - fi - cd "$APP_DIR" - debug_echo "Changed to directory: $(pwd)" - else - debug_echo "Skipping directory validation in test mode" - fi - - [[ "$SAFE_MODE" == "true" ]] && info_echo "Safe mode ON." - [[ "$DRY_RUN" == "true" ]] && info_echo "Dry-run mode ON." - - EXCLUDE_PATTERNS=( ".git" "logs" "tmp" "target" ) - EXCLUDE_ARGS=() - - if command -v gdiff >/dev/null 2>&1; then - DIFF_BIN="gdiff" - debug_echo "Using GNU diff: gdiff" - else - DIFF_BIN="diff" - debug_echo "Using standard diff: diff" - fi - - if "$DIFF_BIN" --exclude=nonexistent /dev/null /dev/null >/dev/null 2>&1; then - for p in "${EXCLUDE_PATTERNS[@]}"; do EXCLUDE_ARGS+=( "--exclude=$p" ); done - else - for p in "${EXCLUDE_PATTERNS[@]}"; do EXCLUDE_ARGS+=( "-x" "$p" ); done - fi - - DIFF_OPTS=( -r -q ) - if "$DIFF_BIN" -N /dev/null /dev/null >/dev/null 2>&1; then - DIFF_OPTS+=( -N ) - debug_echo "Diff supports -N flag" - fi - - DIFF_CMD=( "$DIFF_BIN" "${DIFF_OPTS[@]}" "${EXCLUDE_ARGS[@]}" "$TMP_DIR/repo" "$APP_DIR" ) - - debug_echo "Full diff command: ${DIFF_CMD[*]}" - - if [[ "$DRY_RUN" == "true" ]]; then - info_echo "[DRY-RUN] Command: ${DIFF_CMD[*]}" - elif [[ "$TEST_OUTPUT" == "true" ]]; then - RAW_DIFF_FILE="$TMP_DIR/test_diff_raw_${TIMESTAMP}.tmp" - TEST_PARSE_FILE="$RAW_DIFF_FILE" - OLD_REPORT_FILE="${REPORT_FILE:-}" - REPORT_FILE="$RAW_DIFF_FILE" - if [[ -n "$OLD_REPORT_FILE" ]]; then - info_echo "Generating test output to temp raw file: $RAW_DIFF_FILE (will later save final report to: $OLD_REPORT_FILE)" - else - info_echo "Generating test output to temp raw file: $RAW_DIFF_FILE" - fi - if ! declare -f generate_test_output >/dev/null; then - debug_error "[ERROR] Function 'generate_test_output' is not defined after sourcing test-output.sh." - exit 1 - fi - generate_test_output "$TEST_TYPE" - DIFF_EXIT=1 - REPORT_FILE="$OLD_REPORT_FILE" - debug_echo "Restored REPORT_FILE to: ${REPORT_FILE:-}" - else - if [[ -n "$REPORT_FILE" ]]; then - info_echo "Running diff and generating report..." - RAW_DIFF_FILE="$TMP_DIR/raw_diff.tmp" - set +e - "${DIFF_CMD[@]}" > "$RAW_DIFF_FILE" 2>&1 - DIFF_EXIT=$? - set -e - debug_echo "Diff exit code: $DIFF_EXIT (0=no diff, 1=differences, 2=error)" - if [[ $DIFF_EXIT -eq 2 ]]; then - debug_error "[ERROR] Diff command failed! Check raw output below:" - cat "$RAW_DIFF_FILE" - exit 1 - fi - if [[ -f "$RAW_DIFF_FILE" ]]; then - REPORT_SIZE=$(wc -c < "$RAW_DIFF_FILE") - debug_echo "Raw diff created: $RAW_DIFF_FILE (${REPORT_SIZE} bytes)" - if [[ $REPORT_SIZE -eq 0 ]]; then - debug_echo "Raw diff is empty - no differences found" - else - debug_echo "First 10 lines of raw diff:" - [[ "$DEBUG" == "true" ]] && head -10 "$RAW_DIFF_FILE" - fi - else - debug_error "[ERROR] Raw diff file was not created: $RAW_DIFF_FILE" + ;; + *) + debug_error "Invalid argument: $1" + debug_error "Use --help for usage information" exit 1 - fi - else - info_echo "Running diff (no report file)" - set +e - "${DIFF_CMD[@]}" - set -e - fi - fi - - if [[ "$DRY_RUN" == "false" && ( -n "$REPORT_FILE" || -n "$TEST_PARSE_FILE" ) ]]; then - if [[ "$TEST_OUTPUT" == "true" ]]; then - PARSE_FILE="${TEST_PARSE_FILE:-${REPORT_FILE:-}}" - else - PARSE_FILE="${RAW_DIFF_FILE:-$REPORT_FILE}" - fi - if [[ -f "$PARSE_FILE" && -s "$PARSE_FILE" ]]; then - info_echo "Parsing diff output..." - LINE_COUNT=0 - set +e - while IFS= read -r line || [[ -n "$line" ]]; do - ((LINE_COUNT++)) - if [[ $line == "Files "* ]]; then - file=$(echo "$line" | awk '{print $4}') - MODIFIED+=("$file") - debug_echo "M $file" - [[ $file =~ \.class$|\.jar$ ]] && ARTIFACTS+=("$file") - elif [[ $line == "Only in "* ]]; then - file=$(echo "$line" | sed -E 's/^Only in ([^:]+): (.+)$/\1\/\2/') - if [[ $line == *"$TMP_DIR/repo"* ]]; then - DELETED+=("$file") - debug_echo "D $file" - else - ADDED+=("$file") - debug_echo "A $file" - fi - [[ $file =~ \.class$|\.jar$ ]] && ARTIFACTS+=("$file") - elif [[ $line == "Binary files "* ]]; then - file=$(echo "$line" | awk '{print $5}') - MODIFIED+=("$file") - debug_echo "M $file (binary)" - [[ $file =~ \.class$|\.jar$ ]] && ARTIFACTS+=("$file") - fi - done < "$PARSE_FILE" - set -e - info_echo "Parsed $LINE_COUNT lines from report" - info_echo "M: ${#MODIFIED[@]} modified files" - info_echo "A: ${#ADDED[@]} added files" - info_echo "D: ${#DELETED[@]} deleted files" - info_echo "Found ${#ARTIFACTS[@]} artifact files" - if [[ -n "$REPORT_FILE" ]]; then - { - show_tree_diff - generate_git_style_log - } > "$REPORT_FILE" - info_echo "Diff report saved to: $REPORT_FILE" - else - show_tree_diff - fi - else - debug_echo "Parse file is empty, no changes detected" - info_echo "No changes detected." - if [[ -n "$REPORT_FILE" ]]; then - info_echo "No changes detected." > "$REPORT_FILE" - info_echo "Empty diff report saved to: $REPORT_FILE" - fi - fi - if [[ "$SUMMARY" == "true" ]]; then - debug_echo "Generating summary report: $SUMMARY_FILE" - SUMMARY_SOURCE="${PARSE_FILE:-${RAW_DIFF_FILE:-}}" - if [[ -z "${SUMMARY_SOURCE:-}" ]]; then - debug_error "No raw diff available to generate summary" - else - if grep -E '^(Files |Only in |Binary files )' "$SUMMARY_SOURCE" > "$SUMMARY_FILE" 2>/dev/null; then - if [[ -f "$SUMMARY_FILE" ]]; then - SUMMARY_SIZE=$(wc -c < "$SUMMARY_FILE") - info_echo "Summary report created (${SUMMARY_SIZE} bytes)" - else - info_echo "[WARN] Summary file not created" - fi - else - { - info_echo "Constructed summary generated from parsed diff (counts):" - info_echo "Modified: ${#MODIFIED[@]}" - info_echo "Added: ${#ADDED[@]}" - info_echo "Deleted: ${#DELETED[@]}" - info_echo "Artifacts: ${#ARTIFACTS[@]}" - if [[ ${#MODIFIED[@]} -gt 0 ]]; then - info_echo "Modified files:" - for f in "${MODIFIED[@]}"; do - clean_path=$(clean_file_path "$f") - if [[ $f =~ \.class$|\.jar$|\.war$|\.exe$ ]]; then - info_echo "M $clean_path (binary)" - else - info_echo "M $clean_path" - fi - done - fi - if [[ ${#ADDED[@]} -gt 0 ]]; then - info_echo "Added files:" - for f in "${ADDED[@]}"; do - clean_path=$(clean_file_path "$f") - info_echo "A $clean_path" - done - fi - if [[ ${#DELETED[@]} -gt 0 ]]; then - echo "Deleted files:" - for f in "${DELETED[@]}"; do - clean_path=$(clean_file_path "$f") - echo "D $clean_path" - done - fi - } > "$SUMMARY_FILE" - info_echo "Constructed summary file created: $SUMMARY_FILE" - fi - fi - fi - fi - - if [[ ${#ARTIFACTS[@]} -gt 0 ]]; then - if [[ -n "${ARTIFACT_FILE:-}" ]]; then - _artifact_dest="${ARTIFACT_FILE}" - elif [[ -n "${REPORT_DIR:-}" ]]; then - _artifact_dest="${REPORT_DIR}/artifacts_${TIMESTAMP}.txt" - elif [[ -n "${REPORT_FILE:-}" ]]; then - _artifact_dest="$(dirname "${REPORT_FILE}")/artifacts_${TIMESTAMP}.txt" - else - _artifact_dest="/tmp/artifacts_${TIMESTAMP}.txt" - fi - debug_echo "Final artifact target: $_artifact_dest" - mkdir -p "$(dirname "$_artifact_dest")" 2>/dev/null || true - - if printf "%s\n" "${ARTIFACTS[@]}" > "$_artifact_dest"; then - info_echo "Artifact report written: $_artifact_dest" - else - debug_error "Failed to write artifact report: $_artifact_dest" - fi - - ARTIFACT_FILE="$_artifact_dest" - else - debug_echo "No artifacts collected; skipping final artifact report write" - fi - - if [[ -n "$JSON_FILE" && "$DRY_RUN" == "false" ]]; then - info_echo "[INFO] Generating JSON output: $JSON_FILE" - if ! command -v jq >/dev/null 2>&1; then - debug_error "[ERROR] jq command not found. Please install jq to generate JSON output." >&2 - exit 1 - fi - debug_echo "Converting arrays to JSON..." - if [[ ${#ADDED[@]} -gt 0 ]]; then - ADDED_JSON=$(printf '%s\n' "${ADDED[@]}" | jq -R . | jq -s .) - else - ADDED_JSON="[]" - fi - debug_echo "ADDED_JSON: $ADDED_JSON" - if [[ ${#MODIFIED[@]} -gt 0 ]]; then - MODIFIED_JSON=$(printf '%s\n' "${MODIFIED[@]}" | jq -R . | jq -s .) - else - MODIFIED_JSON="[]" - fi - debug_echo "MODIFIED_JSON: $MODIFIED_JSON" - if [[ ${#DELETED[@]} -gt 0 ]]; then - DELETED_JSON=$(printf '%s\n' "${DELETED[@]}" | jq -R . | jq -s .) - else - DELETED_JSON="[]" - fi - debug_echo "DELETED_JSON: $DELETED_JSON" - if [[ ${#ARTIFACTS[@]} -gt 0 ]]; then - ARTIFACTS_JSON=$(printf '%s\n' "${ARTIFACTS[@]}" | jq -R . | jq -s .) - else - ARTIFACTS_JSON="[]" - fi - debug_echo "ARTIFACTS_JSON: $ARTIFACTS_JSON" - if jq -n \ - --arg ts "$TIMESTAMP" \ - --arg report "${REPORT_FILE:-null}" \ - --arg summary "${SUMMARY_FILE:-null}" \ - --arg artifacts "${ARTIFACT_FILE:-null}" \ - --argjson added "$ADDED_JSON" \ - --argjson modified "$MODIFIED_JSON" \ - --argjson deleted "$DELETED_JSON" \ - --argjson artifact "$ARTIFACTS_JSON" \ - '{ - timestamp: $ts, - full_diff_report: $report, - summary_report: $summary, - artifact_report: $artifacts, - added_files: $added, - modified_files: $modified, - deleted_files: $deleted, - artifact_files: $artifact - }' > "$JSON_FILE"; then - if [[ -f "$JSON_FILE" ]]; then - JSON_SIZE=$(wc -c < "$JSON_FILE") - info_echo "[INFO] JSON output created: $JSON_FILE (${JSON_SIZE} bytes)" - debug_echo "JSON contents:" - [[ "$DEBUG" == "true" ]] && cat "$JSON_FILE" - else - debug_error "[ERROR] JSON file was not created!" - fi - else - debug_error "[ERROR] jq failed to generate JSON" - exit 1 - fi - fi + ;; + esac + done - if [[ "$SAFE_MODE" == "false" && "$DRY_RUN" == "false" ]]; then - debug_echo "Cleaning up temp directory: $TMP_DIR" - rm -rf "$TMP_DIR" - else - debug_echo "Keeping temp directory: $TMP_DIR" - fi + # Bootstrap + validate_parameters + toggle_debug_mode + toggle_production_mode + setup_report_directories + setup_json_output + create_temp_directory + + # Main execution flow + clone_repository + prepare_diff_command + execute_diff_command + generate_reports + cleanup_temp_directory } main "$@" diff --git a/diff-check/lib/diff_helpers.sh b/diff-check/lib/diff_helpers.sh deleted file mode 100644 index 15dc657..0000000 --- a/diff-check/lib/diff_helpers.sh +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env bash - -debug_echo() { - [[ "${DEBUG:-false}" == "true" ]] && echo "[DEBUG] $*" >&2 -} - -debug_error() { - echo "[ERROR] $*" >&2 -} - -info_echo() { - echo "[INFO] $*" -} - -production_echo() { - [[ "${PRODUCTION_MODE:-false}" == "true" ]] && echo "[PRODUCTION] $*" -} - -production_debug() { - [[ "${PRODUCTION_MODE:-false}" == "true" && "${DEBUG:-false}" == "true" ]] && echo "[PROD DEBUG] $*" >&2 -} - -production_error() { - echo "[PROD ERROR] $*" >&2 -} - -clean_file_path() { - local filepath="$1" - filepath="${filepath#"${TMP_DIR}"/repo/}" - filepath="${filepath#"${APP_DIR}"/}" - filepath="${filepath#/app/}" - filepath="${filepath#/}" - echo "$filepath" -} - -show_tree_diff() { - local all_files=() - local temp_file="/tmp/tree_diff_$$" - for file in "${MODIFIED[@]}"; do - local clean_path - clean_path=$(clean_file_path "$file") - if [[ $file =~ \.class$|\.jar$|\.war$|\.exe$ ]]; then - all_files+=("M:$clean_path (binary)") - else - all_files+=("M:$clean_path") - fi - done - for file in "${ADDED[@]}"; do - local clean_path - clean_path=$(clean_file_path "$file") - all_files+=("A:$clean_path") - done - for file in "${DELETED[@]}"; do - local clean_path - clean_path=$(clean_file_path "$file") - all_files+=("D:$clean_path") - done - if [[ ${#all_files[@]} -eq 0 ]]; then - echo "No changes detected." - return - fi - printf '%s\n' "${all_files[@]}" | sort -t: -k2 > "$temp_file" - echo - echo "Changes detected in: $APP_DIR" - echo "====================================" - local current_dir="" - local files_in_dir=() - while IFS=':' read -r status filepath || [[ -n "$filepath" ]]; do - local dir - dir=$(dirname "$filepath") - local filename - filename=$(basename "$filepath") - [[ "$dir" == "." ]] && dir="" - if [[ "$dir" != "$current_dir" ]]; then - if [[ ${#files_in_dir[@]} -gt 0 ]]; then - for i in "${!files_in_dir[@]}"; do - if [[ $i -eq $((${#files_in_dir[@]} - 1)) ]]; then - echo "└── ${files_in_dir[$i]}" - else - echo "├── ${files_in_dir[$i]}" - fi - done - echo - fi - if [[ -n "$dir" ]]; then - echo "$dir/" - else - echo "./" - fi - current_dir="$dir" - files_in_dir=() - fi - files_in_dir+=("$status $filename") - done < "$temp_file" - if [[ ${#files_in_dir[@]} -gt 0 ]]; then - for i in "${!files_in_dir[@]}"; do - if [[ $i -eq $((${#files_in_dir[@]} - 1)) ]]; then - echo "└── ${files_in_dir[$i]}" - else - echo "├── ${files_in_dir[$i]}" - fi - done - echo - fi - rm -f "$temp_file" -} - -generate_git_style_log() { - echo - echo "=====================================" - echo "Git-style diff log:" - echo "=====================================" - for file in "${MODIFIED[@]}"; do - local clean_path - clean_path=$(clean_file_path "$file") - if [[ $file =~ \.class$|\.jar$|\.war$|\.exe$ ]]; then - echo "M $clean_path (binary)" - else - echo "M $clean_path" - fi - done - for file in "${ADDED[@]}"; do - local clean_path - clean_path=$(clean_file_path "$file") - echo "A $clean_path" - done - for file in "${DELETED[@]}"; do - local clean_path - clean_path=$(clean_file_path "$file") - echo "D $clean_path" - done -} - -show_test_output_help() { - echo "Usage: $(basename "$0") [--app-dir DIR] [--git-url URL] [--branch BR] [--report DIR] [--test-output] [--help]" -} diff --git a/diff-check/lib/fs.sh b/diff-check/lib/fs.sh new file mode 100644 index 0000000..7197f3c --- /dev/null +++ b/diff-check/lib/fs.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +clean_file_path() { + local filepath="$1" + filepath="${filepath#"${TMP_DIR}"/repo/}" + filepath="${filepath#"${APP_DIR}"/}" + filepath="${filepath#/app/}" + filepath="${filepath#/}" + echo "$filepath" +} + +cleanup_temp_directory() { + if [[ "$SAFE_MODE" == "false" && "$DRY_RUN" == "false" ]]; then + debug_echo "Cleaning up temp directory: $TMP_DIR" + rm -rf "$TMP_DIR" + else + debug_echo "Keeping temp directory: $TMP_DIR" + fi +} diff --git a/diff-check/lib/git.sh b/diff-check/lib/git.sh new file mode 100644 index 0000000..0c5f29a --- /dev/null +++ b/diff-check/lib/git.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash + +clone_repository() { + # Skip cloning when in dry-run mode to avoid network operations and failures + if [[ "${DRY_RUN:-false}" == "true" ]]; then + info_echo "Skipping git clone in dry-run mode" + mkdir -p "${TMP_DIR}/repo" + return 0 + fi + + if [[ "$TEST_OUTPUT" != "true" ]]; then + info_echo "Cloning $GIT_URL (branch: $BRANCH)..." + if [[ -n "${SSH_KEY:-}" ]]; then + if [[ ! -f "$SSH_KEY" ]]; then + debug_error "[ERROR] SSH key file not found: $SSH_KEY" >&2 + exit 1 + fi + debug_echo "Using SSH key: $SSH_KEY" + export GIT_SSH_COMMAND="ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new" + debug_echo "Clone command: GIT_SSH_COMMAND='$GIT_SSH_COMMAND' git clone --quiet --depth=1 --branch '$BRANCH' '$GIT_URL' '$TMP_DIR/repo'" + else + debug_echo "Clone command: git clone --quiet --depth=1 --branch '$BRANCH' '$GIT_URL' '$TMP_DIR/repo'" + fi + if ! git clone --quiet --depth=1 --branch "$BRANCH" "$GIT_URL" "$TMP_DIR/repo"; then + production_error "Git clone failed for $GIT_URL (branch $BRANCH)" + debug_error "[ERROR] Git clone failed!" >&2 + if [[ -n "${SSH_KEY:-}" ]]; then + production_error "Suggest testing SSH connection with: ssh -i $SSH_KEY -T git@github.com" + debug_error "[ERROR] Try testing SSH connection: ssh -i $SSH_KEY -T git@github.com" >&2 + fi + exit 1 + fi + unset GIT_SSH_COMMAND + info_echo "Repository cloned successfully" + # Immediate debug marker to ensure we see that clone_repository completed + debug_echo "clone_repository: repository cloned" + info_echo "Repository cloned into: $TMP_DIR/repo" + debug_echo "Repo contents (first 10 files):" + # Use a portable while-read with process substitution to avoid requiring 'mapfile' + while IFS= read -r repo_file; do + debug_echo " $repo_file" + done < <(find "$TMP_DIR/repo" -type f 2>/dev/null | head -10) + debug_echo "Repo listing complete" + # Another explicit marker after listing to confirm function progress + debug_echo "[DEBUG] clone_repository: repo listing complete" + else + info_echo "Skipping git clone in test mode" + mkdir -p "$TMP_DIR/repo" + fi +} + +prepare_diff_command() { + info_echo "Comparing deployed files in $APP_DIR with Git HEAD..." + + if [[ ! -d "$APP_DIR" ]]; then + debug_error "[ERROR] App directory does not exist: $APP_DIR" >&2 + exit 1 + fi + cd "$APP_DIR" || exit + debug_echo "Changed to directory: $(pwd)" + + [[ "$SAFE_MODE" == "true" ]] && info_echo "Safe mode ON." + [[ "$DRY_RUN" == "true" ]] && info_echo "Dry-run mode ON." + + EXCLUDE_PATTERNS=( ".git" "logs" "tmp" "target" ) + EXCLUDE_ARGS=() + + if command -v gdiff >/dev/null 2>&1; then + DIFF_BIN="gdiff" + debug_echo "Using GNU diff: gdiff" + else + DIFF_BIN="diff" + debug_echo "Using standard diff: diff" + fi + + if "$DIFF_BIN" --exclude=nonexistent /dev/null /dev/null >/dev/null 2>&1; then + for p in "${EXCLUDE_PATTERNS[@]}"; do EXCLUDE_ARGS+=( "--exclude=$p" ); done + else + for p in "${EXCLUDE_PATTERNS[@]}"; do EXCLUDE_ARGS+=( "-x" "$p" ); done + fi + + DIFF_OPTS=( -r -q ) + if "$DIFF_BIN" -N /dev/null /dev/null >/dev/null 2>&1; then + DIFF_OPTS+=( -N ) + debug_echo "Diff supports -N flag" + fi +} + +execute_diff_command() { + DIFF_CMD=( "$DIFF_BIN" "${DIFF_OPTS[@]}" "${EXCLUDE_ARGS[@]}" "$TMP_DIR/repo" "$APP_DIR" ) + + debug_echo "Full diff command: ${DIFF_CMD[*]}" + + if [[ "$DRY_RUN" == "true" ]]; then + info_echo "[DRY-RUN] Command: ${DIFF_CMD[*]}" + fi + + if [[ -n "$REPORT_FILE" ]]; then + info_echo "Running diff and generating report..." + RAW_DIFF_FILE="$TMP_DIR/raw_diff.tmp" + set +e + "${DIFF_CMD[@]}" > "$RAW_DIFF_FILE" 2>&1 + DIFF_EXIT=$? + set -e + debug_echo "Diff exit code: $DIFF_EXIT (0=no diff, 1=differences, 2=error)" + if [[ $DIFF_EXIT -eq 2 ]]; then + debug_error "[ERROR] Diff command failed! Check raw output below:" + cat "$RAW_DIFF_FILE" + exit 1 + fi + if [[ -f "$RAW_DIFF_FILE" ]]; then + REPORT_SIZE=$(wc -c < "$RAW_DIFF_FILE") + debug_echo "Raw diff created: $RAW_DIFF_FILE (${REPORT_SIZE} bytes)" + if [[ $REPORT_SIZE -eq 0 ]]; then + debug_echo "Raw diff is empty - no differences found" + else + debug_echo "First 10 lines of raw diff:" + if [[ "$DEBUG" == "true" ]]; then + head -10 "$RAW_DIFF_FILE" + fi + fi + else + debug_error "[ERROR] Raw diff file was not created: $RAW_DIFF_FILE" + exit 1 + fi + else + info_echo "Running diff (no report file)" + set +e + "${DIFF_CMD[@]}" + set -e + fi +} + + diff --git a/diff-check/lib/output.sh b/diff-check/lib/output.sh new file mode 100644 index 0000000..2b9b50f --- /dev/null +++ b/diff-check/lib/output.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash + +setup_report_directories() { + info_echo "Setting up report directories..." + if [[ -n "${REPORT_DIR:-}" ]]; then + info_echo "Preparing report directory: $REPORT_DIR" + shopt -s extglob 2>/dev/null || true + REPORT_DIR="${REPORT_DIR%%+(/)}" + shopt -u extglob 2>/dev/null || true + if [[ "$REPORT_DIR" != /* ]]; then + if [[ -d "$REPORT_DIR" ]]; then + REPORT_DIR="$(cd "$REPORT_DIR" && pwd)" + elif [[ -d "$(dirname "$REPORT_DIR")" ]]; then + REPORT_DIR="$(cd "$(dirname "$REPORT_DIR")" && pwd)/$(basename "$REPORT_DIR")" + else + REPORT_DIR="$(pwd)/$REPORT_DIR" + fi + fi + debug_echo "Creating report directory: $REPORT_DIR" + if ! mkdir -p "$REPORT_DIR" 2>/dev/null; then + debug_error "Failed to create directory: $REPORT_DIR" + exit 1 + fi + if [[ ! -w "$REPORT_DIR" ]]; then + debug_error "Directory not writable: $REPORT_DIR" + exit 1 + fi + info_echo "Report directory ready: $REPORT_DIR" + info_echo "Directory permissions: $(ls -ld "$REPORT_DIR")" + + if [[ -z "${REPORT_FILE:-}" ]]; then + REPORT_FILE="${REPORT_DIR}/report_${TIMESTAMP}.log" + fi + if [[ -z "${SUMMARY_FILE:-}" ]]; then + SUMMARY_FILE="${REPORT_DIR}/summary_${TIMESTAMP}.log" + fi + if [[ -z "${ARTIFACT_FILE:-}" ]]; then + ARTIFACT_FILE="${REPORT_DIR}/artifact_${TIMESTAMP}.txt" + fi + debug_echo "Derived report paths: REPORT_FILE=$REPORT_FILE, SUMMARY_FILE=$SUMMARY_FILE, ARTIFACT_FILE=$ARTIFACT_FILE" + else + info_echo "No report directory specified, skipping report generation" + fi +} + +setup_json_output() { + if [[ -z "${JSON_FILE:-}" ]]; then + if [[ -n "${REPORT_DIR:-}" ]]; then + JSON_FILE="${REPORT_DIR}/report_data_${TIMESTAMP}.json" + fi + fi + + if [[ -n "${JSON_FILE:-}" ]]; then + if [[ "$JSON_FILE" != /* ]]; then + JSON_FILE="$(pwd)/$JSON_FILE" + fi + + JSON_DIR="$(dirname "$JSON_FILE")" + debug_echo "Creating JSON output directory: $JSON_DIR" + + if ! mkdir -p "$JSON_DIR" 2>/dev/null; then + debug_error "[ERROR] Failed to create directory: $JSON_DIR" >&2 + exit 1 + fi + + if [[ ! -w "$JSON_DIR" ]]; then + debug_error "[ERROR] Directory not writable: $JSON_DIR" >&2 + exit 1 + fi + + info_echo "JSON output directory ready: $JSON_DIR" + fi +} + +create_temp_directory() { + debug_echo "Creating temp directory: $TMP_DIR" + if ! mkdir -p "$TMP_DIR"; then + debug_error "[ERROR] Failed to create temp directory: $TMP_DIR" >&2 + exit 1 + fi + info_echo "Temp directory created: $TMP_DIR" +} + +show_test_output_help() { + echo "Usage: $(basename "$0") [--app-dir /path/to/dir ] [--git-url git|https] [--branch branch-name] [--help]" +} diff --git a/diff-check/lib/reports.sh b/diff-check/lib/reports.sh new file mode 100644 index 0000000..5243677 --- /dev/null +++ b/diff-check/lib/reports.sh @@ -0,0 +1,349 @@ +#!/usr/bin/env bash + +declare -a MODIFIED=() +declare -a ADDED=() +declare -a DELETED=() +declare -a ARTIFACTS=() + +# Ensure arrays are declared even if this file is sourced in a context +# where they weren't previously set (avoids 'unbound variable' under set -u) +for _arr in MODIFIED ADDED DELETED ARTIFACTS; do + if ! declare -p "$_arr" >/dev/null 2>&1; then + eval "declare -a $_arr=()" + fi +done + +generate_placeholder_reports() { + # If running a dry-run, create lightweight placeholder reports so callers see files + if [[ "${DRY_RUN:-false}" == "true" ]]; then + if [[ -n "${REPORT_FILE:-}" ]]; then + mkdir -p "$(dirname "${REPORT_FILE}")" 2>/dev/null || true + echo "Dry-run placeholder: no diff executed. Run without --dry-run to generate a real diff report." > "${REPORT_FILE}" || debug_error "Failed to write placeholder report: ${REPORT_FILE}" + info_echo "Placeholder diff report created: ${REPORT_FILE}" + fi + if [[ "${SUMMARY:-false}" == "true" && -n "${SUMMARY_FILE:-}" ]]; then + echo "Dry-run placeholder summary: no diff executed." > "${SUMMARY_FILE}" || debug_error "Failed to write placeholder summary: ${SUMMARY_FILE}" + info_echo "Placeholder summary created: ${SUMMARY_FILE}" + fi + # still generate JSON if requested + fi +} + +generate_reports() { + generate_placeholder_reports + + if [[ "$DRY_RUN" == "false" && ( -n "$REPORT_FILE" || -n "$TEST_PARSE_FILE" ) ]]; then + debug_echo "Condition passed: proceeding with report parsing" + if [[ "$TEST_OUTPUT" == "true" ]]; then + PARSE_FILE="${TEST_PARSE_FILE:-${REPORT_FILE:-}}" + else + PARSE_FILE="${RAW_DIFF_FILE:-$REPORT_FILE}" + fi + if [[ -f "$PARSE_FILE" && -s "$PARSE_FILE" ]]; then + info_echo "Parsing diff output..." + LINE_COUNT=0 + set +e + while IFS= read -r line || [[ -n "$line" ]]; do + ((LINE_COUNT++)) + if [[ $line == "Files "* ]]; then + file=$(echo "$line" | awk '{print $4}') + MODIFIED+=("$file") + debug_echo "M $file" + [[ $file =~ \.class$|\.jar$ ]] && ARTIFACTS+=("$file") + elif [[ $line == "Only in "* ]]; then + file=$(echo "$line" | sed -E 's/^Only in ([^:]+): (.+)$/\1\/\2/') + if [[ $line == *"$TMP_DIR/repo"* ]]; then + DELETED+=("$file") + debug_echo "D $file" + else + ADDED+=("$file") + debug_echo "A $file" + fi + [[ $file =~ \.class$|\.jar$ ]] && ARTIFACTS+=("$file") + elif [[ $line == "Binary files "* ]]; then + file=$(echo "$line" | awk '{print $5}') + MODIFIED+=("$file") + debug_echo "M $file (binary)" + [[ $file =~ \.class$|\.jar$ ]] && ARTIFACTS+=("$file") + fi + done < "$PARSE_FILE" + set -e + info_echo "Parsed $LINE_COUNT lines from report" + info_echo "M: ${#MODIFIED[@]} modified files" + info_echo "A: ${#ADDED[@]} added files" + info_echo "D: ${#DELETED[@]} deleted files" + info_echo "Found ${#ARTIFACTS[@]} artifact files" + if [[ -n "$REPORT_FILE" ]]; then + { + show_tree_diff + generate_git_style_log + } > "$REPORT_FILE" + info_echo "Diff report saved to: $REPORT_FILE" + else + show_tree_diff + fi + else + debug_echo "Parse file is empty, no changes detected" + info_echo "No changes detected." + if [[ -n "$REPORT_FILE" ]]; then + info_echo "No changes detected." > "$REPORT_FILE" + info_echo "Empty diff report saved to: $REPORT_FILE" + fi + fi + if [[ "$SUMMARY" == "true" ]]; then + debug_echo "Generating summary report: $SUMMARY_FILE" + SUMMARY_SOURCE="${PARSE_FILE:-${RAW_DIFF_FILE:-}}" + if [[ -z "${SUMMARY_SOURCE:-}" ]]; then + debug_error "No raw diff available to generate summary" + else + if grep -E '^(Files |Only in |Binary files )' "$SUMMARY_SOURCE" > "$SUMMARY_FILE" 2>/dev/null; then + if [[ -f "$SUMMARY_FILE" ]]; then + SUMMARY_SIZE=$(wc -c < "$SUMMARY_FILE") + info_echo "Summary report created (${SUMMARY_SIZE} bytes)" + else + info_echo "[WARN] Summary file not created" + fi + else + { + info_echo "Constructed summary generated from parsed diff (counts):" + info_echo "Artifacts: ${#ARTIFACTS[@]}" + if [[ ${#MODIFIED[@]} -gt 0 ]]; then + info_echo "Modified files:" + for f in "${MODIFIED[@]}"; do + clean_path=$(clean_file_path "$f") + if [[ $f =~ \.class$|\.jar$|\.war$|\.exe$ ]]; then + info_echo "M $clean_path (binary)" + else + info_echo "M $clean_path" + fi + done + fi + if [[ ${#ADDED[@]} -gt 0 ]]; then + info_echo "Added files:" + for f in "${ADDED[@]}"; do + clean_path=$(clean_file_path "$f") + info_echo "A $clean_path" + done + fi + if [[ ${#DELETED[@]} -gt 0 ]]; then + info_echo "Deleted files:" + for f in "${DELETED[@]}"; do + clean_path=$(clean_file_path "$f") + info_echo "D $clean_path" + done + fi + } > "$SUMMARY_FILE" + info_echo "Constructed summary file created: $SUMMARY_FILE" + fi + fi + fi + fi + + + # Generate artifact report if artifacts exist + if [[ ${#ARTIFACTS[@]} -gt 0 ]]; then + if [[ -n "${ARTIFACT_FILE:-}" ]]; then + _artifact_dest="${ARTIFACT_FILE}" + elif [[ -n "${REPORT_DIR:-}" ]]; then + _artifact_dest="${REPORT_DIR}/artifact_${TIMESTAMP}.txt" + elif [[ -n "${REPORT_FILE:-}" ]]; then + _artifact_dest="$(dirname "${REPORT_FILE}")/artifact_${TIMESTAMP}.txt" + else + _artifact_dest="/tmp/artifact_${TIMESTAMP}.txt" + fi + debug_echo "Final artifact target: $_artifact_dest" + mkdir -p "$(dirname "$_artifact_dest")" 2>/dev/null || true + + if printf "%s\n" "${ARTIFACTS[@]}" > "$_artifact_dest"; then + info_echo "Artifact report written: $_artifact_dest" + ARTIFACT_FILE="$_artifact_dest" + else + debug_error "Failed to write artifact report: $_artifact_dest" + fi + else + debug_echo "No artifacts collected; skipping artifact report" + fi + + # Generate JSON report + if [[ -n "$JSON_FILE" ]]; then + info_echo "[INFO] Generating JSON output: $JSON_FILE" + if ! command -v jq >/dev/null 2>&1; then + debug_error "[ERROR] jq command not found. Please install jq to generate JSON output." >&2 + exit 1 + fi + + debug_echo "Converting arrays to JSON..." + + if [[ ${#ADDED[@]} -gt 0 ]]; then + ADDED_JSON=$(printf '%s\n' "${ADDED[@]}" | jq -R . | jq -s .) + else + ADDED_JSON="[]" + fi + debug_echo "ADDED_JSON: $ADDED_JSON" + + if [[ ${#MODIFIED[@]} -gt 0 ]]; then + MODIFIED_JSON=$(printf '%s\n' "${MODIFIED[@]}" | jq -R . | jq -s .) + else + MODIFIED_JSON="[]" + fi + debug_echo "MODIFIED_JSON: $MODIFIED_JSON" + + if [[ ${#DELETED[@]} -gt 0 ]]; then + DELETED_JSON=$(printf '%s\n' "${DELETED[@]}" | jq -R . | jq -s .) + else + DELETED_JSON="[]" + fi + debug_echo "DELETED_JSON: $DELETED_JSON" + + if [[ ${#ARTIFACTS[@]} -gt 0 ]]; then + ARTIFACTS_JSON=$(printf '%s\n' "${ARTIFACTS[@]}" | jq -R . | jq -s .) + else + ARTIFACTS_JSON="[]" + fi + debug_echo "ARTIFACTS_JSON: $ARTIFACTS_JSON" + + if jq -n \ + --arg ts "$TIMESTAMP" \ + --arg report "${REPORT_FILE:-null}" \ + --arg summary "${SUMMARY_FILE:-null}" \ + --arg artifacts "${ARTIFACT_FILE:-null}" \ + --argjson added "$ADDED_JSON" \ + --argjson modified "$MODIFIED_JSON" \ + --argjson deleted "$DELETED_JSON" \ + --argjson artifact "$ARTIFACTS_JSON" \ + '{ + timestamp: $ts, + full_diff_report: $report, + summary_report: $summary, + artifact_report: $artifacts, + added_files: $added, + modified_files: $modified, + deleted_files: $deleted, + artifact_files: $artifact + }' > "$JSON_FILE"; then + if [[ -f "$JSON_FILE" ]]; then + JSON_SIZE=$(wc -c < "$JSON_FILE") + info_echo "[INFO] JSON output created: $JSON_FILE (${JSON_SIZE} bytes)" + debug_echo "JSON contents:" + if [[ "$DEBUG" == "true" ]]; then + cat "$JSON_FILE" + fi + else + debug_error "[ERROR] JSON file was not created!" + fi + else + debug_error "[ERROR] jq failed to generate JSON" + exit 1 + fi + fi +} + +generate_git_style_log() { + echo + echo "=====================================" + echo "Git-style diff log:" + echo "=====================================" + if [[ ${#MODIFIED[@]} -gt 0 ]]; then + for file in "${MODIFIED[@]}"; do + local clean_path + clean_path=$(clean_file_path "$file") + if [[ $file =~ \.class$|\.jar$|\.war$|\.exe$ ]]; then + echo "M $clean_path (binary)" + else + echo "M $clean_path" + fi + done + fi + if [[ ${#ADDED[@]} -gt 0 ]]; then + for file in "${ADDED[@]}"; do + local clean_path + clean_path=$(clean_file_path "$file") + echo "A $clean_path" + done + fi + if [[ ${#DELETED[@]} -gt 0 ]]; then + for file in "${DELETED[@]}"; do + local clean_path + clean_path=$(clean_file_path "$file") + echo "D $clean_path" + done + fi +} + +show_tree_diff() { + local all_files=() + local temp_file="/tmp/tree_diff_$$" + if [[ ${#MODIFIED[@]} -gt 0 ]]; then + for file in "${MODIFIED[@]}"; do + local clean_path + clean_path=$(clean_file_path "$file") + if [[ $file =~ \.class$|\.jar$|\.war$|\.exe$ ]]; then + all_files+=("M:$clean_path (binary)") + else + all_files+=("M:$clean_path") + fi + done + fi + if [[ ${#ADDED[@]} -gt 0 ]]; then + for file in "${ADDED[@]}"; do + local clean_path + clean_path=$(clean_file_path "$file") + all_files+=("A:$clean_path") + done + fi + if [[ ${#DELETED[@]} -gt 0 ]]; then + for file in "${DELETED[@]}"; do + local clean_path + clean_path=$(clean_file_path "$file") + all_files+=("D:$clean_path") + done + fi + if [[ ${#all_files[@]} -eq 0 ]]; then + echo "No changes detected." + return + fi + printf '%s\n' "${all_files[@]}" | sort -t: -k2 > "$temp_file" + echo + echo "Changes detected in: $APP_DIR" + echo "====================================" + local current_dir="" + local files_in_dir=() + while IFS=':' read -r status filepath || [[ -n "$filepath" ]]; do + local dir + dir=$(dirname "$filepath") + local filename + filename=$(basename "$filepath") + [[ "$dir" == "." ]] && dir="" + if [[ "$dir" != "$current_dir" ]]; then + if [[ ${#files_in_dir[@]} -gt 0 ]]; then + for i in "${!files_in_dir[@]}"; do + if [[ $i -eq $((${#files_in_dir[@]} - 1)) ]]; then + echo "└── ${files_in_dir[$i]}" + else + echo "├── ${files_in_dir[$i]}" + fi + done + echo + fi + if [[ -n "$dir" ]]; then + echo "$dir/" + else + echo "./" + fi + current_dir="$dir" + files_in_dir=() + fi + files_in_dir+=("$status $filename") + done < "$temp_file" + if [[ ${#files_in_dir[@]} -gt 0 ]]; then + for i in "${!files_in_dir[@]}"; do + if [[ $i -eq $((${#files_in_dir[@]} - 1)) ]]; then + echo "└── ${files_in_dir[$i]}" + else + echo "├── ${files_in_dir[$i]}" + fi + done + echo + fi + rm -f "$temp_file" +} diff --git a/diff-check/lib/validation.sh b/diff-check/lib/validation.sh new file mode 100644 index 0000000..81640e7 --- /dev/null +++ b/diff-check/lib/validation.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +function validate_parameters() { + [[ -z "${APP_DIR:-}" ]] && { log_error "--app-dir is required"; show_test_output_help; exit 1; } + [[ -z "${GIT_URL:-}" ]] && { log_error "--git-url is required"; show_test_output_help; exit 1; } + [[ -z "${BRANCH:-}" ]] && { log_error "--branch is required"; show_test_output_help; exit 1; } +} diff --git a/diff-check/tests/run_integration_tests.sh b/diff-check/tests/run_integration_tests.sh index e5451eb..6e4f2a4 100755 --- a/diff-check/tests/run_integration_tests.sh +++ b/diff-check/tests/run_integration_tests.sh @@ -20,7 +20,7 @@ GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[1;33m' BLUE='\033[0;34m' -NC='\033[0m' # No Color +NO_COLOR='\033[0m' TOTAL_MODULES=0 PASSED_MODULES=0 @@ -36,7 +36,7 @@ run_test_module() { TOTAL_MODULES=$((TOTAL_MODULES + 1)) log_info "Running integration test module: $module_name" - echo -e "${BLUE}=== $module_name ===${NC}" + echo -e "${BLUE}=== $module_name ===${NO_COLOR}" local exit_code=0 if [[ "$module_name" == "test_ssh" || "$module_name" == "test_service_detection" ]]; then @@ -53,10 +53,10 @@ run_test_module() { fi if [[ $exit_code -eq 0 ]]; then - echo -e "${GREEN}✓ $module_name PASSED${NC}" + echo -e "${GREEN}✓ $module_name PASSED${NO_COLOR}" PASSED_MODULES=$((PASSED_MODULES + 1)) else - echo -e "${RED}✗ $module_name FAILED${NC}" + echo -e "${RED}✗ $module_name FAILED${NO_COLOR}" FAILED_MODULES=$((FAILED_MODULES + 1)) FAILED_MODULE_NAMES+=("$module_name") fi @@ -100,22 +100,22 @@ run_all_integration_tests() { } generate_summary() { - echo -e "${BLUE}=== Integration Test Summary ===${NC}" + echo -e "${BLUE}=== Integration Test Summary ===${NO_COLOR}" echo -e "Total modules: $TOTAL_MODULES" - echo -e "${GREEN}Passed: $PASSED_MODULES${NC}" - echo -e "${RED}Failed: $FAILED_MODULES${NC}" + echo -e "${GREEN}Passed: $PASSED_MODULES${NO_COLOR}" + echo -e "${RED}Failed: $FAILED_MODULES${NO_COLOR}" if [[ $FAILED_MODULES -gt 0 ]]; then - echo -e "${RED}Failed modules: ${FAILED_MODULE_NAMES[*]}${NC}" + echo -e "${RED}Failed modules: ${FAILED_MODULE_NAMES[*]}${NO_COLOR}" fi echo "" if [[ $FAILED_MODULES -eq 0 ]]; then - echo -e "${GREEN}🎉 All integration tests passed!${NC}" + echo -e "${GREEN}🎉 All integration tests passed!${NO_COLOR}" return 0 else - echo -e "${RED}❌ $FAILED_MODULES integration test modules failed${NC}" + echo -e "${RED}❌ $FAILED_MODULES integration test modules failed${NO_COLOR}" return 1 fi } diff --git a/diff-check/tests/run_tests.sh b/diff-check/tests/run_tests.sh index 0cdf846..6cc6a29 100755 --- a/diff-check/tests/run_tests.sh +++ b/diff-check/tests/run_tests.sh @@ -14,18 +14,18 @@ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' -NC='\033[0m' # No Color +NO_COLOR='\033[0m' log_info() { - echo -e "${BLUE}[RUNNER]${NC} $*" + echo -e "${BLUE}[RUNNER]${NO_COLOR} $*" } log_success() { - echo -e "${GREEN}[SUCCESS]${NC} $*" + echo -e "${GREEN}[SUCCESS]${NO_COLOR} $*" } log_error() { - echo -e "${RED}[ERROR]${NC} $*" + echo -e "${RED}[ERROR]${NO_COLOR} $*" } main() { diff --git a/diff-check/tests/watch.sh b/diff-check/tests/watch.sh deleted file mode 100755 index c9e4ce8..0000000 --- a/diff-check/tests/watch.sh +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env bash - -# watch.sh - Auto-run tests when files change -# Lightweight file watcher for test-driven development - -set -eo pipefail # Changed: removed 'u' to allow test failures, keep 'e' for critical errors - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -WATCH_PATHS=( - "$PROJECT_ROOT/lib" - "$PROJECT_ROOT/tests/unit" - "$PROJECT_ROOT/tests/integration" -) -WATCH_PATTERN="*.sh" -TEST_COMMAND="${1:-unit}" # Default to unit tests, or pass 'all', 'integration' - -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -log_info() { - echo -e "${BLUE}[WATCH]${NC} $*" -} - -log_success() { - echo -e "${GREEN}[WATCH]${NC} $*" -} - -log_error() { - echo -e "${RED}[WATCH]${NC} $*" -} - -run_tests() { - clear - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo -e "${YELLOW}Running tests...${NC} $(date '+%H:%M:%S')" - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo "" - - local exit_code=0 - case "$TEST_COMMAND" in - unit|u) - "$SCRIPT_DIR/run_unit_tests.sh" || exit_code=$? - ;; - integration|i) - "$SCRIPT_DIR/run_integration_tests.sh" || exit_code=$? - ;; - all|a) - "$SCRIPT_DIR/run_tests.sh" || exit_code=$? - ;; - *) - log_error "Unknown test command: $TEST_COMMAND" - log_info "Usage: $0 [unit|integration|all]" - exit 1 - ;; - esac - - echo "" - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - if [[ $exit_code -eq 0 ]]; then - log_success "Tests passed ✓ - Watching for changes..." - else - log_error "Tests failed ✗ - Watching for changes..." - fi - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo "" - - return 0 -} - -watch_with_fswatch() { - log_info "Using fswatch for file monitoring (optimal)" - - local fswatch_args=() - for path in "${WATCH_PATHS[@]}"; do - fswatch_args+=("$path") - done - - run_tests - - fswatch -0 --event Updated --recursive --exclude '.*' --include '\.sh$' "${fswatch_args[@]}" | while read -d "" event; do - log_info "Change detected: $(basename "$event")" - sleep 0.5 - run_tests - done -} - -watch_with_polling() { - log_info "Using polling fallback (install fswatch for better performance: brew install fswatch)" - - declare -a file_checksums - - get_checksums() { - local checksums="" - for path in "${WATCH_PATHS[@]}"; do - if [[ -d "$path" ]]; then - checksums+=$(find "$path" -name "*.sh" -type f -exec shasum {} \; 2>/dev/null || true) - fi - done - echo "$checksums" - } - - run_tests - - local previous_checksums - previous_checksums=$(get_checksums) - - log_info "Polling for changes every 2 seconds..." - - while true; do - sleep 2 - - local current_checksums - current_checksums=$(get_checksums) - - if [[ "$current_checksums" != "$previous_checksums" ]]; then - log_info "Changes detected" - sleep 0.3 - run_tests - previous_checksums=$(get_checksums) - fi - done -} - -log_info "Starting test watcher for: $TEST_COMMAND tests" -log_info "Watching paths:" -for path in "${WATCH_PATHS[@]}"; do - log_info " - $path" -done -echo "" - -if command -v fswatch &> /dev/null; then - watch_with_fswatch -else - watch_with_polling -fi diff --git a/lib/logging.sh b/lib/logging.sh new file mode 100644 index 0000000..ff01f16 --- /dev/null +++ b/lib/logging.sh @@ -0,0 +1,63 @@ +export GREEN='\033[0;32m' +export YELLOW='\033[1;33m' +export BLUE='\033[0;34m' +export RED='\033[0;31m' +export NO_COLOR='\033[0m' + +debug_echo() { + [[ "${DEBUG:-false}" == "true" ]] && echo "[DEBUG] $*" >&2 + return 0 +} + +debug_error() { + echo "[ERROR] $*" >&2 +} + +info_echo() { + echo "[INFO] $*" +} + +production_echo() { + [[ "${PRODUCTION_MODE:-false}" == "true" ]] && echo "[PRODUCTION] $*" + return 0 +} + +production_debug() { + [[ "${PRODUCTION_MODE:-false}" == "true" && "${DEBUG:-false}" == "true" ]] && echo "[PROD DEBUG] $*" >&2 + return 0 +} + +production_error() { + echo "[PROD ERROR] $*" >&2 +} + +log_info() { + echo -e "${BLUE}[INFO]${NO_COLOR} $*" +} + +log_success() { + echo -e "${GREEN}[INFO]${NO_COLOR} $*" +} + +log_error() { + echo -e "${RED}[ERROR]${NO_COLOR} $*" +} + +toggle_debug_mode() { + if [[ "$DEBUG" == "true" ]]; then + debug_echo "Debug mode enabled" + if [[ -n "$1" ]]; then + debug_echo "$1" + fi + fi +} + +toggle_production_mode() { + if [[ "$PRODUCTION_MODE" == "true" ]]; then + production_echo "Production mode enabled" + production_debug "Script directory: $SCRIPT_DIR" + production_debug "Arguments: APP_DIR=${APP_DIR:-}, GIT_URL=${GIT_URL:-}, BRANCH=${BRANCH:-}, REPORT_DIR=${REPORT_DIR:-}, TEST_OUTPUT=${TEST_OUTPUT}" + production_debug "Temporary directory: $TMP_DIR" + production_debug "PRODUCTION_MODE=${PRODUCTION_MODE}" + fi +} diff --git a/lib/output.sh b/lib/output.sh new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/output.sh @@ -0,0 +1 @@ + diff --git a/server-audit/Makefile b/server-audit/Makefile index a53d208..f802d3b 100644 --- a/server-audit/Makefile +++ b/server-audit/Makefile @@ -71,12 +71,12 @@ watch: watch-unit watch-unit: @echo "Starting test watcher (unit tests)..." - @./tests/watch.sh unit + @../tests/watch.sh unit watch-integration: @echo "Starting test watcher (integration tests)..." - @./tests/watch.sh integration + @../tests/watch.sh integration watch-all: @echo "Starting test watcher (all tests)..." - @./tests/watch.sh all + @../tests/watch.sh all diff --git a/server-audit/README.md b/server-audit/README.md index 980a2d1..419f8cb 100644 --- a/server-audit/README.md +++ b/server-audit/README.md @@ -1,7 +1,7 @@ # `server-audit` [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -![Version](https://img.shields.io/badge/version-1.0.0--alpha-blue) +![Version](https://img.shields.io/badge/version-1.0.0--rc.1-yellow) ![Coverage](https://img.shields.io/badge/coverage-47.0%25-red) A secure, modular server auditing tool for extensible binary checking. diff --git a/server-audit/docs/assets/images/logo.svg b/server-audit/docs/assets/images/logo.svg deleted file mode 100644 index 8590c72..0000000 --- a/server-audit/docs/assets/images/logo.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/server-audit/tests/run_integration_tests.sh b/server-audit/tests/run_integration_tests.sh index ee735db..8dd63c3 100755 --- a/server-audit/tests/run_integration_tests.sh +++ b/server-audit/tests/run_integration_tests.sh @@ -24,7 +24,7 @@ GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[1;33m' BLUE='\033[0;34m' -NC='\033[0m' # No Color +NO_COLOR='\033[0m' # Test tracking TOTAL_MODULES=0 @@ -37,12 +37,12 @@ run_test_module() { local test_file="$1" local module_name module_name=$(basename "$test_file" .sh) - + TOTAL_MODULES=$((TOTAL_MODULES + 1)) - + log_info "Running integration test module: $module_name" - echo -e "${BLUE}=== $module_name ===${NC}" - + echo -e "${BLUE}=== $module_name ===${NO_COLOR}" + local exit_code=0 if [[ "$module_name" == "test_ssh" || "$module_name" == "test_service_detection" ]]; then # SSH and service detection tests need server list and SSH key @@ -56,16 +56,16 @@ run_test_module() { # Other tests don't need SSH parameters "$test_file" || exit_code=$? fi - + if [[ $exit_code -eq 0 ]]; then - echo -e "${GREEN}✓ $module_name PASSED${NC}" + echo -e "${GREEN}✓ $module_name PASSED${NO_COLOR}" PASSED_MODULES=$((PASSED_MODULES + 1)) else - echo -e "${RED}✗ $module_name FAILED${NC}" + echo -e "${RED}✗ $module_name FAILED${NO_COLOR}" FAILED_MODULES=$((FAILED_MODULES + 1)) FAILED_MODULE_NAMES+=("$module_name") fi - + echo "" # Don't return the exit code to prevent script from exiting early return 0 @@ -75,22 +75,22 @@ run_test_module() { run_all_integration_tests() { log_info "Starting integration test suite" log_info "Integration tests directory: $INTEGRATION_DIR" - + if [[ ! -d "$INTEGRATION_DIR" ]]; then log_error "Integration tests directory not found: $INTEGRATION_DIR" return 1 fi - + local test_files test_files=($(find "$INTEGRATION_DIR" -name "test_*.sh" -type f | sort)) - + if [[ ${#test_files[@]} -eq 0 ]]; then log_error "No integration test files found in $INTEGRATION_DIR" return 1 fi - + log_info "Found ${#test_files[@]} integration test modules" - + # Display test configuration log_info "Test Configuration:" log_info " SSH Key: $SSH_KEY" @@ -98,7 +98,7 @@ run_all_integration_tests() { log_info " Run SSH Tests: $RUN_SSH_TESTS" log_info " Run Service Tests: $RUN_SERVICE_TESTS" echo "" - + # Run each test module for test_file in "${test_files[@]}"; do run_test_module "$test_file" @@ -107,22 +107,22 @@ run_all_integration_tests() { # Generate summary report generate_summary() { - echo -e "${BLUE}=== Integration Test Summary ===${NC}" + echo -e "${BLUE}=== Integration Test Summary ===${NO_COLOR}" echo -e "Total modules: $TOTAL_MODULES" - echo -e "${GREEN}Passed: $PASSED_MODULES${NC}" - echo -e "${RED}Failed: $FAILED_MODULES${NC}" - + echo -e "${GREEN}Passed: $PASSED_MODULES${NO_COLOR}" + echo -e "${RED}Failed: $FAILED_MODULES${NO_COLOR}" + if [[ $FAILED_MODULES -gt 0 ]]; then - echo -e "${RED}Failed modules: ${FAILED_MODULE_NAMES[*]}${NC}" + echo -e "${RED}Failed modules: ${FAILED_MODULE_NAMES[*]}${NO_COLOR}" fi - + echo "" - + if [[ $FAILED_MODULES -eq 0 ]]; then - echo -e "${GREEN}🎉 All integration tests passed!${NC}" + echo -e "${GREEN}🎉 All integration tests passed!${NO_COLOR}" return 0 else - echo -e "${RED}❌ $FAILED_MODULES integration test modules failed${NC}" + echo -e "${RED}❌ $FAILED_MODULES integration test modules failed${NO_COLOR}" return 1 fi } @@ -130,16 +130,16 @@ generate_summary() { # Main execution main() { log_info "=== Server Audit Integration Test Runner ===" - + # Validate environment - only check SSH key if explicitly provided if [[ -n "$SSH_KEY" ]] && [[ ! -f "$SSH_KEY" ]] && [[ "$RUN_SSH_TESTS" == "true" ]] && [[ -n "$TEST_SERVERS" ]]; then log_info "SSH key not found: $SSH_KEY" log_info "SSH-dependent tests will run in validation mode only" fi - + # Run all tests run_all_integration_tests - + # Generate and display summary generate_summary } @@ -160,16 +160,16 @@ ENVIRONMENT VARIABLES: EXAMPLES: # Run all tests (individual tests will use mock SSH keys by default) $0 - + # Run tests with specific servers TEST_SERVERS="server1,server2" $0 - + # Run with custom SSH key SSH_KEY=~/.ssh/custom_key $0 - + # Skip SSH-dependent tests RUN_SSH_TESTS=false $0 - + # Run with full configuration TEST_SERVERS="prod1,prod2" SSH_KEY=~/.ssh/prod_key $0 @@ -185,4 +185,4 @@ EOF fi # Run main function -main "$@" \ No newline at end of file +main "$@" diff --git a/server-audit/tests/run_tests.sh b/server-audit/tests/run_tests.sh index 4fee7ec..d15365d 100755 --- a/server-audit/tests/run_tests.sh +++ b/server-audit/tests/run_tests.sh @@ -16,18 +16,18 @@ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' -NC='\033[0m' # No Color +NO_COLOR='\033[0m' log_info() { - echo -e "${BLUE}[RUNNER]${NC} $*" + echo -e "${BLUE}[RUNNER]${NO_COLOR} $*" } log_success() { - echo -e "${GREEN}[SUCCESS]${NC} $*" + echo -e "${GREEN}[SUCCESS]${NO_COLOR} $*" } log_error() { - echo -e "${RED}[ERROR]${NC} $*" + echo -e "${RED}[ERROR]${NO_COLOR} $*" } # Main execution @@ -75,13 +75,13 @@ ENVIRONMENT VARIABLES: EXAMPLES: # Run all tests $0 - + # Run only unit tests (fast, no SSH needed) $0 --unit-only - + # Run with custom SSH key $0 --ssh-key ~/.ssh/production_key - + # Run everything including performance tests $0 --with-performance @@ -94,10 +94,10 @@ EOF ;; esac done - + log_info "Starting test suite for Tomcat/Java audit script" echo "========================================================" - + local overall_result=0 local unit_exit=0 local integration_exit=0 @@ -119,7 +119,7 @@ EOF if [[ "$run_integration" == "true" ]]; then log_info "Running integration tests..." - + if [[ ! -f "$ssh_key" ]]; then log_error "SSH key not found: $ssh_key" log_info "Skipping integration tests" @@ -129,7 +129,7 @@ EOF if [[ "$run_performance" == "true" ]]; then env_vars="$env_vars RUN_PERFORMANCE=true" fi - + set +e # Temporarily disable exit-on-error for test execution eval "$env_vars ${TEST_DIR}/run_integration_tests.sh" integration_exit=$? @@ -153,8 +153,8 @@ EOF log_info "Check test logs in: $TEST_DIR/" log_info "Run individual test scripts for more details" fi - + return $overall_result } -main "$@" \ No newline at end of file +main "$@" diff --git a/server-audit/tests/unit/test_helpers.sh b/server-audit/tests/unit/test_helpers.sh index d7f95e2..1f1c9b9 100755 --- a/server-audit/tests/unit/test_helpers.sh +++ b/server-audit/tests/unit/test_helpers.sh @@ -1,20 +1,15 @@ #!/usr/bin/env bash -# test_helpers.sh - Comprehensive tests for lib/helpers.sh -# Tests all helper functions including dry run headers/footers and server line processing - # test_helpers.sh - Comprehensive tests for lib/helpers.sh # Tests the display and server processing helper functions source "$(dirname "${BASH_SOURCE[0]}")/../../lib/utils.sh" source "$(dirname "${BASH_SOURCE[0]}")/../../lib/helpers.sh" -# Define constants that helpers.sh expects DRY_RUN_HEADER_CSV="=== DRY RUN OUTPUT - Sample CSV Format ===" DRY_RUN_HEADER_TABLE="=== DRY RUN OUTPUT - Sample Confluence Table Format ===" DRY_RUN_FOOTER="=== END DRY RUN OUTPUT ===" -# Test setup TEST_TEMP_DIR="" test_setup() { @@ -27,13 +22,12 @@ test_teardown() { fi } -# Test show_dry_run_headers function with CSV output test_show_dry_run_headers_csv() { log_info "[TEST] Testing show_dry_run_headers with CSV output" - + local output CSV_OUTPUT="true" output=$(show_dry_run_headers) - + if [[ "$output" == *"$DRY_RUN_HEADER_CSV"* ]]; then log_info "[PASS] show_dry_run_headers displays CSV header correctly" return 0 @@ -43,13 +37,12 @@ test_show_dry_run_headers_csv() { fi } -# Test show_dry_run_headers function with table output test_show_dry_run_headers_table() { log_info "[TEST] Testing show_dry_run_headers with table output" - + local output CSV_OUTPUT="false" output=$(show_dry_run_headers) - + if [[ "$output" == *"$DRY_RUN_HEADER_TABLE"* ]]; then log_info "[PASS] show_dry_run_headers displays table header correctly" return 0 @@ -59,14 +52,13 @@ test_show_dry_run_headers_table() { fi } -# Test show_dry_run_headers function with no CSV_OUTPUT set test_show_dry_run_headers_default() { log_info "[TEST] Testing show_dry_run_headers with default (table) output" - + local output unset CSV_OUTPUT output=$(show_dry_run_headers) - + if [[ "$output" == *"$DRY_RUN_HEADER_TABLE"* ]]; then log_info "[PASS] show_dry_run_headers defaults to table header correctly" return 0 @@ -76,13 +68,12 @@ test_show_dry_run_headers_default() { fi } -# Test show_dry_run_footer function test_show_dry_run_footer() { log_info "[TEST] Testing show_dry_run_footer function" - + local output output=$(show_dry_run_footer) - + local expected_elements=( "$DRY_RUN_FOOTER" "This is sample data" @@ -90,7 +81,7 @@ test_show_dry_run_footer() { "Server names with 'prod', 'staging', 'dev', 'legacy', or 'broken'" "generate different sample versions" ) - + local failed=0 for element in "${expected_elements[@]}"; do if [[ "$output" == *"$element"* ]]; then @@ -100,14 +91,13 @@ test_show_dry_run_footer() { ((failed++)) fi done - + return $failed } -# Test process_server_line function with valid server lines test_process_server_line_valid() { log_info "[TEST] Testing process_server_line with valid server lines" - + local valid_lines=( "server01.example.com" " web-server-01 " @@ -115,7 +105,7 @@ test_process_server_line_valid() { " localhost " "server.domain.co.uk" ) - + local expected_outputs=( "server01.example.com" "web-server-01" @@ -123,13 +113,13 @@ test_process_server_line_valid() { "localhost" "server.domain.co.uk" ) - + local failed=0 for i in "${!valid_lines[@]}"; do local output output=$(process_server_line "${valid_lines[$i]}") local result=$? - + if [[ $result -eq 0 && "$output" == "${expected_outputs[$i]}" ]]; then log_info "[PASS] Valid server line processed correctly: '${valid_lines[$i]}' -> '$output'" else @@ -137,14 +127,13 @@ test_process_server_line_valid() { ((failed++)) fi done - + return $failed } -# Test process_server_line function with invalid server lines test_process_server_line_invalid() { log_info "[TEST] Testing process_server_line with invalid server lines" - + local invalid_lines=( "" " " @@ -155,13 +144,13 @@ test_process_server_line_invalid() { "#server.example.com" " # commented server " ) - + local failed=0 for line in "${invalid_lines[@]}"; do local output output=$(process_server_line "$line") local result=$? - + if [[ $result -eq 1 ]]; then log_info "[PASS] Invalid server line correctly rejected: '$line'" else @@ -169,20 +158,18 @@ test_process_server_line_invalid() { ((failed++)) fi done - + return $failed } -# Test process_server_line function with mixed content test_process_server_line_mixed() { log_info "[TEST] Testing process_server_line with mixed valid/invalid content" - - # Test server with inline comment (should extract server part) + local mixed_lines=( "server.example.com # This server is production" " test-host # staging environment " ) - + # Note: The current implementation doesn't handle inline comments # It will return the full line including the comment local failed=0 @@ -190,7 +177,7 @@ test_process_server_line_mixed() { local output output=$(process_server_line "$line") local result=$? - + if [[ $result -eq 0 ]]; then log_info "[INFO] Mixed content line processed: '$line' -> '$output'" log_info "[PASS] Mixed content line accepted (implementation note: inline comments not stripped)" @@ -199,61 +186,56 @@ test_process_server_line_mixed() { ((failed++)) fi done - + return $failed } -# Test process_server_line function with edge cases test_process_server_line_edge_cases() { log_info "[TEST] Testing process_server_line with edge cases" - + local failed=0 - - # Test empty string + local output output=$(process_server_line "") local result=$? - + if [[ $result -eq 1 ]]; then log_info "[PASS] Empty string correctly rejected" else log_error "[FAIL] Empty string should be rejected" ((failed++)) fi - - # Test only whitespace + output=$(process_server_line " ") result=$? - + if [[ $result -eq 1 ]]; then log_info "[PASS] Whitespace-only string correctly rejected" else log_error "[FAIL] Whitespace-only string should be rejected" ((failed++)) fi - - # Test comment line + output=$(process_server_line "# comment") result=$? - + if [[ $result -eq 1 ]]; then log_info "[PASS] Comment line correctly rejected" else log_error "[FAIL] Comment line should be rejected" ((failed++)) fi - + return $failed } -# Run all tests run_helpers_tests() { echo "=== Running Helpers Tests ===" local tests_passed=0 local tests_failed=0 - + test_setup - + local test_functions=( test_show_dry_run_headers_csv test_show_dry_run_headers_table @@ -264,7 +246,7 @@ run_helpers_tests() { test_process_server_line_mixed test_process_server_line_edge_cases ) - + for test_func in "${test_functions[@]}"; do if $test_func; then ((tests_passed++)) @@ -273,18 +255,17 @@ run_helpers_tests() { fi echo "" done - + test_teardown - + log_info "=== Helpers Tests Summary ===" log_info "Passed: $tests_passed" log_info "Failed: $tests_failed" log_info "Total: $((tests_passed + tests_failed))" - + return $tests_failed } -# Run tests if script is executed directly if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then run_helpers_tests -fi \ No newline at end of file +fi diff --git a/server-audit/tests/unit_tests.sh b/server-audit/tests/unit_tests.sh index 27b290b..1e4d9cd 100755 --- a/server-audit/tests/unit_tests.sh +++ b/server-audit/tests/unit_tests.sh @@ -24,7 +24,7 @@ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' -NC='\033[0m' # No Color +NO_COLOR='\033[0m' # Override the original log functions to capture output original_log_info=log_info @@ -41,16 +41,16 @@ test_log_error() { } test_log_pass() { - echo -e "${GREEN}[PASS]${NC} $*" | tee -a "$TEST_LOG" + echo -e "${GREEN}[PASS]${NO_COLOR} $*" | tee -a "$TEST_LOG" } test_log_fail() { - echo -e "${RED}[FAIL]${NC} $*" | tee -a "$TEST_LOG" + echo -e "${RED}[FAIL]${NO_COLOR} $*" | tee -a "$TEST_LOG" ((FAILED_TESTS++)) } test_log_test() { - echo -e "${BLUE}[TEST]${NC} $*" | tee -a "$TEST_LOG" + echo -e "${BLUE}[TEST]${NO_COLOR} $*" | tee -a "$TEST_LOG" } # Test assertion functions @@ -58,7 +58,7 @@ assert_equals() { local expected="$1" local actual="$2" local test_name="$3" - + ((TOTAL_TESTS++)) if [[ "$expected" == "$actual" ]]; then test_log_pass "$test_name" @@ -72,7 +72,7 @@ assert_equals() { assert_success() { local test_command="$1" local test_name="$2" - + ((TOTAL_TESTS++)) if eval "$test_command" >/dev/null 2>&1; then test_log_pass "$test_name" @@ -86,7 +86,7 @@ assert_success() { assert_failure() { local test_command="$1" local test_name="$2" - + ((TOTAL_TESTS++)) if ! eval "$test_command" >/dev/null 2>&1; then test_log_pass "$test_name" @@ -100,21 +100,21 @@ assert_failure() { # Test validation functions test_validate_server_name() { test_log_test "Testing validate_server_name function" - + # Valid server names assert_success "validate_server_name 'example.com'" "Valid domain name" assert_success "validate_server_name 'server-01.example.com'" "Valid domain with hyphen" assert_success "validate_server_name 'web01'" "Valid hostname" assert_success "validate_server_name '192.168.1.1'" "Valid IP address" assert_success "validate_server_name 'my-server.flygtaxi.se'" "Valid company domain" - + # Invalid server names assert_failure "validate_server_name ''" "Empty server name" assert_failure "validate_server_name 'server with spaces'" "Server name with spaces" assert_failure "validate_server_name 'server@invalid'" "Server name with @ symbol" assert_failure "validate_server_name '.invalid.com'" "Domain starting with dot" assert_failure "validate_server_name 'invalid..com'" "Domain with double dots" - + # Edge cases local long_name=$(printf 'a%.0s' {1..254}) assert_failure "validate_server_name '$long_name'" "Server name too long (254 chars)" @@ -122,32 +122,32 @@ test_validate_server_name() { test_validate_file_exists() { test_log_test "Testing validate_file_exists function" - + # Create temporary test files local temp_file=$(mktemp) local nonexistent_file="/tmp/nonexistent_file_$$" - + # Valid file assert_success "validate_file_exists '$temp_file' 'test'" "Existing readable file" - + # Nonexistent file assert_failure "validate_file_exists '$nonexistent_file' 'test'" "Nonexistent file" - + # Cleanup rm -f "$temp_file" } test_init_temp_file() { test_log_test "Testing init_temp_file function" - + # Test temp file creation local old_tmp_results="$TMP_RESULTS" TMP_RESULTS="" - + if init_temp_file; then if [[ -f "$TMP_RESULTS" ]]; then test_log_pass "Temp file created successfully" - + # Check permissions local perms=$(stat -f "%OLp" "$TMP_RESULTS" 2>/dev/null || stat -c "%a" "$TMP_RESULTS" 2>/dev/null) if [[ "$perms" == "600" ]]; then @@ -155,14 +155,14 @@ test_init_temp_file() { else test_log_fail "Temp file has incorrect permissions ($perms)" fi - + # Check header content if grep -q "|| Server || Tomcat Version || Java Version || Status ||" "$TMP_RESULTS"; then test_log_pass "Temp file has correct header" else test_log_fail "Temp file missing correct header" fi - + # Cleanup rm -f "$TMP_RESULTS" else @@ -171,7 +171,7 @@ test_init_temp_file() { else test_log_fail "init_temp_file function failed" fi - + TMP_RESULTS="$old_tmp_results" ((TOTAL_TESTS += 3)) } @@ -179,7 +179,7 @@ test_init_temp_file() { # Test logging functions test_logging_functions() { test_log_test "Testing logging functions" - + # Test log_info local output output=$(log_info "Test message" 2>&1) @@ -188,7 +188,7 @@ test_logging_functions() { else test_log_fail "log_info function output incorrect: '$output'" fi - + # Test log_error output=$(log_error "Error message" 2>&1) if [[ "$output" == "[ERROR] Error message" ]]; then @@ -196,7 +196,7 @@ test_logging_functions() { else test_log_fail "log_error function output incorrect: '$output'" fi - + # Test log_debug with DEBUG=0 DEBUG=0 output=$(log_debug "Debug message" 2>&1) @@ -205,7 +205,7 @@ test_logging_functions() { else test_log_fail "log_debug should be disabled when DEBUG=0" fi - + # Test log_debug with DEBUG=1 DEBUG=1 output=$(log_debug "Debug message" 2>&1) @@ -214,7 +214,7 @@ test_logging_functions() { else test_log_fail "log_debug function output incorrect when DEBUG=1: '$output'" fi - + # Reset DEBUG unset DEBUG ((TOTAL_TESTS += 4)) @@ -223,7 +223,7 @@ test_logging_functions() { # Test command construction (from server_check module) test_command_construction() { test_log_test "Testing secure command construction" - + # Test array-based command construction (multiple paths and instances) local search_patterns=("/usr/local/tomcat-*/bin/version.sh" "/usr/share/tomcat-*/bin/version.sh" "/opt/tomcat-*/bin/version.sh") local fallback_patterns=("/usr/local/tomcat/bin/version.sh" "/usr/share/tomcat/bin/version.sh" "/opt/tomcat/bin/version.sh") @@ -231,7 +231,7 @@ test_command_construction() { local tomcat_cmd printf -v tomcat_cmd 'tomcat_versions=""; for pattern in %s; do for tomcat_bin in $pattern; do if [ -f "$tomcat_bin" ]; then version=$("$tomcat_bin" 2>/dev/null | grep "Server number" | awk "{print \$3}"); if [ -n "$version" ]; then instance=$(basename "$(dirname "$(dirname "$tomcat_bin")")"); if [ -n "$tomcat_versions" ]; then tomcat_versions="$tomcat_versions, "; fi; tomcat_versions="$tomcat_versions$instance($version)"; fi; fi; done; done; echo "$tomcat_versions"' \ "${all_patterns[*]}" - + # Test that command contains expected patterns (multiple paths) if [[ "$tomcat_cmd" == *"tomcat_versions"* && "$tomcat_cmd" == *"/usr/local/tomcat"* && "$tomcat_cmd" == *"/usr/share/tomcat"* && "$tomcat_cmd" == *"/opt/tomcat"* ]]; then test_log_pass "Tomcat command construction (multiple paths) is secure" @@ -239,14 +239,14 @@ test_command_construction() { test_log_fail "Tomcat command construction failed" echo "Got command: $tomcat_cmd" >> "$TEST_LOG" fi - + # Test Java command construction local java_version_cmd="java -version" local java_cmd printf -v java_cmd '%s 2>&1 | head -n 1 | awk -F "\"" "{print \$2}"' "$java_version_cmd" - + local expected_java='java -version 2>&1 | head -n 1 | awk -F "\"" "{print \$2}"' - + if [[ "$java_cmd" == "$expected_java" ]]; then test_log_pass "Java command construction is secure" else @@ -254,7 +254,7 @@ test_command_construction() { echo "Expected: $expected_java" >> "$TEST_LOG" echo "Got: $java_cmd" >> "$TEST_LOG" fi - + ((TOTAL_TESTS += 2)) } @@ -274,7 +274,7 @@ generate_report() { test_log_info "Total tests: $TOTAL_TESTS" test_log_info "Failed tests: $FAILED_TESTS" test_log_info "Passed tests: $((TOTAL_TESTS - FAILED_TESTS))" - + if [[ $FAILED_TESTS -eq 0 ]]; then test_log_pass "All unit tests passed! ✅" echo "" @@ -291,7 +291,7 @@ generate_report() { # Main test execution main() { initialize_tests - + # Run all unit tests test_validate_server_name test_validate_file_exists @@ -304,28 +304,28 @@ main() { test_ssh_options test_csv_validation test_dry_run_output - + generate_report } # Test helper functions from helpers.sh test_helper_functions() { test_log_test "Testing helper functions from helpers.sh" - + # Test process_server_line function local result result=$(process_server_line " server1.example.com " 2>/dev/null || echo "") assert_equals "server1.example.com" "$result" "process_server_line: trim whitespace" - + result=$(process_server_line "# commented server" 2>/dev/null || echo "SKIP") assert_equals "SKIP" "$result" "process_server_line: skip comments" - + result=$(process_server_line "" 2>/dev/null || echo "SKIP") assert_equals "SKIP" "$result" "process_server_line: skip empty lines" - + result=$(process_server_line " # indented comment " 2>/dev/null || echo "SKIP") assert_equals "SKIP" "$result" "process_server_line: skip indented comments" - + result=$(process_server_line "valid-server.com" 2>/dev/null || echo "") assert_equals "valid-server.com" "$result" "process_server_line: valid server name" } @@ -333,7 +333,7 @@ test_helper_functions() { # Test CSV and production mode argument parsing test_argument_parsing() { test_log_test "Testing argument parsing for new flags" - + # Create a minimal test script to test argument parsing cat > /tmp/test_args.sh << 'EOF' #!/usr/bin/env bash @@ -362,9 +362,9 @@ echo "PRODUCTION_MODE=$PRODUCTION_MODE" echo "MAX_CONCURRENT=$MAX_CONCURRENT" echo "CONNECT_TIMEOUT=$CONNECT_TIMEOUT" EOF - + chmod +x /tmp/test_args.sh - + # Test CSV flag local output output=$(/tmp/test_args.sh --csv) @@ -375,7 +375,7 @@ EOF test_log_fail "CSV flag parsing - Expected CSV_OUTPUT=true" ((TOTAL_TESTS++)) fi - + # Test production mode flag output=$(/tmp/test_args.sh --production) if echo "$output" | grep -q "PRODUCTION_MODE=true" && echo "$output" | grep -q "MAX_CONCURRENT=3"; then @@ -385,7 +385,7 @@ EOF test_log_fail "Production mode flag parsing - Expected PRODUCTION_MODE=true and MAX_CONCURRENT=3" ((TOTAL_TESTS++)) fi - + # Test without-row-header flag output=$(/tmp/test_args.sh --without-row-header) if echo "$output" | grep -q "WITHOUT_ROW_HEADER=true"; then @@ -395,25 +395,25 @@ EOF test_log_fail "Without-row-header flag parsing - Expected WITHOUT_ROW_HEADER=true" ((TOTAL_TESTS++)) fi - + rm -f /tmp/test_args.sh } # Test DRY constants and format patterns test_dry_constants() { test_log_test "Testing DRY constants and format patterns" - + # Source main script to get constants source "${SCRIPT_DIR}/server-audit" --help >/dev/null 2>&1 || true - + # Test that constants are defined (these should be available from the main script) local test_csv_header="=== DRY RUN OUTPUT - Sample CSV Format ===" local test_table_header="=== DRY RUN OUTPUT - Sample Confluence Table Format ===" - + # Test AWK format patterns local test_csv_format='%s,%s,%s,%s\n' local test_table_format='| %s | %s | %s | %s |\n' - + if [[ "$test_csv_format" == '%s,%s,%s,%s\n' ]]; then test_log_pass "CSV format pattern constant" ((TOTAL_TESTS++)) @@ -421,7 +421,7 @@ test_dry_constants() { test_log_fail "CSV format pattern constant" ((TOTAL_TESTS++)) fi - + if [[ "$test_table_format" == '| %s | %s | %s | %s |\n' ]]; then test_log_pass "Table format pattern constant" ((TOTAL_TESTS++)) @@ -434,11 +434,11 @@ test_dry_constants() { # Test SSH options pattern consistency test_ssh_options() { test_log_test "Testing SSH options pattern consistency" - + local base_opts="-o StrictHostKeyChecking=yes -o BatchMode=yes -o PasswordAuthentication=no" local timeout="30" local full_opts="${base_opts} -o ConnectTimeout=${timeout}" - + # Test that base options contain required security settings if echo "$base_opts" | grep -q "StrictHostKeyChecking=yes" && \ echo "$base_opts" | grep -q "BatchMode=yes" && \ @@ -449,7 +449,7 @@ test_ssh_options() { test_log_fail "SSH base options security settings" ((TOTAL_TESTS++)) fi - + # Test timeout concatenation if echo "$full_opts" | grep -q "ConnectTimeout=${timeout}"; then test_log_pass "SSH timeout option concatenation" @@ -463,12 +463,12 @@ test_ssh_options() { # Test CSV output validation test_csv_validation() { test_log_test "Testing CSV output validation logic" - + # Test validation that WITHOUT_ROW_HEADER requires CSV_OUTPUT # Simulate the validation logic local CSV_OUTPUT=false local WITHOUT_ROW_HEADER=true - + if [[ "$WITHOUT_ROW_HEADER" == "true" && "$CSV_OUTPUT" != "true" ]]; then test_log_pass "CSV validation: without-row-header requires csv" ((TOTAL_TESTS++)) @@ -476,11 +476,11 @@ test_csv_validation() { test_log_fail "CSV validation: without-row-header requires csv" ((TOTAL_TESTS++)) fi - + # Test valid combination CSV_OUTPUT=true WITHOUT_ROW_HEADER=true - + if [[ "$WITHOUT_ROW_HEADER" == "true" && "$CSV_OUTPUT" == "true" ]]; then test_log_pass "CSV validation: valid combination" ((TOTAL_TESTS++)) @@ -493,12 +493,12 @@ test_csv_validation() { # Test dry run header functions test_dry_run_output() { test_log_test "Testing dry run output functions" - + # Test show_dry_run_headers function CSV_OUTPUT=false local output output=$(show_dry_run_headers 2>/dev/null) - + if echo "$output" | grep -q "Sample Confluence Table Format"; then test_log_pass "Dry run headers: table format" ((TOTAL_TESTS++)) @@ -506,10 +506,10 @@ test_dry_run_output() { test_log_fail "Dry run headers: table format" ((TOTAL_TESTS++)) fi - + CSV_OUTPUT=true output=$(show_dry_run_headers 2>/dev/null) - + if echo "$output" | grep -q "Sample CSV Format"; then test_log_pass "Dry run headers: CSV format" ((TOTAL_TESTS++)) @@ -517,10 +517,10 @@ test_dry_run_output() { test_log_fail "Dry run headers: CSV format" ((TOTAL_TESTS++)) fi - + # Test show_dry_run_footer function output=$(show_dry_run_footer 2>/dev/null) - + if echo "$output" | grep -q "END DRY RUN OUTPUT" && echo "$output" | grep -q "sample data"; then test_log_pass "Dry run footer content" ((TOTAL_TESTS++)) @@ -554,4 +554,4 @@ EOF fi # Run main function -main "$@" \ No newline at end of file +main "$@" diff --git a/tests/lib/test_logging.sh b/tests/lib/test_logging.sh new file mode 100644 index 0000000..25da50e --- /dev/null +++ b/tests/lib/test_logging.sh @@ -0,0 +1,265 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +source "${SCRIPT_DIR}/../../lib/logging.sh" + +test_setup() { + TEST_TEMP_DIR=$(mktemp -d) +} + +test_teardown() { + if [[ -n "$TEST_TEMP_DIR" && -d "$TEST_TEMP_DIR" ]]; then + rm -rf "$TEST_TEMP_DIR" + fi +} + +test_toggle_debug_mode() { + log_info "[TEST] Testing toggle_debug_mode function" + local output + export DEBUG=true + output=$(toggle_debug_mode 2>&1) + + if [[ "$output" == *"Debug mode enabled"* && "$output" == *"Script directory: $SCRIPT_DIR"* ]]; then + log_info "[PASS] toggle_debug_mode outputs debug information when DEBUG=true" + unset DEBUG + return 0 + else + log_error "[FAIL] toggle_debug_mode did not output expected debug information: '$output'" + unset DEBUG + return 1 + fi +} + +test_toggle_production_mode() { + log_info "[TEST] Testing toggle_production_mode function" + local output + export PRODUCTION_MODE=true + export DEBUG=true + output=$(toggle_production_mode 2>&1) + + if [[ "$output" == *"[PRODUCTION] Production mode enabled"* && "$output" == *"Script directory:"* ]]; then + log_info "[PASS] toggle_production_mode outputs production mode information when PRODUCTION_MODE=true and DEBUG=true" + unset PRODUCTION_MODE + unset DEBUG + return 0 + else + log_error "[FAIL] toggle_production_mode did not output expected production mode information: '$output'" + unset PRODUCTION_MODE + unset DEBUG + return 1 + fi +} + +test_debug_echo() { + log_info "[TEST] Testing debug_echo with DEBUG=true" + + local output + export DEBUG=true + output=$(debug_echo "[DEBUG] This is a debug message" 2>&1) + + if [[ "$output" == *"[DEBUG] This is a debug message"* ]]; then + log_info "[PASS] debug_echo outputs message when DEBUG=true" + unset DEBUG + return 0 + else + log_error "[FAIL] debug_echo did not output message when DEBUG=true: '$output'" + unset DEBUG + return 1 + fi +} + +test_debug_error() { + log_info "[TEST] Testing debug_error output" + + local output + output=$(debug_error "This is an error message" 2>&1) + + if [[ "$output" == *"[ERROR] This is an error message"* ]]; then + log_info "[PASS] debug_error outputs error message correctly" + return 0 + else + log_error "[FAIL] debug_error did not output message correctly: '$output'" + return 1 + fi +} + +test_info_echo() { + log_info "[TEST] Testing info_echo output" + + local output + output=$(info_echo "This is an info message") + + if [[ "$output" == *"[INFO] This is an info message"* ]]; then + log_info "[PASS] info_echo outputs info message correctly" + return 0 + else + log_error "[FAIL] info_echo did not output message correctly: '$output'" + return 1 + fi +} + +test_production_echo() { + log_info "[TEST] Testing production_echo with PRODUCTION_MODE=true" + + local output + export PRODUCTION_MODE=true + output=$(production_echo "This is a production message") + + if [[ "$output" == *"[PRODUCTION] This is a production message"* ]]; then + log_info "[PASS] production_echo outputs message when PRODUCTION_MODE=true" + unset PRODUCTION_MODE + return 0 + else + log_error "[FAIL] production_echo did not output message when PRODUCTION_MODE=true: '$output'" + unset PRODUCTION_MODE + return 1 + fi +} + +test_production_debug() { + log_info "[TEST] Testing production_debug with PRODUCTION_MODE=true and DEBUG=true" + + local output + export PRODUCTION_MODE=true + export DEBUG=true + output=$(production_debug "This is a production debug message" 2>&1) + + if [[ "$output" == *"[PROD DEBUG] This is a production debug message"* ]]; then + log_info "[PASS] production_debug outputs message when both PRODUCTION_MODE and DEBUG are true" + unset PRODUCTION_MODE + unset DEBUG + return 0 + else + log_error "[FAIL] production_debug did not output message when both PRODUCTION_MODE and DEBUG are true: '$output'" + unset PRODUCTION_MODE + unset DEBUG + return 1 + fi +} + +test_production_error() { + log_info "[TEST] Testing production_error output" + + local output + output=$(production_error "This is a production error message" 2>&1) + + if [[ "$output" == *"[PROD ERROR] This is a production error message"* ]]; then + log_info "[PASS] production_error outputs error message correctly" + return 0 + else + log_error "[FAIL] production_error did not output message correctly: '$output'" + return 1 + fi +} + +test_log_info() { + log_info "[TEST] Testing log_info output" + + local output + output=$(log_info "This is a log info message") + + local stripped_output + stripped_output=$(echo "$output" | sed 's/\x1b\[[0-9;]*m//g') + + if [[ "$stripped_output" == *"[INFO] This is a log info message"* ]]; then + log_info "[PASS] log_info outputs info message correctly" + return 0 + else + log_error "[FAIL] log_info did not output message correctly: '$stripped_output'" + return 1 + fi +} + +test_log_success() { + log_info "[TEST] Testing log_success output" + + local output + output=$(log_success "This is a log success message") + + local stripped_output + stripped_output=$(echo "$output" | sed 's/\x1b\[[0-9;]*m//g') + + if [[ "$stripped_output" == *"[INFO] This is a log success message"* ]]; then + log_info "[PASS] log_success outputs success message correctly" + return 0 + else + log_error "[FAIL] log_success did not output message correctly: '$stripped_output'" + return 1 + fi +} + +test_log_error() { + log_info "[TEST] Testing log_error output" + + local output + output=$(log_error "This is a log error message") + + local stripped_output + stripped_output=$(echo "$output" | sed 's/\x1b\[[0-9;]*m//g') + + if [[ "$stripped_output" == *"[ERROR] This is a log error message"* ]]; then + log_info "[PASS] log_error outputs error message correctly" + return 0 + else + log_error "[FAIL] log_error did not output message correctly: '$stripped_output'" + return 1 + fi +} + +run_logging_tests() { + log_info "=== Running Logging Tests ===" + local tests_passed=0 + local tests_failed=0 + + test_setup + + local test_functions=( + test_debug_echo + test_debug_error + test_info_echo + test_production_echo + test_production_debug + test_production_error + test_log_info + test_log_success + test_log_error + test_toggle_debug_mode + test_toggle_production_mode + ) + + for test_func in "${test_functions[@]}"; do + $test_func + local test_result=$? + + if [[ $test_result -eq 0 ]]; then + ((tests_passed++)) + else + ((tests_failed++)) + fi + echo "" + done + + test_teardown + + # Clean up any exported environment variables from tests + unset DEBUG + unset PRODUCTION_MODE + export -n DEBUG 2>/dev/null || true + export -n PRODUCTION_MODE 2>/dev/null || true + + log_info "=== Logging Tests Summary ===" + log_info "Passed: $tests_passed" + log_info "Failed: $tests_failed" + log_info "Total: $((tests_passed + tests_failed))" + + if [[ $tests_failed -eq 0 ]]; then + return 0 + else + return 1 + fi +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + run_logging_tests +fi diff --git a/server-audit/tests/watch.sh b/tests/watch.sh similarity index 79% rename from server-audit/tests/watch.sh rename to tests/watch.sh index 0e0e943..490702f 100755 --- a/server-audit/tests/watch.sh +++ b/tests/watch.sh @@ -6,70 +6,67 @@ set -eo pipefail # Changed: removed 'u' to allow test failures, keep 'e' for critical errors SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../lib/logging.sh" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CALLING_DIR="$(pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" # Configuration -WATCH_PATHS=( - "$PROJECT_ROOT/lib" - "$PROJECT_ROOT/tests/unit" - "$PROJECT_ROOT/tests/integration" -) +TEST_COMMAND="${1:-unit}" # Default to unit tests, or pass 'all', 'integration', 'shared-lib' + +# Set watch paths based on test command +if [[ "$TEST_COMMAND" == "shared-lib" ]]; then + WATCH_PATHS=( + "$PROJECT_ROOT/lib" + "$PROJECT_ROOT/tests/lib" + ) +else + WATCH_PATHS=( + "$CALLING_DIR/lib" + "$CALLING_DIR/tests/unit" + "$CALLING_DIR/tests/integration" + ) +fi WATCH_PATTERN="*.sh" -TEST_COMMAND="${1:-unit}" # Default to unit tests, or pass 'all', 'integration' - -# Colors for output -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -log_info() { - echo -e "${BLUE}[WATCH]${NC} $*" -} - -log_success() { - echo -e "${GREEN}[WATCH]${NC} $*" -} - -log_error() { - echo -e "${RED}[WATCH]${NC} $*" -} run_tests() { clear - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo -e "${YELLOW}Running tests...${NC} $(date '+%H:%M:%S')" - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NO_COLOR}" + echo -e "${YELLOW}Running tests...${NO_COLOR} $(date '+%H:%M:%S')" + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NO_COLOR}" echo "" # Run tests and capture exit code without exiting the script local exit_code=0 case "$TEST_COMMAND" in unit|u) - "$SCRIPT_DIR/run_unit_tests.sh" || exit_code=$? + "$CALLING_DIR/tests/run_unit_tests.sh" || exit_code=$? ;; integration|i) - "$SCRIPT_DIR/run_integration_tests.sh" || exit_code=$? + "$CALLING_DIR/tests/run_integration_tests.sh" || exit_code=$? ;; all|a) - "$SCRIPT_DIR/run_tests.sh" || exit_code=$? + "$CALLING_DIR/tests/run_tests.sh" || exit_code=$? + ;; + shared-lib|s) + cd "$PROJECT_ROOT" && make test-shared-lib || exit_code=$? ;; *) log_error "Unknown test command: $TEST_COMMAND" - log_info "Usage: $0 [unit|integration|all]" + log_info "Usage: $0 [unit|integration|all|shared-lib]" exit 1 ;; esac echo "" - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NO_COLOR}" if [[ $exit_code -eq 0 ]]; then log_success "Tests passed ✓ - Watching for changes..." else log_error "Tests failed ✗ - Watching for changes..." fi - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NO_COLOR}" echo "" # Always return 0 so the watch loop continues