diff --git a/CHANGELOG.md b/CHANGELOG.md index 643373b..91e16a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [0.4.0] - 2026-03-15 + +### Added +- **`scaffold unittest` subcommand** — new `devopsos scaffold unittest` command that generates + unit testing configuration files and sample test stubs for multiple tech stacks: + - **Python** → `pytest.ini` (with pytest-cov options), `conftest.py` with shared fixtures, + `tests/__init__.py`, and `tests/test_sample.py` with parametrized examples. + - **JavaScript** → Jest (`jest.config.js`), Vitest (`vitest.config.js`), or Mocha (`.mocharc.js`) + configuration plus a matching `tests/sample.test.js`. + - **TypeScript** → same as JavaScript with TypeScript-specific Jest transform (`ts-jest`) + and `tests/sample.test.ts`. + - **Go** → table-driven `_test.go` sample and `Makefile.test` with `test`, `test-race`, + `test-cov`, and `lint` targets. + - Coverage configuration is included by default and can be disabled with `--no-coverage`. + - Supports comma-separated `--languages` to scaffold multiple stacks at once. +- **`generate_unittest_config` MCP tool** — exposes the new unit-test scaffold as an MCP tool + so AI assistants can generate test configurations via conversation. +- **`cli/scaffold_unittest.py`** — the scaffold library backing the new command. +- **`docs/CLI-COMMANDS-REFERENCE.md`** — new section documenting all `unittest` options, + output files, and examples. + +--- + ## [0.3.0] - 2026-03-10 ### Fixed diff --git a/README.md b/README.md index c1da7b8..ef0c9ec 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![CI](https://github.com/cloudengine-labs/devops_os/actions/workflows/ci.yml/badge.svg)](https://github.com/cloudengine-labs/devops_os/actions/workflows/ci.yml) [![Sanity Tests](https://github.com/cloudengine-labs/devops_os/actions/workflows/sanity.yml/badge.svg)](https://github.com/cloudengine-labs/devops_os/actions/workflows/sanity.yml) -[![Version](https://img.shields.io/badge/version-0.2.0-blue)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-0.4.0-blue)](CHANGELOG.md) [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue?logo=python&logoColor=white)](https://www.python.org/) [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE) [![Open Source](https://img.shields.io/badge/open%20source-%E2%9D%A4-red)](https://github.com/cloudengine-labs/devops_os) @@ -29,6 +29,7 @@ DevOps-OS is an open-source DevOps automation platform that scaffolds production | 🚀 **CI/CD Generators** | One-command scaffolding for GitHub Actions, GitLab CI, and Jenkins pipelines | | ☸️ **GitOps Config Generator** | Kubernetes manifests, ArgoCD Applications, and Flux CD Kustomizations | | 📊 **SRE Config Generator** | Prometheus alert rules, Grafana dashboards, and SLO manifests | +| 🧪 **Unit Test Scaffold** | Generate pytest, Jest, Vitest, Mocha, or Go test configs with one command | | 🤖 **MCP Server** | Plug DevOps-OS tools into Claude or ChatGPT as native AI skills | | 🛠️ **Dev Container** | Pre-configured multi-language environment (Python · Java · Go · JavaScript) | | 🔄 **Process-First** | Built-in education on the Process-First SDLC philosophy and how it maps to every DevOps-OS tool | @@ -174,6 +175,13 @@ python -m cli.devopsos scaffold sre --name my-app --team platform --slo-target 9 # Dev container configuration → .devcontainer/devcontainer.json + .devcontainer/devcontainer.env.json python -m cli.devopsos scaffold devcontainer --languages python,go --cicd-tools docker,terraform --kubernetes-tools k9s,flux +# Unit test configs + sample stubs (Python, JS, Go, TypeScript) +python -m cli.devopsos scaffold unittest --name my-app --languages python +python -m cli.devopsos scaffold unittest --name my-app --languages python,javascript,go + +# Combined GitHub Actions + Jenkins in one step +python -m cli.devopsos scaffold cicd --name my-app --type build --languages python --github --jenkins + # Kubernetes manifests python kubernetes/k8s-config-generator.py --name my-app --image ghcr.io/myorg/my-app:v1 ``` @@ -218,7 +226,7 @@ git clone https://github.com/cloudengine-labs/devops_os.git && cd devops_os pip install -r cli/requirements.txt # ── Check version ────────────────────────────────────────────────────────── -python -m cli.devopsos --version # → devopsos version 0.2.0 +python -m cli.devopsos --version # → devopsos version 0.4.0 # ── Interactive project wizard ───────────────────────────────────────────── python -m cli.devopsos init # guided setup for any project @@ -247,6 +255,10 @@ python -m cli.devopsos scaffold devcontainer --languages python,go --cicd-tools # ── Combined CI/CD (GHA + Jenkins in one step) ──────────────────────────── python -m cli.devopsos scaffold cicd --name my-app --type build --languages python --github --jenkins +# ── Unit Tests ───────────────────────────────────────────────────────────── +python -m cli.devopsos scaffold unittest --name my-app --languages python +python -m cli.devopsos scaffold unittest --name my-app --languages python,javascript,go + # ── Process-First philosophy ─────────────────────────────────────────────── python -m cli.devopsos process-first # full overview python -m cli.devopsos process-first --section mapping # which tool for which goal @@ -268,7 +280,7 @@ python -m cli.devopsos scaffold gha --help devops_os/ ├── .devcontainer/ # Dev container config (Dockerfile, devcontainer.json, setup scripts) ├── .github/workflows/ # CI, Sanity Tests, and GitHub Pages workflows -├── cli/ # CLI scaffold tools (scaffold_gha, gitlab, jenkins, argocd, sre, devopsos) +├── cli/ # CLI scaffold tools (scaffold_gha, gitlab, jenkins, argocd, sre, unittest, devopsos) ├── kubernetes/ # Kubernetes manifest generator ├── mcp_server/ # MCP server for AI assistant integration (Claude, ChatGPT) ├── skills/ # Claude & OpenAI tool/function definitions @@ -291,7 +303,7 @@ pip install -r cli/requirements.txt -r mcp_server/requirements.txt pytest pytest python -m pytest cli/test_cli.py mcp_server/test_server.py tests/test_comprehensive.py -v ``` -**Latest results:** ✅ 162 passed · ⚠️ 3 xfailed (known tracked bugs) · ❌ 0 failed +**Latest results:** ✅ 260 passed · ⚠️ 2 xfailed (known tracked bugs) · ❌ 0 failed | Report | Description | |--------|-------------| @@ -346,7 +358,7 @@ You can also customize `.devcontainer/devcontainer.env.json` directly to enable |-------|-------------| | [🚀 Getting Started](docs/GETTING-STARTED.md) | Easy step-by-step guide — **start here** | | [📖 CLI Commands Reference](docs/CLI-COMMANDS-REFERENCE.md) | **Complete reference** — every option, input file, and output location | -| [🖥️ CLI Test Report](docs/CLI-TEST-REPORT.md) | v0.2.0 CLI revamp test results — 52 tests, all passing | +| [🖥️ CLI Test Report](docs/CLI-TEST-REPORT.md) | v0.4.0 CLI test results — 62 tests, all passing | | [🔄 Process-First Philosophy](docs/PROCESS-FIRST.md) | What Process-First means, how it maps to DevOps-OS, and AI learning tips | | [📦 Dev Container Setup](docs/DEVOPS-OS-README.md) | Set up and customize the dev container | | [⚡ Quick Start Reference](docs/DEVOPS-OS-QUICKSTART.md) | Essential CLI commands for all features | @@ -355,6 +367,7 @@ You can also customize `.devcontainer/devcontainer.env.json` directly to enable | [🔧 Jenkins Pipeline Generator](docs/JENKINS-PIPELINE-README.md) | Generate and customize Jenkins pipelines | | [🔄 ArgoCD / Flux GitOps](docs/ARGOCD-README.md) | Generate ArgoCD Applications and Flux Kustomizations | | [📊 SRE Configuration](docs/SRE-CONFIGURATION-README.md) | Prometheus rules, Grafana dashboards, SLO manifests | +| [🧪 Unit Test Scaffold](docs/CLI-COMMANDS-REFERENCE.md#devopsos-scaffold-unittest--unit-test-scaffold-generator) | Generate pytest, Jest, Vitest, Mocha, or Go test configs | | [☸️ Kubernetes Deployments](docs/KUBERNETES-DEPLOYMENT-README.md) | Generate and manage Kubernetes deployment configs | | [🤖 MCP Server](mcp_server/README.md) | Connect DevOps-OS tools to Claude or ChatGPT | | [🧠 AI Skills](skills/README.md) | Use DevOps-OS with the Anthropic API or OpenAI function calling | diff --git a/cli/__version__.py b/cli/__version__.py index 77c731d..3effe6c 100644 --- a/cli/__version__.py +++ b/cli/__version__.py @@ -1,3 +1,3 @@ """Single source of truth for the devopsos package version.""" -__version__ = "0.3.0" +__version__ = "0.4.0" diff --git a/cli/devopsos.py b/cli/devopsos.py index 4651851..beba9f1 100644 --- a/cli/devopsos.py +++ b/cli/devopsos.py @@ -15,6 +15,7 @@ import cli.scaffold_argocd as scaffold_argocd import cli.scaffold_sre as scaffold_sre import cli.scaffold_devcontainer as scaffold_devcontainer +import cli.scaffold_unittest as scaffold_unittest import cli.process_first as process_first from cli import __version__ @@ -559,6 +560,74 @@ def scaffold_cicd_cmd( flags += ["--custom-values", custom_values] _run_scaffold(scaffold_cicd.main, flags) + +# ── scaffold unittest ──────────────────────────────────────────────────────── + +@scaffold_app.command("unittest") +def scaffold_unittest_cmd( + ctx: typer.Context, + name: str = typer.Option("my-app", envvar="DEVOPS_OS_UNITTEST_NAME", + help="Project / application name"), + languages: str = typer.Option("python", envvar="DEVOPS_OS_UNITTEST_LANGUAGES", + help=( + "Comma-separated languages to generate tests for: " + "python, javascript, typescript, go" + )), + framework: str = typer.Option("", envvar="DEVOPS_OS_UNITTEST_FRAMEWORK", + help=( + "Testing framework override (auto-selected by default). " + "JS/TS: jest | mocha | vitest. Python: pytest. Go: go-test." + )), + coverage: bool = typer.Option(True, envvar="DEVOPS_OS_UNITTEST_COVERAGE", + help="Include coverage configuration (default: true)"), + output_dir: str = typer.Option("unittest", "--output-dir", + envvar="DEVOPS_OS_UNITTEST_OUTPUT_DIR", + help="Root output directory for generated files"), +): + """Generate unit testing configuration and sample test files. + + \b + Supported languages and their default frameworks: + python → pytest + pytest-cov + javascript → Jest (override with --framework mocha | vitest) + typescript → Jest (override with --framework mocha | vitest) + go → go test + + \b + Output files (default: unittest/ directory): + unittest/pytest.ini Python pytest configuration + unittest/conftest.py Python shared fixtures + unittest/tests/__init__.py Python test-package marker + unittest/tests/test_sample.py Python sample unit tests + unittest/jest.config.js JavaScript/TypeScript Jest config + unittest/vitest.config.js JavaScript/TypeScript Vitest config + unittest/.mocharc.js JavaScript/TypeScript Mocha config + unittest/tests/sample.test.js JavaScript/TypeScript sample tests + unittest/_test.go Go sample unit test file + unittest/Makefile.test Go Makefile test targets + + \b + Examples: + devopsos scaffold unittest --name my-app --languages python + devopsos scaffold unittest --name my-app --languages javascript --framework jest + devopsos scaffold unittest --name my-app --languages typescript --framework vitest + devopsos scaffold unittest --name my-api --languages go + devopsos scaffold unittest --name my-app --languages python,javascript,go + """ + _show_help_if_no_opts(ctx) + flags = [ + "--name", name, + "--languages", languages, + "--output-dir", output_dir, + ] + if framework: + flags += ["--framework", framework] + if not coverage: + # coverage defaults to True; only pass flag when False + flags.append("--no-coverage") + _run_scaffold(scaffold_unittest.main, flags) + + @app.command() def init( directory: str = typer.Option(".", "--dir", help="Target directory in which the .devcontainer folder will be created (defaults to the current directory)"), diff --git a/cli/scaffold_unittest.py b/cli/scaffold_unittest.py new file mode 100644 index 0000000..1359099 --- /dev/null +++ b/cli/scaffold_unittest.py @@ -0,0 +1,609 @@ +#!/usr/bin/env python3 +""" +DevOps-OS Unit Test Scaffold Generator + +Generates unit testing configuration and sample test files for multiple +tech stacks: + + Python : pytest + pytest-cov (pytest.ini, conftest.py, sample test) + JavaScript/TypeScript: + jest : jest.config.js + sample test + mocha : .mocharc.js + sample test + vitest : vitest.config.js + sample test + Go : go test convention (Makefile snippet + sample *_test.go) + +Output layout (default: / in the current directory): + / + ├── pytest.ini # Python — pytest configuration + ├── conftest.py # Python — shared fixtures + ├── tests/__init__.py # Python — test-package marker + ├── tests/test_sample.py # Python — sample unit tests + ├── jest.config.js # JS/TS (jest) — Jest configuration + ├── vitest.config.js # JS/TS (vitest) — Vitest configuration + ├── .mocharc.js # JS/TS (mocha) — Mocha configuration + ├── tests/sample.test.js # JS/TS — sample unit tests + ├── Makefile.test # Go — test Makefile targets + └── sample_test.go # Go — sample unit test file +""" + +import os +import sys +import argparse +from pathlib import Path + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +ENV_PREFIX = "DEVOPS_OS_UNITTEST_" + +SUPPORTED_LANGUAGES = ["python", "javascript", "typescript", "go"] +JS_FRAMEWORKS = ["jest", "mocha", "vitest"] +FRAMEWORK_DEFAULTS = { + "python": "pytest", + "javascript": "jest", + "typescript": "jest", + "go": "go-test", +} + + +# --------------------------------------------------------------------------- +# Argument parsing +# --------------------------------------------------------------------------- + +def parse_arguments(): + """Parse command-line arguments with environment variable fallbacks.""" + parser = argparse.ArgumentParser( + description="Generate unit testing configuration and sample test files for DevOps-OS" + ) + parser.add_argument( + "--name", + default=os.environ.get(f"{ENV_PREFIX}NAME", "my-app"), + help="Project / application name (used in generated file content)", + ) + parser.add_argument( + "--languages", + default=os.environ.get(f"{ENV_PREFIX}LANGUAGES", "python"), + help=( + "Comma-separated list of languages to generate tests for: " + "python, javascript, typescript, go" + ), + ) + parser.add_argument( + "--framework", + default=os.environ.get(f"{ENV_PREFIX}FRAMEWORK", ""), + help=( + "Testing framework override (auto-selected per language by default). " + "For JavaScript/TypeScript: jest | mocha | vitest. " + "For Python: pytest. For Go: go-test." + ), + ) + parser.add_argument( + "--coverage", + dest="coverage", + action="store_true", + default=os.environ.get(f"{ENV_PREFIX}COVERAGE", "true").lower() + in ("true", "1", "yes"), + help="Include coverage configuration (default: true)", + ) + parser.add_argument( + "--no-coverage", + dest="coverage", + action="store_false", + help="Disable coverage configuration", + ) + parser.add_argument( + "--output-dir", + default=os.environ.get(f"{ENV_PREFIX}OUTPUT_DIR", "unittest"), + help="Root output directory for generated files (default: unittest/)", + ) + return parser.parse_args() + + +# --------------------------------------------------------------------------- +# File-writing helpers +# --------------------------------------------------------------------------- + +def _write(path: Path, content: str) -> Path: + """Write *content* to *path*, creating intermediate directories.""" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) + return path + + +# --------------------------------------------------------------------------- +# Python / pytest generators +# --------------------------------------------------------------------------- + +def generate_pytest_ini(name: str, coverage: bool) -> str: + """Return the content of a pytest.ini file.""" + cov_opts = ( + f"\naddopts =\n" + f" --cov={name.replace('-', '_')}\n" + f" --cov-report=term-missing\n" + f" --cov-report=xml:coverage.xml\n" + f" --cov-report=html:htmlcov\n" + ) if coverage else "" + return ( + "[pytest]\n" + f"# pytest configuration for {name}\n" + "testpaths = tests\n" + "python_files = test_*.py *_test.py\n" + "python_classes = Test*\n" + "python_functions = test_*\n" + f"{cov_opts}" + ) + + +def generate_conftest_py(name: str) -> str: + """Return the content of a conftest.py file with sample fixtures.""" + module = name.replace("-", "_") + return ( + f'"""Shared pytest fixtures for {name}."""\n' + "\n" + "import pytest\n" + "\n" + "\n" + "@pytest.fixture\n" + "def sample_config():\n" + ' """Return a minimal application configuration dict."""\n' + " return {\n" + f' "name": "{name}",\n' + ' "version": "0.1.0",\n' + ' "debug": False,\n' + " }\n" + "\n" + "\n" + "@pytest.fixture\n" + f"def {module}_client(sample_config):\n" + f' """Return a lightweight {name} client stub."""\n' + " class _Stub:\n" + " def __init__(self, cfg):\n" + " self.config = cfg\n" + "\n" + " def ping(self):\n" + " return True\n" + "\n" + f" return _Stub(sample_config)\n" + ) + + +def generate_python_test_sample(name: str) -> str: + """Return a sample Python unit test file.""" + module = name.replace("-", "_") + return ( + f'"""Sample unit tests for {name}.\n' + "\n" + "Replace these stubs with real tests for your application logic.\n" + '"""\n' + "\n" + "import pytest\n" + "\n" + "\n" + f"class Test{module.title().replace('_', '')}Core:\n" + f' """Core unit tests for {name}."""\n' + "\n" + " def test_addition(self):\n" + ' """Trivial sanity check — always passes."""\n' + " assert 1 + 1 == 2\n" + "\n" + " def test_string_format(self):\n" + ' """Verify basic string formatting."""\n' + f' result = f"Hello from {name}"\n' + f' assert "Hello" in result\n' + "\n" + " def test_list_operations(self):\n" + ' """Verify list membership."""\n' + " items = [1, 2, 3, 4, 5]\n" + " assert 3 in items\n" + " assert len(items) == 5\n" + "\n" + "\n" + "class TestEdgeCases:\n" + ' """Edge-case tests."""\n' + "\n" + " def test_empty_string(self):\n" + " assert len(\"\") == 0\n" + "\n" + " def test_none_check(self):\n" + " value = None\n" + " assert value is None\n" + "\n" + " @pytest.mark.parametrize(\n" + " \"a, b, expected\",\n" + " [\n" + " (1, 2, 3),\n" + " (0, 0, 0),\n" + " (-1, 1, 0),\n" + " ],\n" + " )\n" + " def test_parametrized_add(self, a, b, expected):\n" + " assert a + b == expected\n" + "\n" + "\n" + "def test_fixture_ping(sample_config):\n" + f' """Verify the sample_config fixture is populated."""\n' + f' assert sample_config["name"] == "{name}"\n' + ) + + +def generate_python_tests_init() -> str: + return '"""Test package for unit tests."""\n' + + +# --------------------------------------------------------------------------- +# JavaScript / TypeScript generators +# --------------------------------------------------------------------------- + +def generate_jest_config(name: str, is_typescript: bool, coverage: bool) -> str: + """Return the content of a jest.config.js file.""" + transform = ( + " transform: {\n" + " '^.+\\\\.(ts|tsx)$': 'ts-jest',\n" + " },\n" + " moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],\n" + ) if is_typescript else "" + + cov_block = "" + if coverage: + cov_block = ( + " collectCoverage: true,\n" + " coverageDirectory: 'coverage',\n" + " coverageReporters: ['text', 'lcov', 'html'],\n" + " collectCoverageFrom: [\n" + " 'src/**/*.{js,ts}',\n" + " '!src/**/*.d.ts',\n" + " '!src/index.{js,ts}',\n" + " ],\n" + " coverageThreshold: {\n" + " global: {\n" + " branches: 70,\n" + " functions: 70,\n" + " lines: 70,\n" + " statements: 70,\n" + " },\n" + " },\n" + ) + + return ( + f"// Jest configuration for {name}\n" + "/** @type {import('jest').Config} */\n" + "module.exports = {\n" + " testEnvironment: 'node',\n" + " testMatch: [\n" + " '**/__tests__/**/*.[jt]s?(x)',\n" + " '**/?(*.)+(spec|test).[jt]s?(x)',\n" + " ],\n" + f"{transform}" + f"{cov_block}" + " verbose: true,\n" + " clearMocks: true,\n" + "};\n" + ) + + +def generate_vitest_config(name: str, coverage: bool) -> str: + """Return the content of a vitest.config.js file.""" + cov_block = "" + if coverage: + cov_block = ( + " coverage: {\n" + " provider: 'v8',\n" + " reporter: ['text', 'lcov', 'html'],\n" + " include: ['src/**/*.{js,ts}'],\n" + " exclude: ['src/**/*.d.ts', 'node_modules'],\n" + " thresholds: { lines: 70, functions: 70, branches: 70, statements: 70 },\n" + " },\n" + ) + + return ( + f"// Vitest configuration for {name}\n" + "import { defineConfig } from 'vitest/config';\n" + "\n" + "export default defineConfig({\n" + " test: {\n" + " environment: 'node',\n" + " include: ['**/*.{test,spec}.{js,ts}'],\n" + " globals: true,\n" + f"{cov_block}" + " },\n" + "});\n" + ) + + +def generate_mocha_rc(name: str, coverage: bool) -> str: + """Return the content of a .mocharc.js file.""" + nyc_comment = ( + "// Run with: nyc mocha (install nyc for coverage)\n" if coverage else "" + ) + return ( + f"// Mocha configuration for {name}\n" + f"{nyc_comment}" + "module.exports = {\n" + " spec: 'tests/**/*.test.{js,mjs}',\n" + " timeout: 5000,\n" + " reporter: 'spec',\n" + " recursive: true,\n" + "};\n" + ) + + +def generate_js_test_sample(name: str, framework: str, is_typescript: bool) -> str: + """Return a sample JavaScript/TypeScript unit test file.""" + ext = "ts" if is_typescript else "js" + import_style = ( + "import { describe, it, expect, beforeEach } from 'vitest';" + if framework == "vitest" + else ( + "const { expect } = require('chai');" + if framework == "mocha" + else "// Jest globals are injected automatically" + ) + ) + + if framework == "mocha": + return ( + f"// Sample Mocha + Chai unit tests for {name}\n" + f"{import_style}\n" + "\n" + f"describe('{name} core', () => {{\n" + " it('should perform basic arithmetic', () => {\n" + " expect(1 + 1).to.equal(2);\n" + " });\n" + "\n" + " it('should handle string operations', () => {\n" + f" const result = `Hello from {name}`;\n" + " expect(result).to.include('Hello');\n" + " });\n" + "\n" + " it('should work with arrays', () => {\n" + " const items = [1, 2, 3];\n" + " expect(items).to.have.lengthOf(3);\n" + " expect(items).to.include(2);\n" + " });\n" + "});\n" + "\n" + f"describe('{name} edge cases', () => {{\n" + " it('should handle empty values', () => {\n" + " expect('').to.equal('');\n" + " expect(null).to.be.null;\n" + " });\n" + "});\n" + ) + + # jest / vitest share the same expect API + return ( + f"// Sample {framework.capitalize()} unit tests for {name}\n" + f"{import_style}\n" + "\n" + f"describe('{name} core', () => {{\n" + " it('should perform basic arithmetic', () => {\n" + " expect(1 + 1).toBe(2);\n" + " });\n" + "\n" + " it('should handle string operations', () => {\n" + f" const result = `Hello from {name}`;\n" + " expect(result).toContain('Hello');\n" + " });\n" + "\n" + " it('should work with arrays', () => {\n" + " const items = [1, 2, 3];\n" + " expect(items).toHaveLength(3);\n" + " expect(items).toContain(2);\n" + " });\n" + "});\n" + "\n" + f"describe('{name} edge cases', () => {{\n" + " it('should handle null and undefined', () => {\n" + " expect(null).toBeNull();\n" + " expect(undefined).toBeUndefined();\n" + " });\n" + "\n" + " it.each([\n" + " [1, 2, 3],\n" + " [0, 0, 0],\n" + " [-1, 1, 0],\n" + " ])('adds %i + %i to equal %i', (a, b, expected) => {\n" + " expect(a + b).toBe(expected);\n" + " });\n" + "});\n" + ) + + +# --------------------------------------------------------------------------- +# Go generators +# --------------------------------------------------------------------------- + +def generate_go_test_sample(name: str) -> str: + """Return a sample Go unit test file.""" + pkg = name.replace("-", "_").lower() + return ( + f"// Package {pkg} provides unit tests for {name}.\n" + f"package {pkg}_test\n" + "\n" + 'import (\n' + '\t"testing"\n' + ")\n" + "\n" + f"// TestAdd verifies basic arithmetic — replace with real application tests.\n" + "func TestAdd(t *testing.T) {\n" + "\tresult := 1 + 1\n" + "\tif result != 2 {\n" + '\t\tt.Errorf("expected 2, got %d", result)\n' + "\t}\n" + "}\n" + "\n" + "func TestStringContains(t *testing.T) {\n" + f'\tgreeting := "Hello from {name}"\n' + '\tif len(greeting) == 0 {\n' + '\t\tt.Error("expected non-empty greeting")\n' + "\t}\n" + "}\n" + "\n" + "func TestTableDriven(t *testing.T) {\n" + "\tcases := []struct {\n" + "\t\ta, b, want int\n" + "\t}{\n" + "\t\t{1, 2, 3},\n" + "\t\t{0, 0, 0},\n" + "\t\t{-1, 1, 0},\n" + "\t}\n" + "\tfor _, tc := range cases {\n" + "\t\tgot := tc.a + tc.b\n" + "\t\tif got != tc.want {\n" + '\t\t\tt.Errorf("add(%d, %d) = %d; want %d", tc.a, tc.b, got, tc.want)\n' + "\t\t}\n" + "\t}\n" + "}\n" + ) + + +def generate_go_makefile(name: str, coverage: bool) -> str: + """Return Makefile test-target content for a Go project.""" + pkg = name.replace("-", "_").lower() + cov_target = "" + if coverage: + cov_target = ( + "\n" + "# Generate HTML coverage report\n" + "test-coverage: test\n" + f"\tgo tool cover -html=coverage_{pkg}.out -o coverage_{pkg}.html\n" + f"\t@echo \"Coverage report: coverage_{pkg}.html\"\n" + ) + + return ( + f"# Makefile test targets for {name}\n" + "# Include these targets in your project Makefile\n" + "\n" + ".PHONY: test test-verbose test-race lint\n" + "\n" + "# Run all tests\n" + "test:\n" + f"\tgo test -v -count=1 ./...\n" + "\n" + "# Run tests with race-condition detector\n" + "test-race:\n" + f"\tgo test -race -v ./...\n" + "\n" + + ( + "# Run tests with coverage\n" + "test-cov:\n" + f"\tgo test -v -coverprofile=coverage_{pkg}.out ./...\n" + f"\tgo tool cover -func=coverage_{pkg}.out\n" + if coverage else "" + ) + + cov_target + + "\n" + "# Run go vet and staticcheck\n" + "lint:\n" + "\tgo vet ./...\n" + ) + + +# --------------------------------------------------------------------------- +# Top-level generator +# --------------------------------------------------------------------------- + +def generate_unittest_scaffold( + name: str, + languages: str, + framework: str, + coverage: bool, + output_dir: str, +) -> list: + """Generate all unit-testing files for the requested languages. + + Returns a list of ``(Path, description)`` tuples for the written files. + """ + out = Path(output_dir) + written = [] + + lang_list = [l.strip().lower() for l in languages.split(",") if l.strip()] + + for lang in lang_list: + if lang not in SUPPORTED_LANGUAGES: + print(f"Warning: unsupported language '{lang}' — skipping", file=sys.stderr) + continue + + # Resolve framework for this language + fw = framework.strip().lower() if framework.strip() else FRAMEWORK_DEFAULTS.get(lang, "") + + if lang == "python": + _write(out / "pytest.ini", generate_pytest_ini(name, coverage)) + written.append((out / "pytest.ini", "pytest configuration")) + + _write(out / "conftest.py", generate_conftest_py(name)) + written.append((out / "conftest.py", "shared pytest fixtures")) + + _write(out / "tests" / "__init__.py", generate_python_tests_init()) + written.append((out / "tests" / "__init__.py", "test package marker")) + + _write(out / "tests" / "test_sample.py", generate_python_test_sample(name)) + written.append((out / "tests" / "test_sample.py", "sample Python unit tests")) + + elif lang in ("javascript", "typescript"): + is_ts = lang == "typescript" + if fw not in JS_FRAMEWORKS: + fw = "jest" # default JS framework + + if fw == "jest": + _write(out / "jest.config.js", generate_jest_config(name, is_ts, coverage)) + written.append((out / "jest.config.js", "Jest configuration")) + + elif fw == "vitest": + _write(out / "vitest.config.js", generate_vitest_config(name, coverage)) + written.append((out / "vitest.config.js", "Vitest configuration")) + + elif fw == "mocha": + _write(out / ".mocharc.js", generate_mocha_rc(name, coverage)) + written.append((out / ".mocharc.js", "Mocha configuration")) + + ext = "ts" if is_ts else "js" + test_file = out / "tests" / f"sample.test.{ext}" + _write(test_file, generate_js_test_sample(name, fw, is_ts)) + written.append((test_file, f"sample {lang} unit tests ({fw})")) + + elif lang == "go": + pkg = name.replace("-", "_").lower() + test_file = out / f"{pkg}_test.go" + _write(test_file, generate_go_test_sample(name)) + written.append((test_file, "sample Go unit tests")) + + _write(out / "Makefile.test", generate_go_makefile(name, coverage)) + written.append((out / "Makefile.test", "Go Makefile test targets")) + + return written + + +# --------------------------------------------------------------------------- +# main() +# --------------------------------------------------------------------------- + +def main(): + """CLI entry point.""" + args = parse_arguments() + + written = generate_unittest_scaffold( + name=args.name, + languages=args.languages, + framework=args.framework, + coverage=args.coverage, + output_dir=args.output_dir, + ) + + if not written: + print("No files generated. Check --languages value.", file=sys.stderr) + sys.exit(1) + + print(f"Unit test scaffold generated in: {args.output_dir}/") + for path, desc in written: + print(f" {path} ({desc})") + print() + print("Languages:", args.languages) + if args.framework: + print("Framework override:", args.framework) + print("Coverage enabled:", args.coverage) + + +if __name__ == "__main__": + main() diff --git a/cli/test_cli.py b/cli/test_cli.py index 5ee4f25..48884e0 100644 --- a/cli/test_cli.py +++ b/cli/test_cli.py @@ -194,6 +194,7 @@ def test_scaffold_help_lists_new_targets(): assert "gitlab" in result.stdout assert "argocd" in result.stdout assert "sre" in result.stdout + assert "unittest" in result.stdout def test_scaffold_gha_via_cli(): """Regression: `python -m cli.devopsos scaffold gha` must not raise argparse error.""" @@ -596,11 +597,11 @@ def test_process_first_specific_section_no_usage_footer(): # -- scaffold arg pass-through (cli.devopsos vs cli.scaffold_*) ------------ def test_scaffold_help_shows_all_subcommands(): - """`devopsos scaffold --help` lists all 7 scaffold subcommands.""" + """`devopsos scaffold --help` lists all 8 scaffold subcommands.""" result = _run(["-m", "cli.devopsos", "scaffold", "--help"]) assert result.returncode == 0 text = _strip_ansi(result.stdout) - for target in ("gha", "jenkins", "gitlab", "argocd", "sre", "devcontainer", "cicd"): + for target in ("gha", "jenkins", "gitlab", "argocd", "sre", "devcontainer", "cicd", "unittest"): assert target in text, f"scaffold --help should list the '{target}' subcommand" @@ -759,11 +760,219 @@ def test_scaffold_cicd_jenkins_only(): ) +# ── scaffold unittest (new in v0.4.0) ──────────────────────────────────────── + +def test_scaffold_unittest_help_shows_native_options(): + """`devopsos scaffold unittest --help` shows all unittest-specific options.""" + result = _run(["-m", "cli.devopsos", "scaffold", "unittest", "--help"]) + assert result.returncode == 0 + text = _strip_ansi(result.stdout) + for option in ("--name", "--languages", "--framework", "--coverage", "--output-dir"): + assert option in text, f"scaffold unittest --help should show '{option}'" + # Supported languages / frameworks must be mentioned in the help text + assert "python" in text.lower() + assert "javascript" in text.lower() + assert "typescript" in text.lower() + assert "go" in text.lower() + assert "jest" in text.lower() + + +def test_scaffold_unittest_via_cli_python(): + """`devopsos scaffold unittest --languages python` generates pytest.ini and conftest.py.""" + with tempfile.TemporaryDirectory() as tmp: + result = subprocess.run( + [ + sys.executable, "-m", "cli.devopsos", + "scaffold", "unittest", + "--name", "my-api", + "--languages", "python", + "--output-dir", tmp, + ], + capture_output=True, text=True, + cwd=os.path.dirname(os.path.dirname(__file__)), + ) + assert result.returncode == 0, result.stderr + result.stdout + assert "error: unrecognized arguments" not in result.stderr + assert (Path(tmp) / "pytest.ini").is_file(), "pytest.ini should be created" + assert (Path(tmp) / "conftest.py").is_file(), "conftest.py should be created" + assert (Path(tmp) / "tests" / "test_sample.py").is_file(), "test_sample.py should be created" + assert (Path(tmp) / "tests" / "__init__.py").is_file(), "tests/__init__.py should be created" + # pytest.ini must reference the project name + ini_content = (Path(tmp) / "pytest.ini").read_text() + assert "testpaths" in ini_content + assert "--cov=" in ini_content # coverage enabled by default + + +def test_scaffold_unittest_via_cli_javascript_jest(): + """`devopsos scaffold unittest --languages javascript --framework jest` generates jest.config.js.""" + with tempfile.TemporaryDirectory() as tmp: + result = subprocess.run( + [ + sys.executable, "-m", "cli.devopsos", + "scaffold", "unittest", + "--name", "my-lib", + "--languages", "javascript", + "--framework", "jest", + "--output-dir", tmp, + ], + capture_output=True, text=True, + cwd=os.path.dirname(os.path.dirname(__file__)), + ) + assert result.returncode == 0, result.stderr + result.stdout + assert (Path(tmp) / "jest.config.js").is_file(), "jest.config.js should be created" + assert (Path(tmp) / "tests" / "sample.test.js").is_file(), "sample.test.js should be created" + jest_content = (Path(tmp) / "jest.config.js").read_text() + assert "testEnvironment" in jest_content + assert "collectCoverage" in jest_content # coverage enabled by default + + +def test_scaffold_unittest_via_cli_typescript_vitest(): + """`devopsos scaffold unittest --languages typescript --framework vitest` creates vitest.config.js.""" + with tempfile.TemporaryDirectory() as tmp: + result = subprocess.run( + [ + sys.executable, "-m", "cli.devopsos", + "scaffold", "unittest", + "--name", "my-ui", + "--languages", "typescript", + "--framework", "vitest", + "--output-dir", tmp, + ], + capture_output=True, text=True, + cwd=os.path.dirname(os.path.dirname(__file__)), + ) + assert result.returncode == 0, result.stderr + result.stdout + assert (Path(tmp) / "vitest.config.js").is_file(), "vitest.config.js should be created" + assert (Path(tmp) / "tests" / "sample.test.ts").is_file(), "sample.test.ts should be created" + vitest_content = (Path(tmp) / "vitest.config.js").read_text() + assert "defineConfig" in vitest_content + assert "vitest/config" in vitest_content + + +def test_scaffold_unittest_via_cli_javascript_mocha(): + """`devopsos scaffold unittest --languages javascript --framework mocha` generates .mocharc.js.""" + with tempfile.TemporaryDirectory() as tmp: + result = subprocess.run( + [ + sys.executable, "-m", "cli.devopsos", + "scaffold", "unittest", + "--name", "my-service", + "--languages", "javascript", + "--framework", "mocha", + "--output-dir", tmp, + ], + capture_output=True, text=True, + cwd=os.path.dirname(os.path.dirname(__file__)), + ) + assert result.returncode == 0, result.stderr + result.stdout + assert (Path(tmp) / ".mocharc.js").is_file(), ".mocharc.js should be created" + assert (Path(tmp) / "tests" / "sample.test.js").is_file(), "sample.test.js should be created" + mocha_content = (Path(tmp) / ".mocharc.js").read_text() + assert "spec" in mocha_content + assert "tests/**" in mocha_content + + +def test_scaffold_unittest_via_cli_go(): + """`devopsos scaffold unittest --languages go` generates a Go test file and Makefile.""" + with tempfile.TemporaryDirectory() as tmp: + result = subprocess.run( + [ + sys.executable, "-m", "cli.devopsos", + "scaffold", "unittest", + "--name", "my-server", + "--languages", "go", + "--output-dir", tmp, + ], + capture_output=True, text=True, + cwd=os.path.dirname(os.path.dirname(__file__)), + ) + assert result.returncode == 0, result.stderr + result.stdout + assert (Path(tmp) / "my_server_test.go").is_file(), "my_server_test.go should be created" + assert (Path(tmp) / "Makefile.test").is_file(), "Makefile.test should be created" + go_content = (Path(tmp) / "my_server_test.go").read_text() + assert '"testing"' in go_content + assert "TestTableDriven" in go_content + makefile_content = (Path(tmp) / "Makefile.test").read_text() + assert "go test" in makefile_content + assert "test-cov:" in makefile_content + + +def test_scaffold_unittest_via_cli_multi_language(): + """`devopsos scaffold unittest --languages python,javascript,go` generates files for all three.""" + with tempfile.TemporaryDirectory() as tmp: + result = subprocess.run( + [ + sys.executable, "-m", "cli.devopsos", + "scaffold", "unittest", + "--name", "full-stack", + "--languages", "python,javascript,go", + "--output-dir", tmp, + ], + capture_output=True, text=True, + cwd=os.path.dirname(os.path.dirname(__file__)), + ) + assert result.returncode == 0, result.stderr + result.stdout + # Python files + assert (Path(tmp) / "pytest.ini").is_file(), "pytest.ini should be created" + assert (Path(tmp) / "tests" / "test_sample.py").is_file(), "test_sample.py should be created" + # JavaScript files (default: jest) + assert (Path(tmp) / "jest.config.js").is_file(), "jest.config.js should be created" + # Go files + assert (Path(tmp) / "full_stack_test.go").is_file(), "full_stack_test.go should be created" + assert (Path(tmp) / "Makefile.test").is_file(), "Makefile.test should be created" + + +def test_scaffold_unittest_no_coverage_flag(): + """`--no-coverage` removes coverage configuration from generated files.""" + with tempfile.TemporaryDirectory() as tmp: + result = subprocess.run( + [ + sys.executable, "-m", "cli.devopsos", + "scaffold", "unittest", + "--name", "lean-app", + "--languages", "python", + "--no-coverage", + "--output-dir", tmp, + ], + capture_output=True, text=True, + cwd=os.path.dirname(os.path.dirname(__file__)), + ) + assert result.returncode == 0, result.stderr + result.stdout + ini_content = (Path(tmp) / "pytest.ini").read_text() + assert "--cov=" not in ini_content, "pytest.ini should not contain --cov= when --no-coverage is set" + + +def test_scaffold_unittest_via_cli_matches_direct_module_output(): + """Output from `devopsos scaffold unittest` equals `python -m cli.scaffold_unittest` (same defaults).""" + with tempfile.TemporaryDirectory() as tmp1, tempfile.TemporaryDirectory() as tmp2: + common_args = ["--name", "test-app", "--languages", "python"] + + result_unified = subprocess.run( + [sys.executable, "-m", "cli.devopsos", "scaffold", "unittest"] + + common_args + ["--output-dir", tmp1], + capture_output=True, text=True, + cwd=os.path.dirname(os.path.dirname(__file__)), + ) + result_direct = subprocess.run( + [sys.executable, "-m", "cli.scaffold_unittest"] + + common_args + ["--output-dir", tmp2], + capture_output=True, text=True, + cwd=os.path.dirname(os.path.dirname(__file__)), + ) + + assert result_unified.returncode == 0, result_unified.stderr + assert result_direct.returncode == 0, result_direct.stderr + # Both must produce identical pytest.ini content + assert (Path(tmp1) / "pytest.ini").read_text() == (Path(tmp2) / "pytest.ini").read_text(), ( + "Unified CLI and direct module should produce identical pytest.ini" + ) + + # ── graceful exit (no opts) ────────────────────────────────────────────────── def test_scaffold_no_opts_shows_help(): """Each scaffold subcommand shows usage help (not an error) when invoked with no options.""" - targets = ("gha", "jenkins", "gitlab", "argocd", "sre", "devcontainer", "cicd") + targets = ("gha", "jenkins", "gitlab", "argocd", "sre", "devcontainer", "cicd", "unittest") for target in targets: result = subprocess.run( [sys.executable, "-m", "cli.devopsos", "scaffold", target], diff --git a/docs/CLI-COMMANDS-REFERENCE.md b/docs/CLI-COMMANDS-REFERENCE.md index e60742a..469726c 100644 --- a/docs/CLI-COMMANDS-REFERENCE.md +++ b/docs/CLI-COMMANDS-REFERENCE.md @@ -18,6 +18,7 @@ All scaffold commands are available through the **unified `devopsos` CLI** — o - [devopsos scaffold sre — SRE Config Generator](#devopsos-scaffold-sre--sre-config-generator) - [devopsos scaffold devcontainer — Dev Container Generator](#devopsos-scaffold-devcontainer--dev-container-generator) - [devopsos scaffold cicd — Combined CI/CD Generator](#devopsos-scaffold-cicd--combined-cicd-generator) +- [devopsos scaffold unittest — Unit Test Scaffold Generator](#devopsos-scaffold-unittest--unit-test-scaffold-generator) - [devopsos init — Interactive Wizard](#devopsos-init--interactive-wizard) - [devopsos process-first — Process-First Philosophy](#devopsos-process-first--process-first-philosophy) - [Environment Variable Reference](#environment-variable-reference) @@ -56,6 +57,7 @@ python -m cli.devopsos scaffold gha --help # GHA-specific options | Flux CD | `python -m cli.devopsos scaffold argocd --method flux` | `flux/` directory | | SRE configs | `python -m cli.devopsos scaffold sre` | `sre/` directory | | Dev Container | `python -m cli.devopsos scaffold devcontainer` | `.devcontainer/` directory | +| Unit Tests | `python -m cli.devopsos scaffold unittest` | `unittest/` directory | | Interactive wizard | `python -m cli.devopsos init` | varies (see below) | | Process-First | `python -m cli.devopsos process-first` | stdout (educational content) | @@ -347,6 +349,77 @@ python -m cli.devopsos scaffold cicd --name my-app --jenkins --type build --- +## devopsos scaffold unittest — Unit Test Scaffold Generator + +Generates unit testing configuration files and sample test stubs for Python, JavaScript/TypeScript, and Go projects. + +### Invocation + +```bash +python -m cli.devopsos scaffold unittest [options] +``` + +### Supported Languages & Frameworks + +| Language | Default Framework | Override Options | +|----------|-------------------|-----------------| +| `python` | pytest + pytest-cov | _(no override)_ | +| `javascript` | Jest | `jest` \| `mocha` \| `vitest` | +| `typescript` | Jest | `jest` \| `mocha` \| `vitest` | +| `go` | go test | _(no override)_ | + +### Options + +| Option | Env var | Default | Description | +|--------|---------|---------|-------------| +| `--name NAME` | `DEVOPS_OS_UNITTEST_NAME` | `my-app` | Project / application name | +| `--languages LANGS` | `DEVOPS_OS_UNITTEST_LANGUAGES` | `python` | Comma-separated languages: `python`, `javascript`, `typescript`, `go` | +| `--framework FW` | `DEVOPS_OS_UNITTEST_FRAMEWORK` | _(auto)_ | Framework override for JS/TS: `jest` \| `mocha` \| `vitest` | +| `--coverage` / `--no-coverage` | `DEVOPS_OS_UNITTEST_COVERAGE` | `true` | Include or exclude coverage configuration | +| `--output-dir DIR` | `DEVOPS_OS_UNITTEST_OUTPUT_DIR` | `unittest` | Root output directory | + +### Output files + +| File | Language | Description | +|------|----------|-------------| +| `/pytest.ini` | Python | pytest configuration with test discovery and coverage settings | +| `/conftest.py` | Python | Shared pytest fixtures | +| `/tests/__init__.py` | Python | Test-package marker | +| `/tests/test_sample.py` | Python | Sample unit tests with parametrize example | +| `/jest.config.js` | JS/TS (jest) | Jest configuration with optional coverage thresholds | +| `/vitest.config.js` | JS/TS (vitest) | Vitest configuration with optional coverage | +| `/.mocharc.js` | JS/TS (mocha) | Mocha configuration | +| `/tests/sample.test.{js,ts}` | JS/TS | Sample unit tests for the chosen framework | +| `/_test.go` | Go | Sample unit tests (table-driven pattern) | +| `/Makefile.test` | Go | Makefile targets: `test`, `test-race`, `test-cov`, `lint` | + +### Examples + +```bash +# Python project +python -m cli.devopsos scaffold unittest --name my-api --languages python + +# JavaScript with Jest +python -m cli.devopsos scaffold unittest --name my-app --languages javascript --framework jest + +# TypeScript with Vitest +python -m cli.devopsos scaffold unittest --name my-app --languages typescript --framework vitest + +# JavaScript with Mocha +python -m cli.devopsos scaffold unittest --name my-app --languages javascript --framework mocha + +# Go project +python -m cli.devopsos scaffold unittest --name my-service --languages go + +# Full-stack project (all three) +python -m cli.devopsos scaffold unittest --name my-platform --languages python,javascript,go + +# Disable coverage config +python -m cli.devopsos scaffold unittest --name my-app --languages python --no-coverage +``` + +--- + ## devopsos init — Interactive Wizard Prompts you to select languages, CI/CD tools, Kubernetes tools, build tools, code analysis tools, and DevOps tools. Then writes a dev container config. @@ -408,6 +481,7 @@ Every flag for every command has a corresponding environment variable. The prefi | `scaffold argocd` | `DEVOPS_OS_ARGOCD_` | `DEVOPS_OS_ARGOCD_AUTO_SYNC=true` | | `scaffold sre` | `DEVOPS_OS_SRE_` | `DEVOPS_OS_SRE_SLO_TARGET=99.5` | | `scaffold devcontainer` | `DEVOPS_OS_DEVCONTAINER_` | `DEVOPS_OS_DEVCONTAINER_LANGUAGES=python,go` | +| `scaffold unittest` | `DEVOPS_OS_UNITTEST_` | `DEVOPS_OS_UNITTEST_LANGUAGES=python,go` | Environment variables are looked up at startup and used as default values when the corresponding flag is not supplied. Explicit flags always take precedence over environment variables. diff --git a/docs/DEVOPS-OS-QUICKSTART.md b/docs/DEVOPS-OS-QUICKSTART.md index 01c9e71..6e30b3f 100644 --- a/docs/DEVOPS-OS-QUICKSTART.md +++ b/docs/DEVOPS-OS-QUICKSTART.md @@ -13,6 +13,7 @@ This guide provides the essential CLI commands for using all functionalities of - [GitOps / ArgoCD & Flux CD](#gitops--argocd--flux-cd) - [SRE Configuration](#sre-configuration) - [Container Configuration](#container-configuration) +- [Unit Test Scaffold](#unit-test-scaffold) - [Common Options for All Generators](#common-options-for-all-generators) - [Troubleshooting](#troubleshooting) @@ -365,19 +366,84 @@ cat > .devcontainer/devcontainer.env.json << EOF EOF ``` +## Unit Test Scaffold + +Generate ready-to-use unit test configuration files and sample test stubs for Python, JavaScript/TypeScript, and Go. + +### Python (pytest) + +```bash +# Generate pytest.ini, conftest.py, and a sample test file +python -m cli.devopsos scaffold unittest --name my-api --languages python + +# Without coverage configuration +python -m cli.devopsos scaffold unittest --name my-api --languages python --no-coverage +``` + +**Output:** `pytest.ini`, `conftest.py`, `tests/__init__.py`, `tests/test_sample.py` + +### JavaScript / TypeScript + +```bash +# JavaScript with Jest (default) +python -m cli.devopsos scaffold unittest --name my-app --languages javascript --framework jest + +# JavaScript with Mocha +python -m cli.devopsos scaffold unittest --name my-app --languages javascript --framework mocha + +# TypeScript with Vitest +python -m cli.devopsos scaffold unittest --name my-app --languages typescript --framework vitest +``` + +**Output (Jest):** `jest.config.js`, `tests/sample.test.js` +**Output (Vitest):** `vitest.config.js`, `tests/sample.test.ts` +**Output (Mocha):** `.mocharc.js`, `tests/sample.test.js` + +### Go + +```bash +# Generate a table-driven Go test file + Makefile +python -m cli.devopsos scaffold unittest --name my-service --languages go +``` + +**Output:** `my_service_test.go`, `Makefile.test` + +### Multi-stack (all three in one command) + +```bash +python -m cli.devopsos scaffold unittest --name my-platform --languages python,javascript,go +``` + +### All options + +```bash +# Show all available options +python -m cli.devopsos scaffold unittest --help +``` + +| Option | Env var | Default | Description | +|--------|---------|---------|-------------| +| `--name NAME` | `DEVOPS_OS_UNITTEST_NAME` | `my-app` | Project name | +| `--languages LANGS` | `DEVOPS_OS_UNITTEST_LANGUAGES` | `python` | Comma-separated: `python`, `javascript`, `typescript`, `go` | +| `--framework FW` | `DEVOPS_OS_UNITTEST_FRAMEWORK` | _(auto)_ | JS/TS override: `jest` \| `mocha` \| `vitest` | +| `--coverage` / `--no-coverage` | `DEVOPS_OS_UNITTEST_COVERAGE` | `true` | Include or exclude coverage config | +| `--output-dir DIR` | `DEVOPS_OS_UNITTEST_OUTPUT_DIR` | `.` | Root output directory | + +--- + ## Common Options for All Generators -| Option | `scaffold gha` | `scaffold gitlab` | `scaffold jenkins` | `scaffold argocd` | `scaffold sre` | `scaffold devcontainer` | Description | -|--------|:-:|:-:|:-:|:-:|:-:|:-:|-------------| -| `--name` | ✓ | ✓ | ✓ | ✓ | ✓ | — | Name of the workflow/pipeline/app | -| `--type` | ✓ | ✓ | ✓ | — | — | — | Type of workflow/pipeline | -| `--languages` | ✓ | ✓ | ✓ | — | — | ✓ | Languages to enable | -| `--kubernetes` | ✓ | ✓ | ✓ | — | — | — | Include K8s deployment steps | -| `--k8s-method` | ✓ | ✓ | ✓ | — | — | — | K8s deployment method | -| `--output` | ✓ | ✓ | ✓ | — | — | — | Output file path | -| `--output-dir` | — | — | — | ✓ | ✓ | ✓ | Output directory | -| `--custom-values` | ✓ | ✓ | ✓ | — | — | — | Custom configuration JSON file | -| `--image` | ✓ | ✓ | ✓ | ✓ | — | — | Container image to use | +| Option | `scaffold gha` | `scaffold gitlab` | `scaffold jenkins` | `scaffold argocd` | `scaffold sre` | `scaffold devcontainer` | `scaffold unittest` | Description | +|--------|:-:|:-:|:-:|:-:|:-:|:-:|:-:|-------------| +| `--name` | ✓ | ✓ | ✓ | ✓ | ✓ | — | ✓ | Name of the workflow/pipeline/app | +| `--type` | ✓ | ✓ | ✓ | — | — | — | — | Type of workflow/pipeline | +| `--languages` | ✓ | ✓ | ✓ | — | — | ✓ | ✓ | Languages to enable | +| `--kubernetes` | ✓ | ✓ | ✓ | — | — | — | — | Include K8s deployment steps | +| `--k8s-method` | ✓ | ✓ | ✓ | — | — | — | — | K8s deployment method | +| `--output` | ✓ | ✓ | ✓ | — | — | — | — | Output file path | +| `--output-dir` | — | — | — | ✓ | ✓ | ✓ | ✓ | Output directory | +| `--custom-values` | ✓ | ✓ | ✓ | — | — | — | — | Custom configuration JSON file | +| `--image` | ✓ | ✓ | ✓ | ✓ | — | — | — | Container image to use | ## Troubleshooting @@ -389,6 +455,7 @@ python -m cli.devopsos scaffold jenkins --help python -m cli.devopsos scaffold argocd --help python -m cli.devopsos scaffold sre --help python -m cli.devopsos scaffold devcontainer --help +python -m cli.devopsos scaffold unittest --help # Verify generated output locations ls -la .github/workflows/ # GitHub Actions diff --git a/hugo-docs/content/docs/getting-started/_index.md b/hugo-docs/content/docs/getting-started/_index.md index 60bb2fc..a6b189f 100644 --- a/hugo-docs/content/docs/getting-started/_index.md +++ b/hugo-docs/content/docs/getting-started/_index.md @@ -20,6 +20,7 @@ DevOps-OS is a toolkit that generates production-ready CI/CD pipelines, Kubernet | GitOps / Deploy | ArgoCD, Flux CD, kubectl, Kustomize | | Containers | Docker, Helm | | SRE / Observability | Prometheus alert rules, Grafana dashboards, SLO configs | +| Unit Testing | pytest, Jest, Vitest, Mocha, Go test | | AI Integration | Claude (MCP Server), OpenAI (function calling) | --- @@ -141,7 +142,27 @@ python -m cli.devopsos scaffold sre --name my-app --team platform --- -## 6 — Interactive wizard (all-in-one) +## 6 — Generate unit test configs + +```bash +# Python — generates pytest.ini, conftest.py, and a sample test file +python -m cli.devopsos scaffold unittest --name my-app --languages python + +# JavaScript with Jest +python -m cli.devopsos scaffold unittest --name my-app --languages javascript --framework jest + +# TypeScript with Vitest +python -m cli.devopsos scaffold unittest --name my-app --languages typescript --framework vitest + +# Multi-stack (Python + JavaScript + Go) in one command +python -m cli.devopsos scaffold unittest --name my-platform --languages python,javascript,go +``` + +See [CLI Reference]({{< relref "/docs/reference" >}}) for all options and output file paths. + +--- + +## 7 — Interactive wizard (all-in-one) ```bash python -m cli.devopsos init # interactive project configurator @@ -150,11 +171,13 @@ python -m cli.devopsos scaffold gitlab # scaffold GitLab CI python -m cli.devopsos scaffold jenkins # scaffold Jenkins python -m cli.devopsos scaffold argocd # scaffold ArgoCD / Flux python -m cli.devopsos scaffold sre # scaffold SRE configs +python -m cli.devopsos scaffold cicd # scaffold GHA + Jenkins in one step +python -m cli.devopsos scaffold unittest # scaffold unit test configs ``` --- -## 7 — Use with an AI assistant +## 8 — Use with an AI assistant ```bash pip install -r mcp_server/requirements.txt diff --git a/hugo-docs/content/docs/reference/_index.md b/hugo-docs/content/docs/reference/_index.md index e1498ae..6278600 100644 --- a/hugo-docs/content/docs/reference/_index.md +++ b/hugo-docs/content/docs/reference/_index.md @@ -21,6 +21,8 @@ Complete reference for every DevOps-OS CLI command: options, default values, env | Flux CD | `python -m cli.devopsos scaffold argocd --method flux` | `flux/` directory | | SRE configs | `python -m cli.devopsos scaffold sre` | `sre/` directory | | Dev Container | `python -m cli.devopsos scaffold devcontainer` | `.devcontainer/` directory | +| Combined CI/CD | `python -m cli.devopsos scaffold cicd` | `.github/workflows/` + `Jenkinsfile` | +| Unit Tests | `python -m cli.devopsos scaffold unittest` | varies by language (see below) | | Interactive wizard | `python -m cli.devopsos init` | varies | | Process-First guide | `python -m cli.devopsos process-first` | stdout (educational content) | @@ -172,6 +174,28 @@ python -m cli.devopsos scaffold devcontainer [options] --- +## scaffold unittest — Unit Test Scaffold + +```bash +python -m cli.devopsos scaffold unittest [options] +``` + +| Option | Env var | Default | Description | +|--------|---------|---------|-------------| +| `--name NAME` | `DEVOPS_OS_UNITTEST_NAME` | `my-app` | Project name | +| `--languages LANGS` | `DEVOPS_OS_UNITTEST_LANGUAGES` | `python` | Comma-separated: `python`, `javascript`, `typescript`, `go` | +| `--framework FW` | `DEVOPS_OS_UNITTEST_FRAMEWORK` | _(auto)_ | JS/TS framework: `jest` \| `mocha` \| `vitest` | +| `--coverage` / `--no-coverage` | `DEVOPS_OS_UNITTEST_COVERAGE` | `true` | Include or exclude coverage config | +| `--output-dir DIR` | `DEVOPS_OS_UNITTEST_OUTPUT_DIR` | `.` | Root output directory | + +**Output (Python):** `pytest.ini`, `conftest.py`, `tests/__init__.py`, `tests/test_sample.py` +**Output (JS/TS Jest):** `jest.config.js`, `tests/sample.test.{js,ts}` +**Output (JS/TS Vitest):** `vitest.config.js`, `tests/sample.test.{js,ts}` +**Output (JS/TS Mocha):** `.mocharc.js`, `tests/sample.test.{js,ts}` +**Output (Go):** `_test.go`, `Makefile.test` + +--- + ## devopsos — Unified CLI ```bash @@ -193,6 +217,8 @@ python -m cli.devopsos scaffold jenkins # Jenkins python -m cli.devopsos scaffold argocd # ArgoCD / Flux python -m cli.devopsos scaffold sre # SRE configs python -m cli.devopsos scaffold devcontainer # Dev container +python -m cli.devopsos scaffold cicd # Combined GHA + Jenkins +python -m cli.devopsos scaffold unittest # Unit test configs ``` ### process-first command diff --git a/mcp_server/server.py b/mcp_server/server.py index 8ec8039..d297425 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -583,6 +583,78 @@ def generate_sre_configs( ) + +# --------------------------------------------------------------------------- +# Tool: generate_unittest_config +# --------------------------------------------------------------------------- + +@mcp.tool() +def generate_unittest_config( + name: str = "my-app", + languages: str = "python", + framework: str = "", + coverage: bool = True, +) -> str: + """ + Generate unit testing configuration and sample test files for the given tech stack. + + Supported languages and their default frameworks: + - python → pytest + pytest-cov + - javascript → Jest (override: jest | mocha | vitest) + - typescript → Jest (override: jest | mocha | vitest) + - go → go test + + Args: + name: Project / application name. + languages: Comma-separated languages (python, javascript, typescript, go). + framework: Testing framework override (empty = auto-select per language). + coverage: Include coverage configuration. + + Returns: + JSON string with file names as keys and generated file contents as values. + """ + from cli import scaffold_unittest + + result = {} + + lang_list = [l.strip().lower() for l in languages.split(",") if l.strip()] + fw = framework.strip().lower() if framework else "" + + for lang in lang_list: + if lang not in scaffold_unittest.SUPPORTED_LANGUAGES: + continue + + resolved_fw = fw if fw else scaffold_unittest.FRAMEWORK_DEFAULTS.get(lang, "") + is_ts = lang == "typescript" + + if lang == "python": + result["pytest.ini"] = scaffold_unittest.generate_pytest_ini(name, coverage) + result["conftest.py"] = scaffold_unittest.generate_conftest_py(name) + result["tests/__init__.py"] = scaffold_unittest.generate_python_tests_init() + result["tests/test_sample.py"] = scaffold_unittest.generate_python_test_sample(name) + + elif lang in ("javascript", "typescript"): + if resolved_fw not in scaffold_unittest.JS_FRAMEWORKS: + resolved_fw = "jest" + if resolved_fw == "jest": + result["jest.config.js"] = scaffold_unittest.generate_jest_config(name, is_ts, coverage) + elif resolved_fw == "vitest": + result["vitest.config.js"] = scaffold_unittest.generate_vitest_config(name, coverage) + elif resolved_fw == "mocha": + result[".mocharc.js"] = scaffold_unittest.generate_mocha_rc(name, coverage) + ext = "ts" if is_ts else "js" + result[f"tests/sample.test.{ext}"] = scaffold_unittest.generate_js_test_sample( + name, resolved_fw, is_ts + ) + + elif lang == "go": + pkg = name.replace("-", "_").lower() + result[f"{pkg}_test.go"] = scaffold_unittest.generate_go_test_sample(name) + result["Makefile.test"] = scaffold_unittest.generate_go_makefile(name, coverage) + + return json.dumps(result, indent=2) + + # --------------------------------------------------------------------------- # Entry point # --------------------------------------------------------------------------- diff --git a/tests/test_comprehensive.py b/tests/test_comprehensive.py index 2f08529..9f2ef05 100644 --- a/tests/test_comprehensive.py +++ b/tests/test_comprehensive.py @@ -24,6 +24,7 @@ scaffold_gitlab, scaffold_argocd, scaffold_sre, + scaffold_unittest, ) from mcp_server.server import ( generate_github_actions_workflow, @@ -33,6 +34,7 @@ generate_argocd_config, generate_sre_configs, scaffold_devcontainer, + generate_unittest_config, ) @@ -1498,3 +1500,342 @@ def test_claude_argocd_tool_has_method_enum(self): assert "enum" in method_prop assert "argocd" in method_prop["enum"] assert "flux" in method_prop["enum"] + + +# =========================================================================== +# CLI: scaffold_unittest +# =========================================================================== + +class TestScaffoldUnittest: + """Tests for the unit testing scaffold generator.""" + + # ── Python / pytest ────────────────────────────────────────────────────── + + def test_pytest_ini_contains_testpaths(self): + content = scaffold_unittest.generate_pytest_ini("my-app", coverage=True) + assert "testpaths = tests" in content + + def test_pytest_ini_contains_coverage_options(self): + content = scaffold_unittest.generate_pytest_ini("my-app", coverage=True) + assert "--cov=" in content + assert "--cov-report=xml" in content + + def test_pytest_ini_no_coverage_when_disabled(self): + content = scaffold_unittest.generate_pytest_ini("my-app", coverage=False) + assert "--cov=" not in content + + def test_conftest_py_contains_fixture(self): + content = scaffold_unittest.generate_conftest_py("my-app") + assert "@pytest.fixture" in content + assert "sample_config" in content + + def test_conftest_py_uses_app_name(self): + content = scaffold_unittest.generate_conftest_py("my-svc") + assert "my-svc" in content + + def test_python_test_sample_has_test_class(self): + content = scaffold_unittest.generate_python_test_sample("my-app") + assert "class Test" in content + assert "def test_" in content + + def test_python_test_sample_has_parametrize(self): + content = scaffold_unittest.generate_python_test_sample("my-app") + assert "parametrize" in content + + # ── JavaScript / Jest ──────────────────────────────────────────────────── + + def test_jest_config_has_test_environment(self): + content = scaffold_unittest.generate_jest_config("my-app", is_typescript=False, coverage=True) + assert "testEnvironment" in content + assert "node" in content + + def test_jest_config_with_coverage(self): + content = scaffold_unittest.generate_jest_config("my-app", is_typescript=False, coverage=True) + assert "collectCoverage" in content + assert "coverageThreshold" in content + + def test_jest_config_without_coverage(self): + content = scaffold_unittest.generate_jest_config("my-app", is_typescript=False, coverage=False) + assert "collectCoverage" not in content + + def test_jest_config_typescript_transform(self): + content = scaffold_unittest.generate_jest_config("my-app", is_typescript=True, coverage=False) + assert "ts-jest" in content + assert "moduleFileExtensions" in content + + def test_js_test_sample_jest_uses_expect(self): + content = scaffold_unittest.generate_js_test_sample("my-app", "jest", is_typescript=False) + assert "expect(" in content + assert "toBe(" in content + assert "describe(" in content + + def test_js_test_sample_mocha_uses_chai(self): + content = scaffold_unittest.generate_js_test_sample("my-app", "mocha", is_typescript=False) + assert "require('chai')" in content + assert ".to.equal" in content + + def test_js_test_sample_vitest_import(self): + content = scaffold_unittest.generate_js_test_sample("my-app", "vitest", is_typescript=False) + assert "from 'vitest'" in content + + # ── JavaScript / Vitest ────────────────────────────────────────────────── + + def test_vitest_config_has_defineconfig(self): + content = scaffold_unittest.generate_vitest_config("my-app", coverage=True) + assert "defineConfig" in content + assert "vitest/config" in content + + def test_vitest_config_coverage_block(self): + content = scaffold_unittest.generate_vitest_config("my-app", coverage=True) + assert "coverage" in content + assert "thresholds" in content + + def test_vitest_config_no_coverage(self): + content = scaffold_unittest.generate_vitest_config("my-app", coverage=False) + assert "thresholds" not in content + + # ── JavaScript / Mocha ─────────────────────────────────────────────────── + + def test_mocha_rc_has_spec_glob(self): + content = scaffold_unittest.generate_mocha_rc("my-app", coverage=True) + assert "spec" in content + assert "tests/**" in content + + def test_mocha_rc_mentions_nyc_when_coverage(self): + content = scaffold_unittest.generate_mocha_rc("my-app", coverage=True) + assert "nyc" in content + + # ── Go / go test ───────────────────────────────────────────────────────── + + def test_go_test_sample_has_testing_import(self): + content = scaffold_unittest.generate_go_test_sample("my-service") + assert '"testing"' in content + + def test_go_test_sample_has_table_driven_test(self): + content = scaffold_unittest.generate_go_test_sample("my-service") + assert "TestTableDriven" in content + + def test_go_test_sample_uses_package_name(self): + content = scaffold_unittest.generate_go_test_sample("my-service") + assert "my_service_test" in content + + def test_go_makefile_has_test_target(self): + content = scaffold_unittest.generate_go_makefile("my-service", coverage=True) + assert "test:" in content + assert "go test" in content + + def test_go_makefile_has_coverage_target(self): + content = scaffold_unittest.generate_go_makefile("my-service", coverage=True) + assert "test-cov:" in content + assert "coverprofile" in content + + def test_go_makefile_no_coverage_when_disabled(self): + content = scaffold_unittest.generate_go_makefile("my-service", coverage=False) + assert "coverprofile" not in content + + # ── generate_unittest_scaffold (file-system) ───────────────────────────── + + def test_generate_scaffold_python_writes_files(self): + with tempfile.TemporaryDirectory() as tmp: + written = scaffold_unittest.generate_unittest_scaffold( + name="my-app", + languages="python", + framework="", + coverage=True, + output_dir=tmp, + ) + paths = [str(p) for p, _ in written] + assert any("pytest.ini" in p for p in paths) + assert any("conftest.py" in p for p in paths) + assert any("test_sample.py" in p for p in paths) + + def test_generate_scaffold_javascript_jest_writes_files(self): + with tempfile.TemporaryDirectory() as tmp: + written = scaffold_unittest.generate_unittest_scaffold( + name="my-app", + languages="javascript", + framework="jest", + coverage=True, + output_dir=tmp, + ) + paths = [str(p) for p, _ in written] + assert any("jest.config.js" in p for p in paths) + assert any("sample.test.js" in p for p in paths) + + def test_generate_scaffold_typescript_vitest_writes_files(self): + with tempfile.TemporaryDirectory() as tmp: + written = scaffold_unittest.generate_unittest_scaffold( + name="my-app", + languages="typescript", + framework="vitest", + coverage=False, + output_dir=tmp, + ) + paths = [str(p) for p, _ in written] + assert any("vitest.config.js" in p for p in paths) + assert any("sample.test.ts" in p for p in paths) + + def test_generate_scaffold_go_writes_files(self): + with tempfile.TemporaryDirectory() as tmp: + written = scaffold_unittest.generate_unittest_scaffold( + name="my-service", + languages="go", + framework="", + coverage=True, + output_dir=tmp, + ) + paths = [str(p) for p, _ in written] + assert any("_test.go" in p for p in paths) + assert any("Makefile.test" in p for p in paths) + + def test_generate_scaffold_multi_language(self): + with tempfile.TemporaryDirectory() as tmp: + written = scaffold_unittest.generate_unittest_scaffold( + name="full-stack", + languages="python,javascript,go", + framework="", + coverage=True, + output_dir=tmp, + ) + paths = [str(p) for p, _ in written] + # Python + assert any("pytest.ini" in p for p in paths) + # JavaScript + assert any("jest.config.js" in p for p in paths) + # Go + assert any("_test.go" in p for p in paths) + + def test_generate_scaffold_unsupported_language_skips(self): + with tempfile.TemporaryDirectory() as tmp: + written = scaffold_unittest.generate_unittest_scaffold( + name="my-app", + languages="rust", + framework="", + coverage=True, + output_dir=tmp, + ) + assert written == [] + + def test_generate_scaffold_mocha_framework(self): + with tempfile.TemporaryDirectory() as tmp: + written = scaffold_unittest.generate_unittest_scaffold( + name="my-app", + languages="javascript", + framework="mocha", + coverage=True, + output_dir=tmp, + ) + paths = [str(p) for p, _ in written] + assert any(".mocharc.js" in p for p in paths) + assert not any("jest.config.js" in p for p in paths) + + # ── CLI module invocation ───────────────────────────────────────────────── + + def test_cli_python_scaffold(self): + with tempfile.TemporaryDirectory() as tmp: + result = _run_module( + "cli.scaffold_unittest", + ["--name", "my-app", "--languages", "python", "--output-dir", tmp], + ) + assert result.returncode == 0 + assert "pytest.ini" in result.stdout + assert os.path.exists(os.path.join(tmp, "pytest.ini")) + + def test_cli_javascript_jest_scaffold(self): + with tempfile.TemporaryDirectory() as tmp: + result = _run_module( + "cli.scaffold_unittest", + ["--name", "my-lib", "--languages", "javascript", + "--framework", "jest", "--output-dir", tmp], + ) + assert result.returncode == 0 + assert os.path.exists(os.path.join(tmp, "jest.config.js")) + + def test_cli_go_scaffold(self): + with tempfile.TemporaryDirectory() as tmp: + result = _run_module( + "cli.scaffold_unittest", + ["--name", "my-api", "--languages", "go", "--output-dir", tmp], + ) + assert result.returncode == 0 + assert os.path.exists(os.path.join(tmp, "my_api_test.go")) + + def test_cli_no_coverage_flag(self): + with tempfile.TemporaryDirectory() as tmp: + result = _run_module( + "cli.scaffold_unittest", + ["--name", "my-app", "--languages", "python", + "--no-coverage", "--output-dir", tmp], + ) + assert result.returncode == 0 + content = open(os.path.join(tmp, "pytest.ini")).read() + assert "--cov=" not in content + + +# =========================================================================== +# MCP Server: generate_unittest_config +# =========================================================================== + +class TestMCPServerUnittest: + """Tests for the MCP server generate_unittest_config tool.""" + + def test_python_returns_pytest_ini(self): + result = json.loads(generate_unittest_config(name="my-api", languages="python")) + assert "pytest.ini" in result + assert "conftest.py" in result + assert "tests/test_sample.py" in result + + def test_javascript_jest_returns_jest_config(self): + result = json.loads(generate_unittest_config( + name="my-app", languages="javascript", framework="jest")) + assert "jest.config.js" in result + assert "tests/sample.test.js" in result + + def test_javascript_mocha_returns_mocharc(self): + result = json.loads(generate_unittest_config( + name="my-app", languages="javascript", framework="mocha")) + assert ".mocharc.js" in result + + def test_javascript_vitest_returns_vitest_config(self): + result = json.loads(generate_unittest_config( + name="my-app", languages="javascript", framework="vitest")) + assert "vitest.config.js" in result + + def test_typescript_jest_returns_ts_test_file(self): + result = json.loads(generate_unittest_config( + name="my-app", languages="typescript", framework="jest")) + assert "tests/sample.test.ts" in result + assert "jest.config.js" in result + + def test_go_returns_test_file_and_makefile(self): + result = json.loads(generate_unittest_config( + name="my-service", languages="go")) + assert "my_service_test.go" in result + assert "Makefile.test" in result + + def test_multi_language_returns_all_files(self): + result = json.loads(generate_unittest_config( + name="full-stack", languages="python,javascript,go")) + assert "pytest.ini" in result + assert "jest.config.js" in result + assert "full_stack_test.go" in result + + def test_coverage_true_includes_coverage_config(self): + result = json.loads(generate_unittest_config( + name="my-app", languages="python", coverage=True)) + assert "--cov=" in result["pytest.ini"] + + def test_coverage_false_excludes_coverage_config(self): + result = json.loads(generate_unittest_config( + name="my-app", languages="python", coverage=False)) + assert "--cov=" not in result["pytest.ini"] + + def test_go_test_file_content_has_testing_import(self): + result = json.loads(generate_unittest_config( + name="my-svc", languages="go")) + assert '"testing"' in result["my_svc_test.go"] + + def test_unsupported_language_returns_empty(self): + result = json.loads(generate_unittest_config( + name="my-app", languages="rust")) + assert result == {}