diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a0236fc2..e527b3ae 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -107,7 +107,7 @@ jobs: run: echo "PYTHONPATH=${GITHUB_WORKSPACE}/release_notes_generator/release_notes_generator" >> $GITHUB_ENV - name: Check code coverage with Pytest - run: pytest --cov=. -v tests/ --cov-fail-under=80 + run: pytest --cov=. -v tests/unit --cov-fail-under=80 mypy-check: runs-on: ubuntu-latest diff --git a/.specify/constitution.md b/.specify/constitution.md index 2b03e219..3d709b3c 100644 --- a/.specify/constitution.md +++ b/.specify/constitution.md @@ -1,11 +1,12 @@ # Release Notes Scrapper Action Constitution @@ -65,11 +66,23 @@ manage version tagging; it consumes existing tags. 7. `ReleaseNotesBuilder` iterates chapters → collects matching records → applies row format templates / duplicity markers. 8. Final Markdown string emitted; composite action sets `release-notes` output. -### Boundary Rules +### Boundary Rules (Refined) - Action inputs boundary: all configurable behavior must pass via declared inputs (no hidden runtime switches). - GitHub API boundary: all repository data access encapsulated within miner and rate limiter logic. - Formatting boundary: only row format templates and chapters influence visible line structure; business logic must not directly embed presentation markup elsewhere. +- Error boundary: modules MUST NOT leak raw exceptions across boundaries; they must convert failures into logged events + and structured return values (see Principle 9). Internal exceptions MAY be raised and caught within the same module. +- Utility boundary: functions in `utils/` MUST be demonstrably used by at least one importing module or removed (see Principle 8 & 10). + +### Module Boundary Follow-Up +A scheduled audit SHALL verify: +- `utils/` contains only actively referenced functions (dead code removal list to be created). +- `generator.py` remains orchestration-only (no direct formatting or low-level API calls beyond miner invocation). +- `builder/` never performs mining; strictly transforms records to Markdown. +- `record/factory` isolates construction logic; future refactors MAY extract validation into a separate `validators/` module. +- Logging configuration centralization: confirm no duplicate ad-hoc log setup outside `main.py`. +Outcome: Produce a follow-up task list referencing each violation if found; merge only with accompanying unit tests. ## 3. Data & Integrations @@ -119,13 +132,39 @@ manage version tagging; it consumes existing tags. ## 5. Quality & Testing ### Test Types -- Unit tests (expected for pure utility and formatting functions). +- Unit tests for pure utility, transformation, formatting, and record construction functions. - Integration tests (e.g. `integration_test.py`) covering end-to-end generation using mocked or controlled data. -- No explicit contract tests yet; future addition may define record/chapter contract snapshots. +- Future: contract/snapshot tests MAY be introduced for chapter output stability (not mandatory yet). + +### Test Directory Structure (New) +``` +tests/ + unit/ # All Python unit tests (test_.py) - REQUIRED location + integration/ # Future integration tests (current single file may migrate here) + fixtures/ # Shared static test data & factories (optional) + helpers/ # Helper utilities used only by tests (must be imported by tests/*) + release_notes/ # Domain-specific sample data (review for possible move under fixtures/) + utils/ # Test-only utility functions (rename to helpers/ or remove if redundant) +``` +Rules: +- All unit tests MUST reside under `tests/unit/` (root-level `test_*.py` files SHALL be relocated). +- Naming: `test_.py`; multiple related small targets MAY share one file if cohesive. +- Test style: uses ONLY `pytest` (no unittest classes). Prefer functions + fixtures. +- Fixtures: define shared objects in `tests/conftest.py` or per-file fixtures; keep scope minimal. +- Parametrization: use `@pytest.mark.parametrize` for input matrix instead of loops. +- Coverage: new logic MUST raise overall coverage or keep it steady; dropping coverage requires explicit justification. +- NEW: Unit test file path MUST mirror source relative package path (Principle 12). For source file `release_notes_generator/utils/constants.py`, the test lives at `tests/unit/release_notes_generator/utils/test_constants.py`. +- Branch Naming: Feature / fix / docs / chore PRs MUST originate from correctly prefixed branch (Principle 13); CI may validate. + +### Organization & Integration +- Integration tests MUST import public interfaces only (`main`, `ReleaseNotesGenerator`) not internal private helpers. +- Unit tests MUST avoid real network calls; use mocking or local sample data. +- Cross-test independence: tests MUST NOT rely on execution order; no shared mutation outside fixture scope. +- Relocation of existing root-level unit tests into `tests/unit/` SHALL be part of first compliance PR post-amendment. ### Coverage -- `pytest-cov` integrated; HTML coverage artifacts seen in `htmlcov/`. Target: maintain or improve existing coverage - (implicit baseline > minimal demonstration). New core logic MUST include tests before implementation (Test‑First Principle). +- `pytest-cov` integrated; HTML coverage artifacts under `htmlcov/`. Baseline maintained or improved. New core logic MUST + include tests before implementation (Test‑First Principle). ### Static Analysis & Review - `pylint`, `mypy` required to pass (configuration present). @@ -135,7 +174,9 @@ manage version tagging; it consumes existing tags. ### Quality Gates (Minimum Acceptance) - Tests: ALL must pass. - Lint + type: zero blocking errors. +- No unused functions/methods (see Principle 10) — introduce usage or delete in same PR. - Backward compatibility: no silent change to input names or placeholder semantics without version bump & documentation update. +- Branch naming compliance (Principle 13) — allowed prefixes: feature/, fix/, docs/, chore/; rename if violated. ## 6. Constraints & Compatibility @@ -169,6 +210,7 @@ manage version tagging; it consumes existing tags. - Hierarchy expansion could incur additional API calls increasing latency. - Duplicate detection edge cases may confuse users if same issue intentionally spans categories. - CodeRabbit integration features may parse unintended summary content (format variance risk). +- Dead code accumulation in `utils/` may reduce clarity if Principle 10 not enforced promptly. ### Assumptions - Repository uses semantic version tags (prefixed optionally by `v`). @@ -200,6 +242,7 @@ manage version tagging; it consumes existing tags. ### Compliance Review - PR template or automated check SHOULD reference Constitution principles (especially Test‑First & Stability) before merge. - Violations require explicit justification section in PR description. +- Review checklist MUST confirm Principle 13 prefix correctness and scope alignment. ### Release Management - Tagging strategy external; this action consumes tags. Recommend semantic versioning for repository releases. @@ -236,7 +279,7 @@ Ensure Constitution Check section in `plan.md` passes before advancing to detail ## Change Log / Versioning - Project releases follow Git tags; this constitution uses semantic versioning independent of code releases. -- Current Constitution Version: 1.0.1 (initial ratification). +- Current Constitution Version: 1.4.0 (amended with new principles & test structure). - Future amendments tracked via Sync Impact Report at top of this file. ## Core Principles @@ -277,5 +320,53 @@ Mining MUST use rate limiter abstraction; avoid redundant API calls (e.g. re-fet MUST short-circuit when disabled. Performance considerations addressed before accepting features that multiply API calls. Rationale: Preserves quota & improves speed on large repositories. +### Principle 8: Lean Python Design +Prefer simple functions and modules over unnecessary classes. A class MUST only be introduced when stateful behavior or +polymorphism is required. Utility modules SHOULD expose pure functions. Avoid deep inheritance; favor composition. +Rationale: Reduces complexity and improves readability & testability. + +### Principle 9: Localized Error Handling & Non-Exceptional Flow +Modules MUST catch internal exceptions and convert them into structured return values plus logged messages. Cross-module +exception propagation (raising raw exceptions across boundaries) is prohibited except for truly unrecoverable setup +failures at the entry point (`main`). Return either a valid result or a clearly logged empty/partial result. +Rationale: Ensures predictable action behavior and prevents silent termination in CI pipelines. + +### Principle 10: Dead Code Prohibition +No unused methods/functions SHALL remain in the codebase (properties or inherited abstract/interface methods excepted). +Utility files MUST contain only actively invoked functions. Removal of unused code MUST occur in the same PR that +introduces its obsolescence. +Rationale: Prevents confusion, reduces maintenance overhead, and keeps coverage meaningful. + +### Principle 11: Focused & Informative Comments +Comments MUST explain non-obvious logic, constraints, or reasoning succinctly. Prohibited: narrative, outdated, or +speculative comments. Allowed: brief context before complex loops, rationale for workaround, links to issue references. +Comments SHOULD be maintained or updated alongside code changes; stale comments MUST be removed. +Rationale: Enhances clarity without adding noise. + +### Principle 12: Test Path Mirroring +Each unit test file MUST reside under `tests/unit/` mirroring the source package path and file name: `tests/unit//test_.py`. +Mandatory Rules: +- One test file per source file unless tightly coupled logic demands grouping (justify in PR). +- Legacy non-mirrored category folders are deprecated; migrate incrementally without reducing coverage. +- New or refactored modules require mirrored test path in same PR. +Rationale: Ensures predictable test discovery, simplifies navigation between code and tests, and supports scalable refactors. + +### Principle 13: Branch Naming Consistency +All new branches for work MUST start with one of the approved prefixes followed by a concise kebab-case descriptor (optional numeric ID). +Approved prefixes: +- `feature/` – new features & enhancements +- `fix/` – bug fixes / defect resolutions +- `docs/` – documentation-only updates +- `chore/` – maintenance, dependency bumps, CI, non-behavioral refactors +Examples: +`feature/add-hierarchy-support`, `fix/567-handle-empty-chapters`, `docs/improve-readme-start`, `chore/upgrade-semver-lib` +Rules: +- Prefix REQUIRED and MUST be in approved set; rename non-compliant branches prior to PR. +- Descriptor: lowercase kebab-case; hyphen separators; no spaces/underscores/trailing slash. +- Optional numeric ID may precede description (`fix/987-null-title`). +- Category alignment: branch prefix MUST match primary scope of PR contents. +- Avoid vague descriptors (`update`, `changes`). Prefer action or subject (`improve-logging`, `remove-dead-code`). +Rationale: Standardizes history, enables automated governance checks, clarifies intent for reviewers & tooling. + ## Governance Metadata -**Version**: 1.0.1 | **Ratified**: 2025-10-12 | **Last Amended**: 2025-10-12 +**Version**: 1.4.0 | **Ratified**: 2025-10-12 | **Last Amended**: 2025-10-14 diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index 1ed8d77a..7bfd3ae4 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -1,50 +1,151 @@ -# [PROJECT_NAME] Constitution - + + +# Generate Release Notes Action Constitution ## Core Principles -### [PRINCIPLE_1_NAME] - -[PRINCIPLE_1_DESCRIPTION] - +### Principle 1: Test‑First Reliability +All core logic (mining, filtering, record building, formatting) MUST have failing unit tests written before implementation +and passing after. Refactors MUST preserve existing green tests. No feature merges without unit tests. +Rationale: Prevent regressions & maintain deterministic behavior for CI consumers. + +### Principle 2: Explicit Configuration Boundaries +Runtime behavior MUST be controlled only via declared GitHub Action inputs. Hidden flags or undeclared env vars prohibited. +Add inputs → MINOR version bump; rename/remove → MAJOR bump. +Rationale: Ensures predictability & backward compatibility for workflows pinning versions. + +### Principle 3: Deterministic Output Formatting +Given identical repository state & inputs, release notes MUST be identical. Ordering MUST be stable (sorted where needed). +Row template placeholders MUST remain consistent (additions allowed; removals require MAJOR bump). +Rationale: Stable diffs & reliable downstream automation (publishing, auditing). + +### Principle 4: Minimal Surface & Single Responsibility +Modules stay focused (inputs parsing, mining, building, logging). Cross-cutting concerns (tag creation, external alerts) +are excluded; implement in separate tools/actions. Avoid feature creep. +Rationale: Low maintenance cost & clear mental model. + +### Principle 5: Transparency & Observability +Structured logging MUST trace lifecycle: start → inputs validated → mining done → build done → finish. Errors logged with +context; verbose flag unlocks extra diagnostics without altering behavior. +Rationale: Fast debugging in ephemeral CI environments. + +### Principle 6: Safe Extensibility +New placeholders, chapters, or hierarchy features MUST default to non-breaking behavior. Provide opt-in flags if impact +uncertain. Document additions in README + release notes. +Rationale: Incremental evolution without destabilizing existing users. + +### Principle 7: Resource-Conscious GitHub API Usage +All mining MUST route through rate limiter abstractions. Disable hierarchy expansion when feature off. Avoid redundant +fetches (cache IDs once retrieved). Performance concerns addressed before merging API-heavy features. +Rationale: Preserves rate limits & improves speed. + +### Principle 8: Lean Python Design +Prefer pure functions; introduce classes ONLY when stateful behavior or polymorphism required. Avoid deep inheritance; +favor composition. Utility modules keep narrow surface. +Rationale: Improves readability, testability, and reduces accidental complexity. + +### Principle 9: Localized Error Handling & Non-Exceptional Flow +Do NOT raise raw exceptions across module boundaries. Catch internally → log → return structured result (empty/partial). +Only unrecoverable initialization failures (e.g., missing auth token) may exit early at entry point. +Rationale: Predictable action completion and clear diagnostics. + +### Principle 10: Dead Code Prohibition +Unused functions/methods (except properties or required inherited methods) MUST be removed in same PR that obsoletes them. +Utility files contain ONLY invoked logic. CI or review MUST flag new unused code. +Rationale: Prevents confusion & keeps coverage meaningful. -### [PRINCIPLE_2_NAME] - -[PRINCIPLE_2_DESCRIPTION] - +### Principle 11: Focused & Informative Comments +Comments MUST succinctly explain non-obvious logic, constraints, or workaround rationale. Prohibited: stale, narrative, +Speculative, or redundant comments. Maintain or delete on change; never leave outdated intent. +Rationale: Enhances clarity without noise. -### [PRINCIPLE_3_NAME] - -[PRINCIPLE_3_DESCRIPTION] - +### Principle 12: Test Path Mirroring +Unit tests MUST mirror source file paths inside `tests/unit/`: +`release_notes_generator//file.py` → `tests/unit/release_notes_generator//test_file.py`. +Rules: +- New tests follow mirroring immediately. +- Grouping multiple source files in one test file requires justification (shared invariant or helper pattern). +- Legacy categorized folders (`tests/release_notes`, `tests/data`, `tests/model`, `tests/utils`) are transitional; migrate gradually without lowering coverage. +Rationale: Streamlines navigation, encourages focused tests, reduces ambiguity in ownership. -### [PRINCIPLE_4_NAME] - -[PRINCIPLE_4_DESCRIPTION] - +### Principle 13: Branch Naming Consistency +All new work branches MUST start with an approved prefix followed by a concise kebab-case descriptor (optional leading numeric ID). +Allowed prefixes (enforced): +- `feature/` → Feature & enhancement work introducing new capability or non-trivial behavior +- `fix/` → Bug fixes addressing defects (issues labeled bug/error) +- `docs/` → Documentation-only changes (README, docs/, CONTRIBUTING, DEVELOPER guides) +- `chore/` → Maintenance, dependency updates, CI adjustments, refactors without behavioral change +Examples: +- `feature/add-hierarchy-support`, `feature/123-hierarchy-support` +- `fix/456-null-pointer-on-empty-labels` +- `docs/improve-hierarchy-guide` +- `chore/update-pylint-config` +Rules: +- Prefix MUST be one of the allowed set; otherwise branch renamed before PR. +- Descriptor: lowercase kebab-case; hyphens only; no spaces/underscores/trailing slash. +- Optional numeric ID may precede description: `fix/987-label-trim`. +- Avoid vague terms (`update`, `changes`); state intent (`improve-logging`, `relabel-duplicate-detection`). +- Forbidden: mixing categories (e.g., `feature-fix/`), uppercase, camelCase. +- Scope alignment: PR description MUST align with chosen prefix category; reviewers reject mismatches (e.g., docs-only PR on feature/ branch). +Rationale: Enables automated classification, precise audit tooling, clearer commit/PR history semantics, and supports future CI policy enforcement. -### [PRINCIPLE_5_NAME] - -[PRINCIPLE_5_DESCRIPTION] - +## Quality & Testing -## [SECTION_2_NAME] - +- Test Directory Structure: + - tests/unit/: All unit tests (test_.py) — required location. + - tests/integration/: End-to-end tests (integration_test.py to be migrated here when reorganized). + - tests/fixtures/: Optional static data samples. + - tests/helpers/ & tests/utils/: Test-only helpers (utils may merge into helpers). +- Framework: pytest ONLY (no unittest classes). +- Coverage: Enforce threshold ≥80% (existing command uses --cov-fail-under=80). New logic must keep or raise coverage. +- Independence: Tests MUST not depend on run order or mutate shared global state beyond fixture scope. +- Parametrization: Use @pytest.mark.parametrize instead of manual loops. +- Integration tests import public interfaces only (e.g., main entry, generator class). +- Failing tests are written first (Principle 1) for new core logic. +- NEW: Path mirroring (Principle 12) enforced for all new/changed modules. +- Transitional Migration Plan: Add tasks in upcoming PRs to relocate remaining categorized tests. +- Branch Naming Check: Implementation PRs MUST originate from an allowed prefixed branch (`feature/`, `fix/`, `docs/`, `chore/`). (Principle 13) -[SECTION_2_CONTENT] - +## Workflow & Quality Gates -## [SECTION_3_NAME] - +Pre-merge local mandatory checkers (from DEVELOPER.md): +1. Formatting: black --check (line length 120 per pyproject.toml). +2. Linting: pylint global score target ≥9.5 (no fatal errors). +3. Typing: mypy (0 blocking errors) — treat Any proliferation as smell (justify if unavoidable). +4. Tests: pytest all green. +5. Coverage: ≥80% overall; justify any temporary dip (must be recovered within next PR). +6. Dead Code: grep for unused utilities; remove or reference usage in same PR. +7. Determinism: (Manual) Validate repeated runs produce identical output for sample dataset. +8. Branch Naming: CI/Review MUST verify allowed prefix (feature|fix|docs|chore). Non-compliant branches BLOCK merge until renamed. -[SECTION_3_CONTENT] - +Quality Gate Failure Handling: +- Minor failures (formatting, lint) → fix immediately; do not merge with waivers unless urgent hotfix. +- Coverage dip → requires explicit justification + recovery plan (link issue ID). +- Non-deterministic output → BLOCKING until resolved. +- Branch naming violation → BLOCKING until branch renamed; no exception (prefix set: feature|fix|docs|chore). ## Governance - -[GOVERNANCE_RULES] - +- Constitution supersedes ad-hoc practices; PRs MUST state compliance or list justified exceptions. +- Versioning (this constitution): Semantic (MAJOR.MINOR.PATCH). + - MAJOR: Remove/redefine a principle or backward incompatible process change. + - MINOR: Add new principle/section (current change qualifies here: Branch Naming Consistency). + - PATCH: Clarifications/typos with no semantic effect. +- Amendment Flow: + 1. Propose change with rationale & impact assessment. + 2. Update Sync Impact Report header (include affected templates & TODOs). + 3. Bump version according to rule above. + 4. Obtain maintainer approval (≥1) — emergency fixes allow retroactive review. +- Compliance Review: PR template SHOULD reference Principles 1, 2, 10, 12, 13 (multi-prefix) + coverage threshold. Reviewers reject if principles violated without justification. +- Backward Compatibility: Input names & placeholder semantics require MAJOR bump if changed. +- Enforcement: CI pipeline SHOULD automate black, pylint, mypy, pytest, coverage threshold; manual deterministic checks remain. Branch naming can be auto-validated by simple prefix check script. -**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE] - \ No newline at end of file +**Version**: 1.4.0 | **Ratified**: 2025-10-12 | **Last Amended**: 2025-10-14 diff --git a/.specify/scripts/bash/common.sh b/.specify/scripts/bash/common.sh deleted file mode 100755 index 34e5d4bb..00000000 --- a/.specify/scripts/bash/common.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash -# Common functions and variables for all scripts - -# Get repository root, with fallback for non-git repositories -get_repo_root() { - if git rev-parse --show-toplevel >/dev/null 2>&1; then - git rev-parse --show-toplevel - else - # Fall back to script location for non-git repos - local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - (cd "$script_dir/../../.." && pwd) - fi -} - -# Get current branch, with fallback for non-git repositories -get_current_branch() { - # First check if SPECIFY_FEATURE environment variable is set - if [[ -n "${SPECIFY_FEATURE:-}" ]]; then - echo "$SPECIFY_FEATURE" - return - fi - - # Then check git if available - if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then - git rev-parse --abbrev-ref HEAD - return - fi - - # For non-git repos, try to find the latest feature directory - local repo_root=$(get_repo_root) - local specs_dir="$repo_root/specs" - - if [[ -d "$specs_dir" ]]; then - local latest_feature="" - local highest=0 - - for dir in "$specs_dir"/*; do - if [[ -d "$dir" ]]; then - local dirname=$(basename "$dir") - if [[ "$dirname" =~ ^([0-9]{3})- ]]; then - local number=${BASH_REMATCH[1]} - number=$((10#$number)) - if [[ "$number" -gt "$highest" ]]; then - highest=$number - latest_feature=$dirname - fi - fi - fi - done - - if [[ -n "$latest_feature" ]]; then - echo "$latest_feature" - return - fi - fi - - echo "main" # Final fallback -} - -# Check if we have git available -has_git() { - git rev-parse --show-toplevel >/dev/null 2>&1 -} - -check_feature_branch() { - local branch="$1" - local has_git_repo="$2" - - # For non-git repos, we can't enforce branch naming but still provide output - if [[ "$has_git_repo" != "true" ]]; then - echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2 - return 0 - fi - - if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then - echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 - echo "Feature branches should be named like: 001-feature-name" >&2 - return 1 - fi - - return 0 -} - -get_feature_dir() { echo "$1/specs/$2"; } - -get_feature_paths() { - local repo_root=$(get_repo_root) - local current_branch=$(get_current_branch) - local has_git_repo="false" - - if has_git; then - has_git_repo="true" - fi - - local feature_dir=$(get_feature_dir "$repo_root" "$current_branch") - - cat </dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; } diff --git a/.specify/templates/plan-template.md b/.specify/templates/plan-template.md index 70fa8f97..3cb340d9 100644 --- a/.specify/templates/plan-template.md +++ b/.specify/templates/plan-template.md @@ -1,6 +1,6 @@ # Implementation Plan: [FEATURE] -**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link] +**Branch**: `/[###-descriptor]` (prefix ∈ {feature, fix, docs, chore}) | **Date**: [DATE] | **Spec**: [link] **Input**: Feature specification from `/specs/[###-feature-name]/spec.md` **Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow. @@ -17,21 +17,28 @@ the iteration process. --> -**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION] -**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION] -**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A] -**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION] -**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION] -**Project Type**: [single/web/mobile - determines source structure] -**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION] -**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION] -**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION] +**Language/Version**: [e.g., Python 3.11 or NEEDS CLARIFICATION] +**Primary Dependencies**: [e.g., PyGithub, PyYAML, semver or NEEDS CLARIFICATION] +**Testing**: pytest ONLY (per Constitution) +**Target Platform**: GitHub Action runners (Ubuntu) or NEEDS CLARIFICATION +**Performance Goals**: [domain-specific, e.g., <5s generation time for 500 issues] +**Constraints**: [e.g., rate limit adherence, stable deterministic output] +**Scale/Scope**: [e.g., repos up to 10k issues in release window] ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* -[Gates determined based on constitution file] +Mandatory alignment items: +- Test‑First Reliability: Provide failing unit test list BEFORE implementation. +- Explicit Configuration Boundaries: All new behavior exposed via action inputs (list any new inputs needed). +- Deterministic Output Formatting: Confirm ordering & placeholders remain stable. +- Lean Python Design: Justify each new class; prefer functions for stateless logic. +- Localized Error Handling: Define how errors are logged instead of cross-module exceptions. +- Dead Code Prohibition: Identify any code to delete made obsolete by this feature. +- Test Path Mirroring: Confirm new unit tests placed in `tests/unit//test_.py`. +- Branch Naming Consistency: Confirm current branch uses allowed prefix (feature|fix|docs|chore): + `git rev-parse --abbrev-ref HEAD | grep -E '^(feature|fix|docs|chore)/'`. ## Project Structure @@ -48,51 +55,19 @@ specs/[###-feature]/ ``` ### Source Code (repository root) - ``` -# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT) -src/ -├── models/ -├── services/ -├── cli/ -└── lib/ +release_notes_generator/ + ...existing modules... tests/ -├── contract/ -├── integration/ -└── unit/ - -# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected) -backend/ -├── src/ -│ ├── models/ -│ ├── services/ -│ └── api/ -└── tests/ - -frontend/ -├── src/ -│ ├── components/ -│ ├── pages/ -│ └── services/ -└── tests/ - -# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected) -api/ -└── [same as backend above] - -ios/ or android/ -└── [platform-specific structure: feature modules, UI flows, platform tests] + unit/ # All unit tests (REQUIRED) + integration/ # End-to-end tests (if any new ones added by feature) + fixtures/ # Static data samples (optional) + helpers/ # Test helper utilities ``` -**Structure Decision**: [Document the selected structure and reference the real -directories captured above] +**Structure Decision**: [Document any new modules or directories added] ## Complexity Tracking @@ -100,5 +75,5 @@ directories captured above] | Violation | Why Needed | Simpler Alternative Rejected Because | |-----------|------------|-------------------------------------| -| [e.g., 4th project] | [current need] | [why 3 projects insufficient] | -| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] | +| [e.g., new class] | [current need] | [function approach insufficient due to state] | +| [e.g., added input] | [feature toggle] | [implicit behavior would break determinism] | diff --git a/.specify/templates/spec-template.md b/.specify/templates/spec-template.md index c67d9149..218cfad9 100644 --- a/.specify/templates/spec-template.md +++ b/.specify/templates/spec-template.md @@ -1,10 +1,22 @@ # Feature Specification: [FEATURE NAME] -**Feature Branch**: `[###-feature-name]` +**Work Branch**: `/[###-descriptor]` where `` ∈ {feature, fix, docs, chore} **Created**: [DATE] **Status**: Draft **Input**: User description: "$ARGUMENTS" +## Constitution Alignment (Mandatory) +List how this feature will comply with core principles: +- Test‑First (P1): Failing unit tests in `tests/unit/test_.py` BEFORE implementation. +- Explicit Configuration Boundaries (P2): New behavior exposed only via documented action inputs (list if any needed). +- Deterministic Output (P3): Define ordering / formatting impacts; MUST remain stable across runs. +- Lean Python Design (P8): Prefer functions; justify any new class (state or polymorphism requirement). +- Localized Error Handling (P9): Describe logging + return strategy; no cross-module exception raises. +- Dead Code Prohibition (P10): Identify any functions to remove or refactor; commit with tests. +- Focused Comments (P11): Plan for concise logic/rationale comments; avoid narrative. +- Test Path Mirroring (P12): Place unit tests at `tests/unit//test_.py`. +- Branch Naming Consistency (P13): Branch MUST start with one of: `feature/`, `fix/`, `docs/`, `chore/`. Use kebab-case descriptor (optional numeric ID). Rename before merge if violated. + ## User Scenarios & Testing *(mandatory)* @@ -45,9 +33,11 @@ description: "Task list template for feature implementation" **Purpose**: Project initialization and basic structure -- [ ] T001 Create project structure per implementation plan -- [ ] T002 Initialize [language] project with [framework] dependencies -- [ ] T003 [P] Configure linting and formatting tools +- [ ] T001 Create any new module directories in `release_notes_generator/` +- [ ] T002 [P] Ensure mirrored test path structure for new/relocated tests (Principle 12) +- [ ] T002a Verify branch prefix matches regex `^(feature|fix|docs|chore)/` (Principle 13) or rename before proceeding +- [ ] T003 [P] Add initial failing unit tests in `tests/unit/` for new logic (Test‑First gate) +- [ ] T004 [P] Configure/verify linting and formatting tools --- @@ -55,16 +45,12 @@ description: "Task list template for feature implementation" **Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented -**⚠️ CRITICAL**: No user story work can begin until this phase is complete +**⚠ CRITICAL**: No user story work can begin until this phase is complete -Examples of foundational tasks (adjust based on your project): - -- [ ] T004 Setup database schema and migrations framework -- [ ] T005 [P] Implement authentication/authorization framework -- [ ] T006 [P] Setup API routing and middleware structure -- [ ] T007 Create base models/entities that all stories depend on -- [ ] T008 Configure error handling and logging infrastructure -- [ ] T009 Setup environment configuration management +- [ ] T005 Implement feature configuration parsing (test: `tests/unit/test_action_inputs.py` extended) +- [ ] T006 [P] Add utilities (if needed) with tests (`tests/unit/test_utils_.py`) +- [ ] T007 Setup error handling pattern (log & return) — no cross-module exception leakage +- [ ] T008 Dead code removal (list obsolete functions) + tests ensuring replacement paths **Checkpoint**: Foundation ready - user story implementation can now begin in parallel @@ -72,179 +58,98 @@ Examples of foundational tasks (adjust based on your project): ## Phase 3: User Story 1 - [Title] (Priority: P1) 🎯 MVP -**Goal**: [Brief description of what this story delivers] - -**Independent Test**: [How to verify this story works on its own] +**Goal**: [Brief description] -### Tests for User Story 1 (OPTIONAL - only if tests requested) ⚠️ +**Independent Test**: [How to verify] -**NOTE: Write these tests FIRST, ensure they FAIL before implementation** +### Mandatory Tests for User Story 1 -- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py -- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py +- [ ] T009 [P] [US1] Unit tests for new pure functions in `tests/unit/test_.py` (start failing) +- [ ] T010 [US1] Update integration test (if scope touched) in `tests/integration/test_generation.py` (optional creation) ### Implementation for User Story 1 -- [ ] T012 [P] [US1] Create [Entity1] model in src/models/[entity1].py -- [ ] T013 [P] [US1] Create [Entity2] model in src/models/[entity2].py -- [ ] T014 [US1] Implement [Service] in src/services/[service].py (depends on T012, T013) -- [ ] T015 [US1] Implement [endpoint/feature] in src/[location]/[file].py -- [ ] T016 [US1] Add validation and error handling -- [ ] T017 [US1] Add logging for user story 1 operations +- [ ] T011 [P] [US1] Implement function(s) in `release_notes_generator/.py` +- [ ] T012 [US1] Logging additions (INFO lifecycle, DEBUG details) +- [ ] T013 [US1] Ensure deterministic ordering adjustments -**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently +**Checkpoint**: User Story 1 fully functional & independently testable --- ## Phase 4: User Story 2 - [Title] (Priority: P2) -**Goal**: [Brief description of what this story delivers] +**Goal**: [Brief description] -**Independent Test**: [How to verify this story works on its own] +**Independent Test**: [How to verify] -### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️ +### Mandatory Tests for User Story 2 -- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py -- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py +- [ ] T014 [P] [US2] Unit tests for added logic ### Implementation for User Story 2 -- [ ] T020 [P] [US2] Create [Entity] model in src/models/[entity].py -- [ ] T021 [US2] Implement [Service] in src/services/[service].py -- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py -- [ ] T023 [US2] Integrate with User Story 1 components (if needed) +- [ ] T015 [US2] Implement logic in existing module +- [ ] T016 [US2] Update records builder ensuring no cross-module exceptions -**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently +**Checkpoint**: User Stories 1 & 2 independently functional --- ## Phase 5: User Story 3 - [Title] (Priority: P3) -**Goal**: [Brief description of what this story delivers] +**Goal**: [Brief description] -**Independent Test**: [How to verify this story works on its own] +**Independent Test**: [How to verify] -### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️ +### Mandatory Tests for User Story 3 -- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py -- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py +- [ ] T017 [P] [US3] Unit tests for added logic ### Implementation for User Story 3 -- [ ] T026 [P] [US3] Create [Entity] model in src/models/[entity].py -- [ ] T027 [US3] Implement [Service] in src/services/[service].py -- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py +- [ ] T018 [US3] Implement functionality +- [ ] T019 [US3] Update documentation/comments (concise, logic-focused) -**Checkpoint**: All user stories should now be independently functional - ---- - -[Add more user story phases as needed, following the same pattern] +**Checkpoint**: All user stories functional; tests green --- ## Phase N: Polish & Cross-Cutting Concerns -**Purpose**: Improvements that affect multiple user stories - -- [ ] TXXX [P] Documentation updates in docs/ -- [ ] TXXX Code cleanup and refactoring -- [ ] TXXX Performance optimization across all stories -- [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/ -- [ ] TXXX Security hardening -- [ ] TXXX Run quickstart.md validation +- [ ] TXXX [P] Documentation updates in `README.md`, `docs/` +- [ ] TXXX Code cleanup (remove any newly unused code) +- [ ] TXXX Performance optimization +- [ ] TXXX [P] Additional unit tests (edge cases) in `tests/unit/` +- [ ] TXXX Security/robustness improvements --- ## Dependencies & Execution Order -### Phase Dependencies - -- **Setup (Phase 1)**: No dependencies - can start immediately -- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories -- **User Stories (Phase 3+)**: All depend on Foundational phase completion - - User stories can then proceed in parallel (if staffed) - - Or sequentially in priority order (P1 → P2 → P3) -- **Polish (Final Phase)**: Depends on all desired user stories being complete +- Setup → Foundational → User Stories (can parallelize after Foundational) → Polish +- Failing unit tests precede implementation per story +- No story proceeds without its mandatory unit tests -### User Story Dependencies - -- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories -- **User Story 2 (P2)**: Can start after Foundational (Phase 2) - May integrate with US1 but should be independently testable -- **User Story 3 (P3)**: Can start after Foundational (Phase 2) - May integrate with US1/US2 but should be independently testable - -### Within Each User Story - -- Tests (if included) MUST be written and FAIL before implementation -- Models before services -- Services before endpoints -- Core implementation before integration -- Story complete before moving to next priority - -### Parallel Opportunities - -- All Setup tasks marked [P] can run in parallel -- All Foundational tasks marked [P] can run in parallel (within Phase 2) -- Once Foundational phase completes, all user stories can start in parallel (if team capacity allows) -- All tests for a user story marked [P] can run in parallel -- Models within a story marked [P] can run in parallel -- Different user stories can be worked on in parallel by different team members - ---- +## Parallel Opportunities -## Parallel Example: User Story 1 - -```bash -# Launch all tests for User Story 1 together (if tests requested): -Task: "Contract test for [endpoint] in tests/contract/test_[name].py" -Task: "Integration test for [user journey] in tests/integration/test_[name].py" - -# Launch all models for User Story 1 together: -Task: "Create [Entity1] model in src/models/[entity1].py" -Task: "Create [Entity2] model in src/models/[entity2].py" -``` - ---- +- All tasks marked [P] can run in parallel +- Different user stories can be developed concurrently once Foundational completes ## Implementation Strategy ### MVP First (User Story 1 Only) - -1. Complete Phase 1: Setup -2. Complete Phase 2: Foundational (CRITICAL - blocks all stories) -3. Complete Phase 3: User Story 1 -4. **STOP and VALIDATE**: Test User Story 1 independently -5. Deploy/demo if ready +1. Setup & Foundational +2. User Story 1 tests → implementation → validation +3. Demo/merge if stable ### Incremental Delivery - -1. Complete Setup + Foundational → Foundation ready -2. Add User Story 1 → Test independently → Deploy/Demo (MVP!) -3. Add User Story 2 → Test independently → Deploy/Demo -4. Add User Story 3 → Test independently → Deploy/Demo -5. Each story adds value without breaking previous stories - -### Parallel Team Strategy - -With multiple developers: - -1. Team completes Setup + Foundational together -2. Once Foundational is done: - - Developer A: User Story 1 - - Developer B: User Story 2 - - Developer C: User Story 3 -3. Stories complete and integrate independently - ---- +Add each story with its own failing tests → implementation → validation cycle. ## Notes -- [P] tasks = different files, no dependencies -- [Story] label maps task to specific user story for traceability -- Each user story should be independently completable and testable -- Verify tests fail before implementing -- Commit after each task or logical group -- Stop at any checkpoint to validate story independently -- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence - - +- Avoid unused functions (delete immediately if obsoleted) +- Prefer functions over classes unless state/polymorphism required +- Handle errors locally; log & return +- Comments concise & logic-focused +- Test Path Mirroring required for new tests diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f3da1e1b..3c0ba981 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,6 +25,19 @@ * Ensure the Pull Request description clearly outlines your solution. * Link your PR to the relevant _Issue_. +## Branch Naming (Principle 13) +Branches MUST start with one of the allowed prefixes: `feature/`, `fix/`, `docs/`, `chore/` +Examples: +- `feature/add-hierarchy-support` +- `fix/567-handle-empty-chapter` +- `docs/improve-contribution-guide` +- `chore/update-ci-python-version` +Rename if needed before pushing: +```shell +git branch -m fix/ +``` +Use lowercase kebab-case and reflect actual scope. + ### Community and Communication If you have any questions or need help, don't hesitate to reach out through our GitHub discussion section. We're here to help! @@ -33,4 +46,4 @@ If you have any questions or need help, don't hesitate to reach out through our Your contributions are invaluable to us. Thank you for being part of the AbsaOSS community and helping us grow and improve! -The AbsaOSS Team \ No newline at end of file +The AbsaOSS Team diff --git a/DEVELOPER.md b/DEVELOPER.md index 1bc261a9..6c60f886 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -125,7 +125,7 @@ Example: Unit tests are written using pytest. To run the tests, use the following command: ```shell -pytest tests/ +pytest tests/unit ``` This will execute all tests located in the tests directory. @@ -135,8 +135,8 @@ This will execute all tests located in the tests directory. Code coverage is collected using the pytest-cov coverage tool. To run the tests and collect coverage information, use the following command: ```shell -pytest --cov=. -v tests/ --cov-fail-under=80 # Check coverage threshold -pytest --cov=. -v tests/ --cov-fail-under=80 --cov-report=html # Generate HTML report +pytest --cov=. -v tests/unit --cov-fail-under=80 # Check coverage threshold +pytest --cov=. -v tests/unit --cov-fail-under=80 --cov-report=html # Generate HTML report ``` This will execute all tests in the tests directory and generate a code coverage report. @@ -179,3 +179,25 @@ export INPUT_GITHUB_TOKEN=$(printenv ) # Run the Python script python3 .//main.py ``` + +## Branch Naming Convention (Principle 13) +All work branches MUST use an allowed prefix followed by a concise kebab-case descriptor (optional numeric ID): +Allowed prefixes: +- feature/ : new functionality & enhancements +- fix/ : bug fixes / defect resolutions +- docs/ : documentation-only updates +- chore/ : maintenance, CI, dependency bumps, non-behavioral refactors +Examples: +- feature/add-hierarchy-support +- fix/456-null-title-parsing +- docs/update-readme-quickstart +- chore/upgrade-pygithub +Rules: +- Prefix mandatory; rename non-compliant branches before PR (`git branch -m feature/` etc.). +- Descriptor lowercase kebab-case; hyphens only; avoid vague terms (`update`, `changes`). +- Align scope: a docs-only PR MUST use docs/ prefix, not feature/. +Verification Tip: +```shell +git rev-parse --abbrev-ref HEAD | grep -E '^(feature|fix|docs|chore)/' || echo 'Branch naming violation (expected allowed prefix)' +``` +Future possible prefixes (not enforced yet): `refactor/`, `perf/`. diff --git a/integration_test.py b/integration_test.py deleted file mode 100644 index 0112ca21..00000000 --- a/integration_test.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -This script demonstrates how to use the BulkSubIssueCollector to find sub-issues -""" - -import os -import urllib3 - -from release_notes_generator.data.utils.bulk_sub_issue_collector import CollectorConfig, BulkSubIssueCollector - -urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - -class MissingTokenError(ValueError): - """Raised when GITHUB_TOKEN environment variable is not set.""" - -token = os.getenv("GITHUB_TOKEN") -if token is None: - raise MissingTokenError("GITHUB_TOKEN environment variable is not set") - -# WARNING: TLS verification is disabled for testing purposes only. -# Do not use this configuration in production. -cfg = CollectorConfig(verify_tls=False) - -collector = BulkSubIssueCollector(token, cfg=cfg) - -new_parents = [ - "absa-group/AUL#2960", -] - -while new_parents: - new_parents = collector.scan_sub_issues_for_parents(new_parents) - print("New parents found:", new_parents) - print("Collected sub-issues so far:", collector.parents_sub_issues) diff --git a/tests/integration/integration_test.py b/tests/integration/integration_test.py new file mode 100644 index 00000000..16d800da --- /dev/null +++ b/tests/integration/integration_test.py @@ -0,0 +1,28 @@ +# Integration test relocated per Constitution test directory structure. +# Performs a smoke scan of sub-issues when GITHUB_TOKEN is provided; otherwise skipped. +import os +import pytest +import urllib3 + +from release_notes_generator.data.utils.bulk_sub_issue_collector import CollectorConfig, BulkSubIssueCollector + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +pytestmark = pytest.mark.skipif( + not os.getenv("GITHUB_TOKEN"), reason="GITHUB_TOKEN not set for integration test" +) + + +def test_bulk_sub_issue_collector_smoke(): + token = os.getenv("GITHUB_TOKEN") + assert token is not None # guarded by skip above + cfg = CollectorConfig(verify_tls=False) + collector = BulkSubIssueCollector(token, cfg=cfg) + new_parents = ["absa-group/AUL#2960"] + iterations = 0 + while new_parents and iterations < 2: # limit iterations for test speed + new_parents = collector.scan_sub_issues_for_parents(new_parents) + iterations += 1 + # Collector internal state should be dict-like even if empty + assert hasattr(collector, "parents_sub_issues") + diff --git a/tests/release_notes/builder/__init__.py b/tests/unit/__init__.py similarity index 100% rename from tests/release_notes/builder/__init__.py rename to tests/unit/__init__.py diff --git a/tests/conftest.py b/tests/unit/conftest.py similarity index 100% rename from tests/conftest.py rename to tests/unit/conftest.py diff --git a/tests/release_notes/chapters/__init__.py b/tests/unit/release_notes_generator/__init__.py similarity index 100% rename from tests/release_notes/chapters/__init__.py rename to tests/unit/release_notes_generator/__init__.py diff --git a/tests/release_notes/data/__init__.py b/tests/unit/release_notes_generator/builder/__init__.py similarity index 100% rename from tests/release_notes/data/__init__.py rename to tests/unit/release_notes_generator/builder/__init__.py diff --git a/tests/release_notes/builder/test_release_notes_builder.py b/tests/unit/release_notes_generator/builder/test_release_notes_builder.py similarity index 99% rename from tests/release_notes/builder/test_release_notes_builder.py rename to tests/unit/release_notes_generator/builder/test_release_notes_builder.py index 76ae1498..9f8768ca 100644 --- a/tests/release_notes/builder/test_release_notes_builder.py +++ b/tests/unit/release_notes_generator/builder/test_release_notes_builder.py @@ -20,7 +20,7 @@ from release_notes_generator.builder.builder import ReleaseNotesBuilder from release_notes_generator.chapters.custom_chapters import CustomChapters from release_notes_generator.record.factory.default_record_factory import DefaultRecordFactory -from tests.conftest import mock_safe_call_decorator, MockLabel +from tests.unit.conftest import mock_safe_call_decorator, MockLabel # pylint: disable=pointless-string-statement """ diff --git a/tests/release_notes/data/utils/__init__.py b/tests/unit/release_notes_generator/chapters/__init__.py similarity index 100% rename from tests/release_notes/data/utils/__init__.py rename to tests/unit/release_notes_generator/chapters/__init__.py diff --git a/tests/release_notes/chapters/test_base_chapters.py b/tests/unit/release_notes_generator/chapters/test_base_chapters.py similarity index 100% rename from tests/release_notes/chapters/test_base_chapters.py rename to tests/unit/release_notes_generator/chapters/test_base_chapters.py diff --git a/tests/release_notes/chapters/test_chapter.py b/tests/unit/release_notes_generator/chapters/test_chapter.py similarity index 100% rename from tests/release_notes/chapters/test_chapter.py rename to tests/unit/release_notes_generator/chapters/test_chapter.py diff --git a/tests/release_notes/chapters/test_custom_chapters.py b/tests/unit/release_notes_generator/chapters/test_custom_chapters.py similarity index 100% rename from tests/release_notes/chapters/test_custom_chapters.py rename to tests/unit/release_notes_generator/chapters/test_custom_chapters.py diff --git a/tests/release_notes/chapters/test_service_chapters.py b/tests/unit/release_notes_generator/chapters/test_service_chapters.py similarity index 100% rename from tests/release_notes/chapters/test_service_chapters.py rename to tests/unit/release_notes_generator/chapters/test_service_chapters.py diff --git a/tests/release_notes/record/__init__.py b/tests/unit/release_notes_generator/data/__init__.py similarity index 100% rename from tests/release_notes/record/__init__.py rename to tests/unit/release_notes_generator/data/__init__.py diff --git a/tests/release_notes/data/test_filter.py b/tests/unit/release_notes_generator/data/test_filter.py similarity index 99% rename from tests/release_notes/data/test_filter.py rename to tests/unit/release_notes_generator/data/test_filter.py index d2b737d1..3f1cd502 100644 --- a/tests/release_notes/data/test_filter.py +++ b/tests/unit/release_notes_generator/data/test_filter.py @@ -22,7 +22,6 @@ from release_notes_generator.data.filter import FilterByRelease from release_notes_generator.model.mined_data import MinedData -from tests.conftest import mock_repo def test_filter_no_release(mocker): diff --git a/tests/release_notes/data/test_miner.py b/tests/unit/release_notes_generator/data/test_miner.py similarity index 99% rename from tests/release_notes/data/test_miner.py rename to tests/unit/release_notes_generator/data/test_miner.py index 443e2001..572b405a 100644 --- a/tests/release_notes/data/test_miner.py +++ b/tests/unit/release_notes_generator/data/test_miner.py @@ -30,7 +30,7 @@ from release_notes_generator.data.miner import DataMiner from release_notes_generator.data.utils.bulk_sub_issue_collector import BulkSubIssueCollector from release_notes_generator.model.mined_data import MinedData -from tests.conftest import FakeRepo +from tests.unit.conftest import FakeRepo class ChildBulkSubIssueCollector(BulkSubIssueCollector): diff --git a/tests/release_notes/record/factory/__init__.py b/tests/unit/release_notes_generator/data/utils/__init__.py similarity index 100% rename from tests/release_notes/record/factory/__init__.py rename to tests/unit/release_notes_generator/data/utils/__init__.py diff --git a/tests/release_notes/data/utils/test_bulk_sub_issue_collector.py b/tests/unit/release_notes_generator/data/utils/test_bulk_sub_issue_collector.py similarity index 100% rename from tests/release_notes/data/utils/test_bulk_sub_issue_collector.py rename to tests/unit/release_notes_generator/data/utils/test_bulk_sub_issue_collector.py diff --git a/tests/release_notes/__init__.py b/tests/unit/release_notes_generator/model/__init__.py similarity index 100% rename from tests/release_notes/__init__.py rename to tests/unit/release_notes_generator/model/__init__.py diff --git a/tests/release_notes/model/test_commit_record.py b/tests/unit/release_notes_generator/model/test_commit_record.py similarity index 100% rename from tests/release_notes/model/test_commit_record.py rename to tests/unit/release_notes_generator/model/test_commit_record.py diff --git a/tests/release_notes/model/test_issue_record.py b/tests/unit/release_notes_generator/model/test_issue_record.py similarity index 100% rename from tests/release_notes/model/test_issue_record.py rename to tests/unit/release_notes_generator/model/test_issue_record.py diff --git a/tests/release_notes/model/test_pull_request_record.py b/tests/unit/release_notes_generator/model/test_pull_request_record.py similarity index 100% rename from tests/release_notes/model/test_pull_request_record.py rename to tests/unit/release_notes_generator/model/test_pull_request_record.py diff --git a/tests/release_notes/model/test_record.py b/tests/unit/release_notes_generator/model/test_record.py similarity index 100% rename from tests/release_notes/model/test_record.py rename to tests/unit/release_notes_generator/model/test_record.py diff --git a/tests/release_notes/record/factory/utils/__init__.py b/tests/unit/release_notes_generator/record/__init__.py similarity index 100% rename from tests/release_notes/record/factory/utils/__init__.py rename to tests/unit/release_notes_generator/record/__init__.py diff --git a/tests/unit/release_notes_generator/record/factory/__init__.py b/tests/unit/release_notes_generator/record/factory/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/release_notes/record/factory/test_default_record_factory.py b/tests/unit/release_notes_generator/record/factory/test_default_record_factory.py similarity index 99% rename from tests/release_notes/record/factory/test_default_record_factory.py rename to tests/unit/release_notes_generator/record/factory/test_default_record_factory.py index 11f088c1..0d86f406 100644 --- a/tests/release_notes/record/factory/test_default_record_factory.py +++ b/tests/unit/release_notes_generator/record/factory/test_default_record_factory.py @@ -28,7 +28,7 @@ from release_notes_generator.model.mined_data import MinedData from release_notes_generator.model.record.pull_request_record import PullRequestRecord from release_notes_generator.record.factory.default_record_factory import DefaultRecordFactory -from tests.conftest import mock_safe_call_decorator +from tests.unit.conftest import mock_safe_call_decorator # generate - non hierarchy issue records diff --git a/tests/unit/release_notes_generator/record/factory/utils/__init__.py b/tests/unit/release_notes_generator/record/factory/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_action_inputs.py b/tests/unit/release_notes_generator/test_action_inputs.py similarity index 64% rename from tests/test_action_inputs.py rename to tests/unit/release_notes_generator/test_action_inputs.py index 88d213a3..a58b5ff9 100644 --- a/tests/test_action_inputs.py +++ b/tests/unit/release_notes_generator/test_action_inputs.py @@ -13,13 +13,11 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import logging +import logging import pytest - from release_notes_generator.action_inputs import ActionInputs -# Data-driven test cases success_case = { "get_github_repository": "owner/repo_name", "get_tag_name": "tag_name", @@ -27,7 +25,7 @@ "get_chapters": [{"title": "Title", "label": "Label"}], "get_hierarchy": False, "get_duplicity_scope": "custom", - "get_duplicity_icon": "🔁", + "get_duplicity_icon": "\U0001f501", "get_warnings": True, "get_published_at": False, "get_skip_release_notes_labels": ["skip"], @@ -59,7 +57,6 @@ ("get_hierarchy", "not_bool", "Hierarchy must be a boolean."), ] - def apply_mocks(case, mocker): patchers = [] for key, value in case.items(): @@ -68,12 +65,10 @@ def apply_mocks(case, mocker): patchers.append(patcher) return patchers - def stop_mocks(patchers): for patcher in patchers: patcher.stop() - def test_validate_inputs_success(mocker): patchers = apply_mocks(success_case, mocker) try: @@ -90,110 +85,88 @@ def test_validate_inputs_failure(method, value, expected_error, mocker): try: mock_error = mocker.patch("release_notes_generator.action_inputs.logger.error") mock_exit = mocker.patch("sys.exit") - ActionInputs.validate_inputs() - mock_error.assert_called_with(expected_error) mock_exit.assert_called_once_with(1) - finally: stop_mocks(patchers) - def test_get_github_repository(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="owner/repo") assert "owner/repo" == ActionInputs.get_github_repository() - def test_get_github_token(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="fake-token") assert ActionInputs.get_github_token() == "fake-token" - def test_get_tag_name_version_full(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="v1.0.0") assert ActionInputs.get_tag_name() == "v1.0.0" - def test_get_tag_name_version_shorted_with_v(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="v1.2") assert ActionInputs.get_tag_name() == "v1.2.0" - def test_get_tag_name_version_shorted_no_v(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="1.2") assert ActionInputs.get_tag_name() == "v1.2.0" - def test_get_tag_name_empty(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="") assert ActionInputs.get_tag_name() == "" - def test_get_tag_name_invalid_format(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="v1.2.beta") with pytest.raises(ValueError) as excinfo: ActionInputs.get_tag_name() assert "Invalid version tag format: 'v1.2.beta'. Expected vMAJOR.MINOR[.PATCH], e.g. 'v0.2' or 'v0.2.0'." in str(excinfo.value) - def test_get_tag_from_name_version_full(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="v1.0.0") assert ActionInputs.get_from_tag_name() == "v1.0.0" - def test_get_from_tag_name_version_shorted_with_v(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="v1.2") assert ActionInputs.get_from_tag_name() == "v1.2.0" - def test_get_from_tag_name_version_shorted_no_v(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="1.2") assert ActionInputs.get_from_tag_name() == "v1.2.0" - def test_get_from_tag_name_empty(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="") assert ActionInputs.get_from_tag_name() == "" - def test_get_from_tag_name_invalid_format(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="v1.2.beta") with pytest.raises(ValueError) as excinfo: ActionInputs.get_from_tag_name() assert "Invalid version tag format: 'v1.2.beta'. Expected vMAJOR.MINOR[.PATCH], e.g. 'v0.2' or 'v0.2.0'." in str(excinfo.value) - def test_get_chapters_success(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="[{\"title\": \"Title\", \"label\": \"Label\"}]") assert ActionInputs.get_chapters() == [{"title": "Title", "label": "Label"}] - def test_get_chapters_exception(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="wrong value") assert [] == ActionInputs.get_chapters() - def test_get_chapters_yaml_error(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="[{\"title\": \"Title\" \"label\": \"Label\"}]") assert [] == ActionInputs.get_chapters() - def test_get_warnings(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="true") assert ActionInputs.get_warnings() is True - def test_get_published_at(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="false") assert ActionInputs.get_published_at() is False - def test_get_skip_release_notes_label(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="skip-release-notes") assert ActionInputs.get_skip_release_notes_labels() == ["skip-release-notes"] - def test_get_skip_release_notes_label_not_defined(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="") assert ActionInputs.get_skip_release_notes_labels() == ["skip-release-notes"] @@ -202,56 +175,46 @@ def test_get_skip_release_notes_labels(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="skip-release-notes, another-skip-label") assert ActionInputs.get_skip_release_notes_labels() == ["skip-release-notes", "another-skip-label"] - def test_get_skip_release_notes_labels_no_space(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="skip-release-notes,another-skip-label") assert ActionInputs.get_skip_release_notes_labels() == ["skip-release-notes", "another-skip-label"] - def test_get_print_empty_chapters(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="true") assert ActionInputs.get_print_empty_chapters() is True - def test_get_verbose_verbose_by_action_input(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="true") mocker.patch("os.getenv", return_value=0) assert ActionInputs.get_verbose() is True - def test_get_verbose_verbose_by_workflow_debug(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="false") mocker.patch("os.getenv", return_value="1") assert ActionInputs.get_verbose() is True - def test_get_verbose_verbose_by_both(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="true") mocker.patch("os.getenv", return_value=1) assert ActionInputs.get_verbose() is True - def test_get_verbose_not_verbose(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="false") mocker.patch("os.getenv", return_value=0) assert ActionInputs.get_verbose() is False - def test_get_duplicity_scope_wrong_value(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="huh") mock_error = mocker.patch("release_notes_generator.action_inputs.logger.error") - assert ActionInputs.get_duplicity_scope() == "BOTH" mock_error.assert_called_with("Error: '%s' is not a valid DuplicityType.", "HUH") - def test_detect_row_format_invalid_keywords_no_invalid_keywords(caplog): caplog.set_level(logging.ERROR) row_format = "{number} _{title}_ in {pull-requests}" ActionInputs._detect_row_format_invalid_keywords(row_format) assert len(caplog.records) == 0 - def test_detect_row_format_invalid_keywords_with_invalid_keywords(caplog): caplog.set_level(logging.ERROR) row_format = "{number} _{title}_ in {pull-requests} {invalid_key} {another_invalid}" @@ -264,7 +227,6 @@ def test_detect_row_format_invalid_keywords_with_invalid_keywords(caplog): actual_errors = [record.getMessage() for record in caplog.records] assert actual_errors == expected_errors - def test_get_row_format_hierarchy_issue_cleans_invalid_keywords(mocker, caplog): caplog.set_level(logging.ERROR) mocker.patch( @@ -274,51 +236,41 @@ def test_get_row_format_hierarchy_issue_cleans_invalid_keywords(mocker, caplog): fmt = ActionInputs.get_row_format_hierarchy_issue() assert "{invalid}" not in fmt - def test_clean_row_format_invalid_keywords_no_keywords(): expected_row_format = "{number} _{title}_ in {pull-requests}" actual_format = ActionInputs._detect_row_format_invalid_keywords(expected_row_format, clean=True) assert expected_row_format == actual_format - def test_clean_row_format_invalid_keywords_nested_braces(): row_format = "{number} _{title}_ in {pull-requests} {invalid_key} {another_invalid}" expected_format = "{number} _{title}_ in {pull-requests} " actual_format = ActionInputs._detect_row_format_invalid_keywords(row_format, clean=True) assert expected_format == actual_format - def test_release_notes_title_default(): assert ActionInputs.get_release_notes_title() == "[Rr]elease [Nn]otes:" - def test_release_notes_title_custom(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="Custom Title") assert ActionInputs.get_release_notes_title() == "Custom Title" - def test_coderabbit_support_active_default(mocker): assert not ActionInputs.is_coderabbit_support_active() - def test_coderabbit_release_notes_title_default(): assert ActionInputs.get_coderabbit_release_notes_title() == "Summary by CodeRabbit" - def test_coderabbit_release_notes_title_custom(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="Custom Title") assert ActionInputs.get_coderabbit_release_notes_title() == "Custom Title" - def test_coderabbit_summary_ignore_groups_default(): assert ActionInputs.get_coderabbit_summary_ignore_groups() == [] - def test_coderabbit_summary_ignore_groups_custom(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value="Group1\nGroup2") assert ActionInputs.get_coderabbit_summary_ignore_groups() == ["Group1", "Group2"] - def test_coderabbit_summary_ignore_groups_int_input(mocker): mock_log_error = mocker.patch("release_notes_generator.action_inputs.logger.error") mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value=1) @@ -326,9 +278,145 @@ def test_coderabbit_summary_ignore_groups_int_input(mocker): mock_log_error.assert_called_once() assert "coderabbit_summary_ignore_groups' is not a valid string" in mock_log_error.call_args[0][0] - def test_coderabbit_summary_ignore_groups_empty_group_input(mocker): mocker.patch("release_notes_generator.action_inputs.get_action_input", return_value=",") # Note: this is not valid input which is catched by the validation_inputs() method assert ActionInputs.get_coderabbit_summary_ignore_groups() == ['', ''] +# Mirrored test file for release_notes_generator/generator.py +# Extracted from previous aggregated test_release_notes_generator.py + +import time +from datetime import datetime, timedelta +from github import Github +from release_notes_generator.generator import ReleaseNotesGenerator +from release_notes_generator.chapters.custom_chapters import CustomChapters +from release_notes_generator.utils.constants import ROW_FORMAT_ISSUE + +def test_generate_release_notes_repository_not_found(mocker): + github_mock = mocker.Mock(spec=Github) + github_mock.get_repo.return_value = None + mock_rate_limit = mocker.Mock() + mock_rate_limit.rate.remaining = 10 + mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600 + github_mock.get_rate_limit.return_value = mock_rate_limit + custom_chapters = CustomChapters(print_empty_chapters=True) + release_notes = ReleaseNotesGenerator(github_mock, custom_chapters).generate() + assert release_notes is None + +def test_generate_release_notes_latest_release_not_found( + mocker, + mock_repo, + mock_issue_closed, + mock_issue_closed_i1_bug, + mock_pull_closed_with_rls_notes_101, + mock_pull_closed_with_rls_notes_102, + mock_commit, +): + github_mock = mocker.Mock(spec=Github) + github_mock.get_repo.return_value = mock_repo + mock_repo.created_at = datetime.now() - timedelta(days=10) + mock_repo.get_issues.return_value = [mock_issue_closed, mock_issue_closed_i1_bug] + mock_repo.get_pulls.return_value = [mock_pull_closed_with_rls_notes_101, mock_pull_closed_with_rls_notes_102] + mock_repo.get_commits.return_value = [mock_commit] + mock_repo.get_release.return_value = None + mock_repo.get_releases.return_value = [] + mock_issue_closed.created_at = mock_repo.created_at + timedelta(days=2) + mock_issue_closed_i1_bug.created_at = mock_repo.created_at + timedelta(days=7) + mock_issue_closed_i1_bug.closed_at = mock_repo.created_at + timedelta(days=6) + mock_pull_closed_with_rls_notes_101.merged_at = mock_repo.created_at + timedelta(days=2) + mock_pull_closed_with_rls_notes_102.merged_at = mock_repo.created_at + timedelta(days=7) + mock_rate_limit = mocker.Mock() + mock_rate_limit.rate.remaining = 1000 + github_mock.get_rate_limit.return_value = mock_rate_limit + mocker.patch("release_notes_generator.record.factory.default_record_factory.get_issues_for_pr", return_value=None) + custom_chapters = CustomChapters(print_empty_chapters=True) + release_notes = ReleaseNotesGenerator(github_mock, custom_chapters).generate() + assert release_notes is not None + assert "- N/A: #121 _Fix the bug_" in release_notes + assert "- N/A: #122 _I1+bug_" in release_notes + assert "- PR: #101 _Fixed bug_" in release_notes + assert "- PR: #102 _Fixed bug_" in release_notes + +def test_generate_release_notes_latest_release_found_by_created_at( + mocker, + mock_repo, + mock_git_release, + mock_issue_closed_i1_bug, + mock_pull_closed_with_rls_notes_101, + mock_pull_closed_with_rls_notes_102, + mock_commit, +): + github_mock = mocker.Mock(spec=Github) + github_mock.get_repo.return_value = mock_repo + mock_repo.created_at = datetime.now() - timedelta(days=10) + mock_repo.published_at = datetime.now() - timedelta(days=9) + mock_repo.get_issues.return_value = [mock_issue_closed_i1_bug] + mock_repo.get_pulls.return_value = [mock_pull_closed_with_rls_notes_101, mock_pull_closed_with_rls_notes_102] + mock_repo.get_commits.return_value = [mock_commit] + mock_commit.commit.author.date = mock_repo.created_at + timedelta(days=1) + mock_repo.get_release.return_value = None + mock_repo.get_releases.return_value = [] + mock_issue_closed_i1_bug.created_at = mock_repo.created_at + timedelta(days=7) + mock_issue_closed_i1_bug.closed_at = mock_repo.created_at + timedelta(days=6) + mock_pull_closed_with_rls_notes_101.merged_at = mock_repo.created_at + timedelta(days=2) + mock_pull_closed_with_rls_notes_101.closed_at = mock_repo.created_at + timedelta(days=2) + mock_pull_closed_with_rls_notes_102.merged_at = mock_repo.created_at + timedelta(days=7) + mock_pull_closed_with_rls_notes_102.closed_at = mock_repo.created_at + timedelta(days=7) + mock_git_release.created_at = mock_repo.created_at + timedelta(days=5) + mock_git_release.published_at = mock_repo.created_at + timedelta(days=5) + mocker.patch("release_notes_generator.data.miner.DataMiner.get_latest_release", return_value=mock_git_release) + mock_rate_limit = mocker.Mock() + mock_rate_limit.rate.remaining = 1000 + github_mock.get_rate_limit.return_value = mock_rate_limit + mock_get_action_input = mocker.patch("release_notes_generator.utils.gh_action.get_action_input") + mock_get_action_input.side_effect = lambda first_arg, **kwargs: ( + "{number} _{title}_ in {pull-requests} {unknown} {another-unknown}" if first_arg == ROW_FORMAT_ISSUE else None + ) + mocker.patch("release_notes_generator.record.factory.default_record_factory.get_issues_for_pr", return_value=None) + custom_chapters = CustomChapters(print_empty_chapters=True) + release_notes = ReleaseNotesGenerator(github_mock, custom_chapters).generate() + assert release_notes is not None + assert "- N/A: #122 _I1+bug_" in release_notes + assert "- PR: #101 _Fixed bug_" not in release_notes + assert "- PR: #102 _Fixed bug_" in release_notes + +def test_generate_release_notes_latest_release_found_by_published_at( + mocker, + mock_repo, + mock_git_release, + mock_issue_closed_i1_bug, + mock_pull_closed_with_rls_notes_101, + mock_pull_closed_with_rls_notes_102, + mock_commit, +): + github_mock = mocker.Mock(spec=Github) + github_mock.get_repo.return_value = mock_repo + mock_repo.created_at = datetime.now() - timedelta(days=10) + mocker.patch("release_notes_generator.action_inputs.ActionInputs.get_published_at", return_value="true") + mock_repo.get_issues.return_value = [mock_issue_closed_i1_bug] + mock_repo.get_pulls.return_value = [mock_pull_closed_with_rls_notes_101, mock_pull_closed_with_rls_notes_102] + mock_repo.get_commits.return_value = [mock_commit] + mock_commit.commit.author.date = mock_repo.created_at + timedelta(days=1) + mock_repo.get_release.return_value = None + mock_repo.get_releases.return_value = [] + mock_issue_closed_i1_bug.created_at = mock_repo.created_at + timedelta(days=7) + mock_issue_closed_i1_bug.closed_at = mock_repo.created_at + timedelta(days=8) + mock_pull_closed_with_rls_notes_101.merged_at = mock_repo.created_at + timedelta(days=2) + mock_pull_closed_with_rls_notes_101.closed_at = mock_repo.created_at + timedelta(days=2) + mock_pull_closed_with_rls_notes_102.merged_at = mock_repo.created_at + timedelta(days=7) + mock_pull_closed_with_rls_notes_102.closed_at = mock_repo.created_at + timedelta(days=7) + github_mock.get_repo().get_latest_release.return_value = mock_git_release + mock_git_release.created_at = mock_repo.created_at + timedelta(days=5) + mock_git_release.published_at = mock_repo.created_at + timedelta(days=5) + mocker.patch("release_notes_generator.data.miner.DataMiner.get_latest_release", return_value=mock_git_release) + mock_rate_limit = mocker.Mock() + mock_rate_limit.rate.remaining = 1000 + github_mock.get_rate_limit.return_value = mock_rate_limit + mocker.patch("release_notes_generator.record.factory.default_record_factory.get_issues_for_pr", return_value=None) + custom_chapters = CustomChapters(print_empty_chapters=True) + release_notes = ReleaseNotesGenerator(github_mock, custom_chapters).generate() + assert release_notes is not None + assert "- N/A: #122 _I1+bug_" in release_notes + assert "- PR: #101 _Fixed bug_" not in release_notes + assert "- PR: #102 _Fixed bug_" in release_notes diff --git a/tests/test_release_notes_generator.py b/tests/unit/release_notes_generator/test_generator.py similarity index 92% rename from tests/test_release_notes_generator.py rename to tests/unit/release_notes_generator/test_generator.py index 19ab8c0b..74bda111 100644 --- a/tests/test_release_notes_generator.py +++ b/tests/unit/release_notes_generator/test_generator.py @@ -44,13 +44,13 @@ def test_generate_release_notes_repository_not_found(mocker): def test_generate_release_notes_latest_release_not_found( - mocker, - mock_repo, - mock_issue_closed, - mock_issue_closed_i1_bug, - mock_pull_closed_with_rls_notes_101, - mock_pull_closed_with_rls_notes_102, - mock_commit, + mocker, + mock_repo, + mock_issue_closed, + mock_issue_closed_i1_bug, + mock_pull_closed_with_rls_notes_101, + mock_pull_closed_with_rls_notes_102, + mock_commit, ): github_mock = mocker.Mock(spec=Github) github_mock.get_repo.return_value = mock_repo @@ -85,13 +85,13 @@ def test_generate_release_notes_latest_release_not_found( def test_generate_release_notes_latest_release_found_by_created_at( - mocker, - mock_repo, - mock_git_release, - mock_issue_closed_i1_bug, - mock_pull_closed_with_rls_notes_101, - mock_pull_closed_with_rls_notes_102, - mock_commit, + mocker, + mock_repo, + mock_git_release, + mock_issue_closed_i1_bug, + mock_pull_closed_with_rls_notes_101, + mock_pull_closed_with_rls_notes_102, + mock_commit, ): github_mock = mocker.Mock(spec=Github) github_mock.get_repo.return_value = mock_repo @@ -138,13 +138,13 @@ def test_generate_release_notes_latest_release_found_by_created_at( def test_generate_release_notes_latest_release_found_by_published_at( - mocker, - mock_repo, - mock_git_release, - mock_issue_closed_i1_bug, - mock_pull_closed_with_rls_notes_101, - mock_pull_closed_with_rls_notes_102, - mock_commit, + mocker, + mock_repo, + mock_git_release, + mock_issue_closed_i1_bug, + mock_pull_closed_with_rls_notes_101, + mock_pull_closed_with_rls_notes_102, + mock_commit, ): github_mock = mocker.Mock(spec=Github) github_mock.get_repo.return_value = mock_repo diff --git a/tests/release_notes/model/__init__.py b/tests/unit/release_notes_generator/utils/__init__.py similarity index 100% rename from tests/release_notes/model/__init__.py rename to tests/unit/release_notes_generator/utils/__init__.py diff --git a/tests/utils/test_decorators.py b/tests/unit/release_notes_generator/utils/test_decorators.py similarity index 100% rename from tests/utils/test_decorators.py rename to tests/unit/release_notes_generator/utils/test_decorators.py diff --git a/tests/utils/test_gh_action.py b/tests/unit/release_notes_generator/utils/test_gh_action.py similarity index 100% rename from tests/utils/test_gh_action.py rename to tests/unit/release_notes_generator/utils/test_gh_action.py diff --git a/tests/utils/test_github_rate_limiter.py b/tests/unit/release_notes_generator/utils/test_github_rate_limiter.py similarity index 100% rename from tests/utils/test_github_rate_limiter.py rename to tests/unit/release_notes_generator/utils/test_github_rate_limiter.py diff --git a/tests/utils/test_logging_config.py b/tests/unit/release_notes_generator/utils/test_logging_config.py similarity index 100% rename from tests/utils/test_logging_config.py rename to tests/unit/release_notes_generator/utils/test_logging_config.py diff --git a/tests/utils/test_pull_request_utils.py b/tests/unit/release_notes_generator/utils/test_pull_request_utils.py similarity index 100% rename from tests/utils/test_pull_request_utils.py rename to tests/unit/release_notes_generator/utils/test_pull_request_utils.py diff --git a/tests/utils/test_utils.py b/tests/unit/release_notes_generator/utils/test_utils.py similarity index 100% rename from tests/utils/test_utils.py rename to tests/unit/release_notes_generator/utils/test_utils.py diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py deleted file mode 100644 index 1b19ad1b..00000000 --- a/tests/utils/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# -# Copyright 2023 ABSA Group Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#