diff --git a/.github/workflows/static_analysis_and_tests.yml b/.github/workflows/static_analysis_and_tests.yml index dd311d9..86dad83 100644 --- a/.github/workflows/static_analysis_and_tests.yml +++ b/.github/workflows/static_analysis_and_tests.yml @@ -83,8 +83,6 @@ jobs: - name: Install dependencies run: | pip install -r requirements.txt - - name: Set PYTHONPATH environment variable - run: echo "PYTHONPATH=${GITHUB_WORKSPACE}/living_documentation_generator/living_documentation_generator" >> $GITHUB_ENV - name: Check code coverage with Pytest run: pytest --cov=. -v tests/ --cov-fail-under=80 diff --git a/README.md b/README.md index 6c66b96..ce7e387 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,43 @@ # Living Documentation Generator - [Motivation](#motivation) +- [Mining Regimes](#mining-regimes) - [Usage](#usage) - [Prerequisites](#prerequisites) - [Adding the Action to Your Workflow](#adding-the-action-to-your-workflow) - [Action Configuration](#action-configuration) - [Environment Variables](#environment-variables) - [Inputs](#inputs) - - [Features de/activation](#features-deactivation) + - [Base Inputs](#base-inputs) + - [Regime Inputs](#regime-inputs) - [Action Outputs](#action-outputs) -- [Expected Output](#expected-output) - - [Index page example](#index-page-example) - - [Issue page example](#issue-page-example) -- [Documentation Ticket Introduction](#documentation-ticket-introduction) - - [Labels](#labels) - - [Hosting Documentation Tickets in a Solo Repository](#hosting-documentation-tickets-in-a-solo-repository) - [Project Setup](#project-setup) - [Run Scripts Locally](#run-scripts-locally) - [Run Pylint Check Locally](#run-pylint-check-locally) - [Run Black Tool Locally](#run-black-tool-locally) - [Run Unit Test](#run-unit-test) - [Code Coverage](#code-coverage) -- [Deployment](#deployment) -- [Features](#features) - - [Data Mining from GitHub Repositories](#data-mining-from-github-repositories) - - [Data Mining from GitHub Projects](#data-mining-from-github-projects) - - [Living Documentation Page Generation](#living-documentation-page-generation) - - [Structured Output](#structured-output) - - [Output Grouped by Topics](#output-grouped-by-topics) +- [Releasing](#releasing) +- [Support](#support) + - [How to Create a Token with Required Scope](#how-to-create-a-token-with-required-scope) + - [How to Store Token as a Secret](#how-to-store-token-as-a-secret) - [Contribution Guidelines](#contribution-guidelines) -- [License Information](#license-information) -- [Contact or Support Information](#contact-or-support-information) + - [License Information](#license-information) + - [Contact or Support Information](#contact-or-support-information) ![vision.jpg](img/vision.jpg) -A tool designed to data-mine GitHub repositories for [documentation tickets](#documentation-ticket-introduction) containing project documentation (e.g. tagged with feature-related labels). This tool automatically generates comprehensive living documentation in Markdown format, providing detailed feature overview pages and in-depth feature descriptions. - ## Motivation Addresses the need for continuously updated documentation accessible to all team members and stakeholders. Achieves this by extracting information directly from GitHub issues and integrating this functionality to deliver real-time, markdown-formatted output. Ensures everyone has the most current project details, fostering better communication and efficiency throughout development. +--- +## Mining Regimes + +This Generator supports multiple mining regimes, each with its own unique functionality. Read more about the regimes at their respective links: +- [Living documentation regime documentation](living_documentation_regime/README.md) + +--- ## Usage ### Prerequisites @@ -53,43 +51,34 @@ See the default action step definition: ```yaml - name: Generate Living Documentation id: generate_living_doc - uses: AbsaOSS/living-doc-generator@v0.1.0 + uses: AbsaOSS/living-doc-generator@v0.3.0 env: - GITHUB_TOKEN: ${{ secrets.LIV_DOC_ACCESS_TOKEN }} + GITHUB-TOKEN: ${{ secrets.REPOSITORIES_ACCESS_TOKEN }} with: - repositories: '[ - { - "organization-name": "fin-services", - "repository-name": "investment-app", - "query-labels": ["feature", "enhancement"], - "projects-title-filter": [] - }, - { - "organization-name": "health-analytics", - "repository-name": "patient-data-analysis", - "query-labels": ["functionality"], - "projects-title-filter": ["Health Data Analysis Project"] - }, - { - "organization-name": "open-source-initiative", - "repository-name": "community-driven-project", - "query-labels": ["improvement"], - "projects-title-filter": ["Community Outreach Initiatives", "CDD Project"] - } - ]' + # regimes de/activation + liv-doc-regime: false ``` +See the default action step definitions for each regime: + +- [Living documentation regime default step definition](living_documentation_regime/README.md#adding-livdoc-regime-to-the-workflow) + +#### Full Example of Action Step Definition + See the full example of action step definition (in example are used non-default values): ```yaml - name: Generate Living Documentation id: generate_living_doc - uses: AbsaOSS/living-doc-generator@v0.1.0 + uses: AbsaOSS/living-doc-generator@v0.3.0 env: - GITHUB_TOKEN: ${{ secrets.LIV_DOC_ACCESS_TOKEN }} + GITHUB-TOKEN: ${{ secrets.REPOSITORIES_ACCESS_TOKEN }} with: - # input repositories + feature to filter projects - repositories: '[ + liv-doc-regime: true # living documentation regime de/activation + verbose-logging: true # project verbose (debug) logging feature de/activation + + # LivDoc Regime configuration + liv-doc-repositories: '[ { "organization-name": "fin-services", "repository-name": "investment-app", @@ -109,131 +98,45 @@ See the full example of action step definition (in example are used non-default "projects-title-filter": ["Community Outreach Initiatives", "CDD Project"] } ]' - - # output directory path for generated documentation - output-path: "/output/directory/path" - - # project state mining feature de/activation - project-state-mining: true - - # structured output feature de/activation - structured-output: true - - # group output by topics feature de/activation - group-output-by-topics: true - - # project verbose (debug) logging feature de/activation - verbose-logging: true + liv-doc-project-state-mining: true # project state mining feature de/activation + liv-doc-structured-output: true # structured output feature de/activation + liv-doc-group-output-by-topics: true # group output by topics feature de/activation ``` +--- ## Action Configuration +This section outlines the essential parameters that are common to all regimes a user can define. + Configure the action by customizing the following parameters based on your needs: ### Environment Variables -- **LIV_DOC_ACCESS_TOKEN** (required): - - **Description**: GitHub access token for authentication, that has a permission to fetch from requested repositories. - - **Usage**: Store it in the GitHub repository secrets and reference it in the workflow file using `${{ secrets.LIV_DOC_ACCESS_TOKEN }}`. +- **REPOSITORIES_ACCESS_TOKEN**: + - **Description**: GitHub access token for authentication, that has a permission to access all defined repositories / projects. + - **Usage**: Store it in the GitHub repository secrets and reference it in the workflow file using `${{ secrets.REPOSITORIES_ACCESS_TOKEN }}`. - **Example**: ```yaml env: - GITHUB_TOKEN: ${{ secrets.LIV_DOC_ACCESS_TOKEN }} + GITHUB-TOKEN: ${{ secrets.REPOSITORIES_ACCESS_TOKEN }} ``` -#### How to Create a Token with Required Scope - -1. Go to your GitHub account settings. -2. Click on the `Developer settings` tab in the left sidebar. -3. In the left sidebar, click on `Personal access tokens` and choose `Tokens (classic)`. -4. Click on the `Generate new token` button and choose `Generate new token (classic)`. -5. Optional - Add a note, what is token for and choose token expiration. -6. Select ONLY bold scope options below: - - **workflow** - - write:packages - - **read:packages** - - admin:org - - **read:org** - - **manage_runners:org** - - admin:public_key - - **read:public_key** - - admin:repo_hook - - **read:repo_hook** - - admin:enterprise - - **manage_runners:enterprise** - - **read:enterprise** - - audit_log - - **read:audit_log** - - project - - **read:project** -7. Copy the token value somewhere, because you won't be able to see it again. -8. Authorize new token to organization you want to fetch from. - -#### How to Store Token as a Secret - -1. Go to the GitHub repository, from which you want to run the GitHub Action. -2. Click on the `Settings` tab in the top bar. -3. In the left sidebar, click on `Secrets and variables` > `Actions`. -4. Click on the `New repository secret` button. -5. Name the token `LIV_DOC_ACCESS_TOKEN` and paste the token value. +The way how to generate and store a token into the GitHub repository secrets is described in the [support chapter](#how-to-create-a-token-with-required-scope). ### Inputs -- **repositories** (required) - - **Description**: A JSON string defining the repositories to be included in the documentation generation. - - **Usage**: List each repository with its organization name, repository name, query labels and attached projects you want to filter if any. Only projects with these titles will be considered. For no filtering projects, leave the list empty. - - **Example**: - ```yaml - with: - repositories: '[ - { - "organization-name": "fin-services", - "repository-name": "investment-app", - "query-labels": ["feature", "enhancement"], - "projects-title-filter": [] - }, - { - "organization-name": "health-analytics", - "repository-name": "patient-data-analysis", - "query-labels": ["functionality"], - "projects-title-filter": ["Health Data Analysis Project"] - }, - { - "organization-name": "open-source-initiative", - "repository-name": "community-driven-project", - "query-labels": ["improvement"], - "projects-title-filter": ["Community Outreach Initiatives", "CDD Project"] - } - ]' - ``` - -### Features De/Activation - -- **project-state-mining** (optional, `default: false`) - - **Description**: Enables or disables the mining of project state data from [GitHub Projects](https://docs.github.com/en/issues/planning-and-tracking-with-projects/learning-about-projects/about-projects). - - **Usage**: Set to true to activate. - - **Example**: - ```yaml - with: - project-state-mining: true - ``` - -- **structured-output** (optional, `default: false`) - - **Description**: Enables or disables structured output. - - **Usage**: Set to true to activate. - - **Example**: - ```yaml - with: - structured-output: true - ``` +In this GitHub action, there are two types of user inputs: +- **Base Inputs**: Inputs that are common to all regimes. +- **Regime Inputs**: Inputs that are specific to a particular regime. -- **group-output-by-topics** (optional, `default: false`) - - **Description**: Enable or disable grouping tickets by topics in the summary index.md file. +#### Base Inputs +- **liv-doc-regime** (required) + - **Description**: Enables or disables Living Documentation regime. - **Usage**: Set to true to activate. - **Example**: ```yaml with: - group-output-by-topics: true + liv-doc-regime: true ``` - **verbose-logging** (optional, `default: false`) @@ -244,7 +147,13 @@ Configure the action by customizing the following parameters based on your needs with: verbose-logging: true ``` + +#### Regime Inputs +Regime-specific inputs are detailed in the respective regime's documentation: +- [Living documentation regime specific inputs](living_documentation_regime/README.md#regime-configuration) + +--- ## Action Outputs The Living Documentation Generator action provides a key output that allows users to locate and access the generated documentation easily. This output can be utilized in various ways within your CI/CD pipeline to ensure the documentation is effectively distributed and accessible. @@ -261,105 +170,11 @@ The output-path can not be an empty string. It can not aim to the root and other - name: Output Documentation Path run: echo "Generated documentation path: ${{ steps.generate_doc.outputs.output-path }}" ``` - -## Expected Output - -The Living Documentation Generator is designed to produce an Issue Summary page (index.md) along with multiple detailed single issue pages. - -### Index Page Example - - -```markdown -# Issue Summary page - -Our project is designed with a myriad of issues to ensure seamless user experience, top-tier functionality, and efficient operations. -Here, you'll find a summarized list of all these issues, their brief descriptions, and links to their detailed documentation. - -## Issue Overview - -| Organization name| Repository name | Issue 'Number - Title' | Linked to project | Project Status | Issue URL | -|------------------|----------------------------|--------------------------------|-------------------|----------------|------------| -| AbsaOSS | living-doc-example-project | [#89 - Test issue 2](89_test_issue_2.md) | 🔴 | --- |[GitHub](#) | -| AbsaOSS | living-doc-example-project | [#88 - Test issue](88_test_issue.md) | 🟢 | Todo |[GitHub](#) | -| AbsaOSS | living-doc-example-project | [#41 - Initial commit.](41_initial_commit.md) | 🟢 | Done |[GitHub](#) | -| AbsaOSS | living-doc-example-project | [#33 - Example bugfix](33_example_bugfix.md) | 🔴 | --- |[GitHub](#) | -``` - -- **Project Status** can have various values depending on the project, such as: Todo, Done, Closed, In Progress, In Review, Blocked, etc. -These values can vary from project to project. -- The `---` symbol is used to indicate that an issue has no required project data. - -### Issue Page Example - -```markdown -# FEAT: Advanced Book Search -| Attribute | Content | -|-------------------|---------------------------------------| -| Organization name | AbsaOSS | -| Repository name | living-doc-example-project | -| Issue number | 17 | -| State | open | -| Issue URL | [GitHub link](#) | -| Created at | 2023-12-12 11:34:52 | -| Updated at | 2023-12-13 10:24:58 | -| Closed at | None | -| Labels | feature | -| Project title | Book Store Living Doc Example project | -| Status | Todo | -| Priority | P1 | -| Size | S | -| MoSCoW | N/A | - -## Issue Content -Users often struggle to find specific books in a large catalog. An advanced search feature would streamline this process, enhancing user experience. - -### Background -... -``` - -## Documentation Ticket Introduction - -A **Documentation Ticket** is a small piece of documentation realised as GitHub Issue dedicated to project documentation. Unlike development-focused tickets, Documentation Ticket remain open continuously, evolving as updates are needed, and can be reopened or revised indefinitely. They are not directly tied to Pull Requests (PRs) but can be referenced for context. - -- **Content Rules**: - - **Non-technical Focus:** - - Keep the documentation body free of technical solution specifics. - - Technical insights should be accessible through linked PRs or Tickets within the development repository. - - **Independent Documentation:** - - Ensure the content remains independent of implementation details to allow a clear, high-level view of the feature or user story's purpose and functionality. - -### Labels - -To enhance clarity, the following label groups define and categorize each Documentation Issue: -- **Topic**: - - **{Name}Topic:** Designates the main focus area or theme relevant to the ticket, assigned by the editor for consistency across related documentation. - - Examples: `ReportingTopic`, `UserManagementTopic`, `SecurityTopic`. - - **NoTopic:** Indicates that the ticket does not align with a specific topic, based on the editor's discretion. -- **Type**: - - **DocumentedUserStory:** Describes a user-centric functionality or process, highlighting its purpose and value. - - Encompasses multiple features, capturing the broader goal from a user perspective. - - **DocumentedFeature:** Details a specific feature, providing a breakdown of its components and intended outcomes. - - Built from various requirements and can relate to multiple User Stories, offering an in-depth look at functionality. - - **DocumentedRequirement:** Outlines individual requirements or enhancements tied to the feature or user story. -- **Issue States**: - - **Upcoming:** The feature, story, or requirement is planned but not yet implemented. - - **Implemented:** The feature or requirement has been completed and is in active use. - - **Deprecated:** The feature or requirement has been phased out or replaced and is no longer supported. - -**DocumentedUserStory** and **DocumentedFeature** serve as **Epics**, whereas **DocumentedRequirement** represents specific items similar to feature enhancements or individual requirements. - -### Hosting Documentation Tickets in a Solo Repository - -Using a dedicated repository solely for documentation tickets provides multiple advantages: -- **Streamlined Management:** This avoids cross-project conflicts and board exclusions and enables specialized templates solely for documentation purposes. -- **Focused Access Control:** This allows a small team to manage and edit documentation without interference, maintaining high-quality content. -- **Optimized Data Mining:** Supports easier and more efficient data extraction for feedback and review cycles through Release Notes. -- **Implementation Reflection:** Mirrors elements from the implementation repositories, providing a high-level knowledge source that is valuable for both business and technical teams. -- **Release Notes Integration:** Documentation can evolve based on insights from release notes, serving as a dynamic feedback loop back to the documentation repository. +--- ## Project Setup -If you need to build the action locally, follow these steps: +If you need to build the action locally, follow these steps for project setup: ### Prepare the Environment @@ -375,6 +190,7 @@ source venv/bin/activate pip install -r requirements.txt ``` +--- ## Run Scripts Locally If you need to run the scripts locally, follow these steps: @@ -392,12 +208,17 @@ Add the shebang line at the top of the sh script file. ### Set the Environment Variables -Set the configuration environment variables in the shell script following the structure below. +Set the configuration environment variables in the shell script following the structure below. +The generator supports mining in multiple regimes, so you can use just the environment variables you need. Also make sure that the INPUT_GITHUB_TOKEN is configured in your environment variables. -INPUT_OUTPUT_PATH can not be an empty string. It can not aim to the root and other project directories as well. ``` +# Essential environment variables for GitHub Action functionality export INPUT_GITHUB_TOKEN=$(printenv GITHUB_TOKEN) -export INPUT_REPOSITORIES='[ +export INPUT_LIV_DOC_REGIME=true +export INPUT_VERBOSE_LOGGING=true + +# Environment variables for LivDoc regime functionality +export INPUT_LIV_DOC_REPOSITORIES='[ { "organization-name": "Organization Name", "repository-name": "example-project", @@ -405,11 +226,9 @@ export INPUT_REPOSITORIES='[ "projects-title-filter": ["Project Title 1"] } ]' -export INPUT_OUTPUT_PATH="/output/directory/path -export INPUT_PROJECT_STATE_MINING="true" -export INPUT_STRUCTURED_OUTPUT="true" -export INPUT_GROUP_OUTPUT_BY_TOPICS="true" -export INPUT_VERBOSE_LOGGING="true" +export INPUT_LIV_DOC_PROJECT_STATE_MINING=true +export INPUT_LIV_DOC_STRUCTURED_OUTPUT=true +export INPUT_LIV_DOC_GROUP_OUTPUT_BY_TOPICS=true ``` ### Running the script locally @@ -436,6 +255,7 @@ chmod +x run_script.sh ./run_script.sh ``` +--- ## Run Pylint Check Locally This project uses [Pylint](https://pypi.org/project/pylint/) tool for static code analysis. @@ -471,7 +291,7 @@ To run Pylint on a specific file, follow the pattern `pylint // `Actions`. +4. Click on the `New repository secret` button. +5. Name the token `REPOSITORIES_ACCESS_TOKEN` and paste the token value. -- **Default Behavior**: By default, the action generates all the documentation in a single directory. -- **Non-default Example**: Use the grouped output feature to organize the generated documentation by topics. - - `group-output-by-topics: true` activates the grouped output feature. - ``` - output - |- topic 1 - |-- issue md page 1 - |-- issue md page 2 - |-- _index.md - |- topic 2 - |-- issue md page 1 - |-- _index.md - |- _index.md - ``` - --- - ## Contribution Guidelines We welcome contributions to the Living Documentation Generator! Whether you're fixing bugs, improving documentation, or proposing new features, your help is appreciated. diff --git a/action.yml b/action.yml index d1bd9b8..3652f0d 100644 --- a/action.yml +++ b/action.yml @@ -1,35 +1,38 @@ name: 'Living Documentation Generator' description: 'Generates living documentation from current state of user defined GitHub repositories.' inputs: - GITHUB_TOKEN: + # Base action inputs + GITHUB-TOKEN: description: 'GitHub token for authentication.' required: true - repositories: - description: 'JSON string defining the repositories to be included in the documentation generation.' + liv-doc-regime: + description: 'Enable or disable the LivDoc regime.' required: true - project-state-mining: - description: 'Enable or disable mining of project state data.' - required: false - default: 'false' verbose-logging: description: 'Enable or disable verbose logging.' required: false default: 'false' - output-path: - description: 'Path to the generated living documentation files.' + + # LivDoc-regime action inputs + liv-doc-repositories: + description: 'JSON string defining the repositories to be included in the documentation generation.' required: false - default: './output' - structured-output: + default: '[]' + liv-doc-project-state-mining: + description: 'Enable or disable mining of project state data.' + required: false + default: 'false' + liv-doc-structured-output: description: 'Enable or disable structured output.' required: false default: 'false' - group-output-by-topics: + liv-doc-group-output-by-topics: description: 'Enable or disable grouping tickets by topics in the summary index.md file.' required: false default: 'false' outputs: output-path: - description: 'Path to the generated living documentation files' + description: 'Path to the generated living documentation files.' value: ${{ steps.liv-doc-generator.outputs.output-path }} branding: @@ -51,16 +54,32 @@ runs: export PYTHONPATH="${PYTHONPATH}:${ACTION_ROOT}/living-doc-generator" shell: bash - - name: Call Living Documentation Generator + - name: Prepare environment based on mining regimes + run: | + # Set base env variables common for all regimes + echo "INPUT_GITHUB_TOKEN=${{ env.GITHUB-TOKEN }}" >> $GITHUB_ENV + echo "INPUT_LIV_DOC_REGIME=${{ inputs.liv-doc-regime }}" >> $GITHUB_ENV + echo "INPUT_VERBOSE_LOGGING=${{ inputs.verbose-logging }}" >> $GITHUB_ENV + + # Add LivDoc-specific env variables if the regime is enabled + if [[ "${{ inputs.liv-doc-regime }}" == "true" ]]; then + echo "INPUT_LIV_DOC_REPOSITORIES=$(echo '${{ inputs.liv-doc-repositories }}' | jq -c .)" >> $GITHUB_ENV + echo "INPUT_LIV_DOC_PROJECT_STATE_MINING=${{ inputs.liv-doc-project-state-mining }}" >> $GITHUB_ENV + echo "INPUT_LIV_DOC_STRUCTURED_OUTPUT=${{ inputs.liv-doc-structured-output }}" >> $GITHUB_ENV + echo "INPUT_LIV_DOC_GROUP_OUTPUT_BY_TOPICS=${{ inputs.liv-doc-group-output-by-topics }}" >> $GITHUB_ENV + fi + shell: bash + + - name: Run Living Documentation Generator id: liv-doc-generator env: - INPUT_GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }} - INPUT_REPOSITORIES: ${{ inputs.repositories }} - INPUT_PROJECT_STATE_MINING: ${{ inputs.project-state-mining }} - INPUT_VERBOSE_LOGGING: ${{ inputs.verbose-logging }} - INPUT_OUTPUT_PATH: ${{ inputs.output-path }} - INPUT_STRUCTURED_OUTPUT: ${{ inputs.structured-output }} - + INPUT_GITHUB_TOKEN: ${{ env.INPUT_GITHUB_TOKEN }} + INPUT_LIV_DOC_REGIME: ${{ env.INPUT_LIV_DOC_REGIME }} + INPUT_VERBOSE_LOGGING: ${{ env.INPUT_VERBOSE_LOGGING }} + INPUT_LIV_DOC_REPOSITORIES: ${{ env.INPUT_LIV_DOC_REPOSITORIES }} + INPUT_LIV_DOC_PROJECT_STATE_MINING: ${{ env.INPUT_LIV_DOC_PROJECT_STATE_MINING }} + INPUT_LIV_DOC_STRUCTURED_OUTPUT: ${{ env.INPUT_LIV_DOC_STRUCTURED_OUTPUT }} + INPUT_LIV_DOC_GROUP_OUTPUT_BY_TOPICS: ${{ env.INPUT_LIV_DOC_GROUP_OUTPUT_BY_TOPICS }} run: | python ${{ github.action_path }}/main.py shell: bash diff --git a/img/vision.jpg b/img/vision.jpg index 9b6bfb7..d09a747 100644 Binary files a/img/vision.jpg and b/img/vision.jpg differ diff --git a/living_documentation_regime/README.md b/living_documentation_regime/README.md new file mode 100644 index 0000000..c56ff1d --- /dev/null +++ b/living_documentation_regime/README.md @@ -0,0 +1,349 @@ +# Living Documentation Regime + +- [Regime De/Activation](#regime-deactivation) +- [Adding Living Documentation Regime to the Workflow](#adding-living-documentation-regime-to-the-workflow) +- [Regime Configuration](#regime-configuration) + - [Regime Inputs](#regime-inputs) +- [Expected Output](#expected-output) + - [Index Page Example](#index-page-example) + - [Issue Page Example](#issue-page-example) +- [Documentation Ticket Introduction](#documentation-ticket-introduction) + - [Labels](#labels) + - [Hosting Documentation Tickets in a Solo Repository](#hosting-documentation-tickets-in-a-solo-repository) +- [Living Documentation Regime Features](#living-documentation-regime-features) + - [Data Mining from GitHub Repositories](#data-mining-from-github-repositories) + - [Data Mining from GitHub Projects](#data-mining-from-github-projects) + - [Living Documentation Page Generation](#living-documentation-page-generation) + - [Structured Output](#structured-output) + - [Output Grouped by Topics](#output-grouped-by-topics) + +This regime is designed to data-mine GitHub repositories for [documentation tickets](#documentation-ticket-introduction) containing project documentation (e.g. tagged with feature-related labels). This tool automatically generates comprehensive living documentation in Markdown format, providing detailed feature overview pages and in-depth feature descriptions. + +## Regime De/Activation + +- **liv-doc-regime** (required) + - **Description**: Enables or disables the Living Documentation regime. + - **Usage**: Set to true to activate. + - **Example**: + ```yaml + with: + liv-doc-regime: true + ``` + +--- +## Adding Living Documentation Regime to the Workflow + +See the default minimal Living Documentation regime action step definition: + +```yaml +- name: Generate Living Documentation + id: generate_living_doc + uses: AbsaOSS/living-doc-generator@v0.3.0 + env: + GITHUB-TOKEN: ${{ secrets.REPOSITORIES_ACCESS_TOKEN }} + with: + liv-doc-regime: true # living documentation regime de/activation + liv-doc-repositories: '[ + { + "organization-name": "fin-services", + "repository-name": "investment-app", + "query-labels": ["feature", "enhancement"], + "projects-title-filter": [] + }, + { + "organization-name": "health-analytics", + "repository-name": "patient-data-analysis", + "query-labels": ["functionality"], + "projects-title-filter": ["Health Data Analysis Project"] + }, + { + "organization-name": "open-source-initiative", + "repository-name": "community-driven-project", + "query-labels": ["improvement"], + "projects-title-filter": ["Community Outreach Initiatives", "CDD Project"] + } + ]' +``` + +See the full example of Living Documentation regime step definition (in example are used non-default values): + +```yaml +- name: Generate Living Documentation + id: generate_living_doc + uses: AbsaOSS/living-doc-generator@v0.3.0 + env: + GITHUB-TOKEN: ${{ secrets.REPOSITORIES_ACCESS_TOKEN }} + with: + liv-doc-regime: true # living documentation regime de/activation + verbose-logging: true # project verbose (debug) logging feature de/activation + + liv-doc-repositories: '[ + { + "organization-name": "fin-services", + "repository-name": "investment-app", + "query-labels": ["feature", "enhancement"], + "projects-title-filter": [] + }, + { + "organization-name": "health-analytics", + "repository-name": "patient-data-analysis", + "query-labels": ["functionality"], + "projects-title-filter": ["Health Data Analysis Project"] + }, + { + "organization-name": "open-source-initiative", + "repository-name": "community-driven-project", + "query-labels": ["improvement"], + "projects-title-filter": ["Community Outreach Initiatives", "CDD Project"] + } + ]' + liv-doc-project-state-mining: true # project state mining feature de/activation + liv-doc-structured-output: true # structured output feature de/activation + liv-doc-group-output-by-topics: true # group output by topics feature de/activation +``` + +--- +## Regime Configuration + +Configure the Living Documentation regime by customizing the following parameters based on your needs: + +### Regime Inputs +- **liv-doc-repositories** (optional, `default: '[]'`) + - **Description**: A JSON string defining the repositories to be included in the documentation generation. + - **Usage**: List each repository with its organization name, repository name, query labels and attached projects you want to filter if any. Only projects with these titles will be considered. For no filtering projects, leave the list empty. + - **Example**: + ```yaml + with: + liv-doc-repositories: '[ + { + "organization-name": "fin-services", + "repository-name": "investment-app", + "query-labels": ["feature", "enhancement"], + "projects-title-filter": [] + }, + { + "organization-name": "health-analytics", + "repository-name": "patient-data-analysis", + "query-labels": ["functionality"], + "projects-title-filter": ["Health Data Analysis Project"] + }, + { + "organization-name": "open-source-initiative", + "repository-name": "community-driven-project", + "query-labels": ["improvement"], + "projects-title-filter": ["Community Outreach Initiatives", "CDD Project"] + } + ]' + ``` + +- **liv-doc-project-state-mining** (optional, `default: false`) + - **Input description**: Enables or disables the mining of project state data from [GitHub Projects](https://docs.github.com/en/issues/planning-and-tracking-with-projects/learning-about-projects/about-projects). + - **Feature description**: [Data mining from GitHub projects](#data-mining-from-github-projects) + - **Usage**: Set to true to activate. + - **Example**: + ```yaml + with: + liv-doc-project-state-mining: true + ``` + +- **liv-doc-structured-output** (optional, `default: false`) + - **Input description**: Enables or disables structured output. + - **Feature description**: [Structured output](#structured-output) + - **Usage**: Set to true to activate. + - **Example**: + ```yaml + with: + liv-doc-structured-output: true + ``` + +- **liv-doc-group-output-by-topics** (optional, `default: false`) + - **Input description**: Enable or disable grouping tickets by topics in the summary index.md file. + - **Feature description**: [Output grouped by topics](#output-grouped-by-topics) + - **Usage**: Set to true to activate. + - **Example**: + ```yaml + with: + liv-doc-group-output-by-topics: true + ``` + +--- +## Expected Output + +The Living Documentation Generator in Living Documentation regime is designed to produce an Issue Summary page (index.md) along with multiple detailed single issue pages. + +### Index Page Example + +```markdown +# Issue Summary page + +Our project is designed with a myriad of issues to ensure seamless user experience, top-tier functionality, and efficient operations. +Here, you'll find a summarized list of all these issues, their brief descriptions, and links to their detailed documentation. + +## Issue Overview + +| Organization name| Repository name | Issue 'Number - Title' | Linked to project | Project Status | Issue URL | +|------------------|----------------------------|--------------------------------|-------------------|----------------|------------| +| AbsaOSS | living-doc-example-project | [#89 - Test issue 2](89_test_issue_2.md) | 🔴 | --- |[GitHub](#) | +| AbsaOSS | living-doc-example-project | [#88 - Test issue](88_test_issue.md) | 🟢 | Todo |[GitHub](#) | +| AbsaOSS | living-doc-example-project | [#41 - Initial commit.](41_initial_commit.md) | 🟢 | Done |[GitHub](#) | +| AbsaOSS | living-doc-example-project | [#33 - Example bugfix](33_example_bugfix.md) | 🔴 | --- |[GitHub](#) | +``` + +- **Project Status** can have various values depending on the project, such as: Todo, Done, Closed, In Progress, In Review, Blocked, etc. +These values can vary from project to project. +- The `---` symbol is used to indicate that an issue has no required project data. + +### Issue Page Example + +```markdown +# FEAT: Advanced Book Search +| Attribute | Content | +|-------------------|---------------------------------------| +| Organization name | AbsaOSS | +| Repository name | living-doc-example-project | +| Issue number | 17 | +| State | open | +| Issue URL | [GitHub link](#) | +| Created at | 2023-12-12 11:34:52 | +| Updated at | 2023-12-13 10:24:58 | +| Closed at | None | +| Labels | feature | +| Project title | Book Store Living Doc Example project | +| Status | Todo | +| Priority | P1 | +| Size | S | +| MoSCoW | N/A | + +## Issue Content +Users often struggle to find specific books in a large catalog. An advanced search feature would streamline this process, enhancing user experience. + +### Background +... +``` + +--- +## Documentation Ticket Introduction + +A **Documentation Ticket** is a small piece of documentation realized as a GitHub Issue dedicated to project documentation. Unlike development-focused tickets, Documentation Ticket can remain in open state continuously, evolving as updates are needed, and can be reopened or revised indefinitely. They are not directly tied to Pull Requests (PRs) but can be referenced for context. + +- **Content Rules**: + - **Non-technical Focus:** + - Keep the documentation body free of technical solution specifics. + - Technical insights should be accessible through linked PRs or Tickets within the development repository. + - **Independent Documentation:** + - Ensure the content remains independent of implementation details to allow a clear, high-level view of the feature or user story's purpose and functionality. + +### Labels + +To enhance clarity, the following label groups define and categorize each Documentation Issue: +- **Topic**: + - **{Name}Topic:** Designates the main focus area or theme relevant to the ticket, assigned by the editor for consistency across related documentation. + - Examples: `ReportingTopic`, `UserManagementTopic`, `SecurityTopic`. + - **NoTopic:** Indicates that the ticket does not align with a specific topic, based on the editor's discretion. +- **Type**: + - **DocumentedUserStory:** Describes a user-centric functionality or process, highlighting its purpose and value. + - Encompasses multiple features, capturing the broader goal from a user perspective. + - **DocumentedFeature:** Details a specific feature, providing a breakdown of its components and intended outcomes. + - Built from various requirements and can relate to multiple User Stories, offering an in-depth look at functionality. + - **DocumentedRequirement:** Outlines individual requirements or enhancements tied to the feature or user story. +- **Issue States**: + - **Upcoming:** The feature, story, or requirement is planned but not yet implemented. + - **Implemented:** The feature or requirement has been completed and is in active use. + - **Deprecated:** The feature or requirement has been phased out or replaced and is no longer supported. + +**DocumentedUserStory** and **DocumentedFeature** serve as **Epics**, whereas **DocumentedRequirement** represents specific items similar to feature enhancements or individual requirements. + +### Hosting Documentation Tickets in a Solo Repository + +Using a dedicated repository solely for documentation tickets provides multiple advantages: +- **Streamlined Management:** This avoids cross-project conflicts, board exclusions and enables specialized templates solely for documentation purposes.- **Focused Access Control:** This allows a small team to manage and edit documentation without interference, maintaining high-quality content. +- **Optimized Data Mining:** Supports easier and more efficient data extraction for feedback and review cycles through Release Notes. +- **Implementation Reflection:** Mirrors elements from the implementation repositories, providing a high-level knowledge source that is valuable for both business and technical teams. +- **Release Notes Integration:** Documentation can evolve based on insights from release notes, serving as a dynamic feedback loop back to the documentation repository. + +--- +## Living Documentation Regime Features + +### Data Mining from GitHub Repositories + +This is a build-in feature, that allows you to define which repositories should be included in the living documentation process. This essential process can not be deactivated inside of regime scope. By specifying repositories, you can focus on the most relevant projects for your documentation needs. + +- **Activation**: This is a built-in feature, so it is always activated. +- **Default Behavior**: By default, the action will include all repositories defined in the repositories input parameter. Each repository is defined with its organization name, repository name, and query labels. + +### Data Mining from GitHub Projects + +This feature allows you to define which projects should be included in the living documentation process. By specifying projects, you can focus on the most relevant projects for your documentation needs. + +- **Activation**: To activate this feature, set the `liv-doc-project-state-mining` input to true. +- **Non-Activated Behavior**: By default, when the feature is inactive, the action will include all projects linked to the repositories. This information is provided by the GitHub API. +- **Activated Example**: Use available options to customize which projects are included in the documentation. + - `project-state-mining: false` deactivates the mining of project state data from GitHub Projects. If set to **false**, project state data will not be included in the generated documentation and project related configuration options will be ignored. + - `projects-title-filter: []` filters the repository attached projects by titles, if list is empty all projects are used. + ```json + { + "organization-name": "absa-group", + "repository-name": "living-doc-example-project", + "query-labels": ["feature", "bug"], + "projects-title-filter": ["Community Outreach Initiatives", "Health Data Analysis"] + } + ``` + +### Living Documentation Page Generation + +The goal is to provide a straightforward view of all issues in a single table, making it easy to see the overall status and details of issues across repositories. + +The current output implementation is designed to work with the AbsaOSS [mdoc viewer](https://github.com/AbsaOSS/cps-mdoc-viewer) solution. +The presence of multiple _index.md files is necessary for the current solution to correctly generate the documentation structure. + +- **Activation**: This is a built-in feature, so it is always activated. +- **Default Behavior**: By default, the action generates a single table that lists all issues from the defined repositories. + +#### Structured Output + +This feature allows you to generate structured output for the living documentation and see a summary `index.md` page for each fetched repository. + +- **Activation**: To activate this feature, set the `liv-doc-structured-output` input to true. +- **Non-Activated Behavior**: By default, when the feature is inactive, the action generates all the documentation in a single directory. +- **Activated Example**: Use the structured output feature to organize the generated documentation by organization and repository name. + - `structured-output: true` activates the structured output feature. + ``` + output + |- org 1 + |--repo 1 + |-- issue md page 1 + |-- issue md page 2 + |-- _index.md + |-- _index.md + |- org 2 + |--repo 1 + |-- issue md page 1 + |-- _index.md + |--repo 2 + ... + |-- _index.md + |- _index.md + ``` + +#### Output Grouped by Topics + +The feature allows you to generate output grouped by topics. This feature is useful when grouping tickets by specific topics or themes. + +To gain a better understanding of the term "Topic", refer to the [Labels](#labels) section. + +- **Activation**: To activate this feature, set the `liv-doc-group-output-by-topics` input to true. +- **Non-Activated Behavior**: By default, when the feature is inactive, the action generates all the documentation in a single directory. +- **Activated Example**: Use the grouped output feature to organize the generated documentation by topics. + - `group-output-by-topics: true` activates the grouped output feature. + ``` + output + |- topic 1 + |-- issue md page 1 + |-- issue md page 2 + |-- _index.md + |- topic 2 + |-- issue md page 1 + |-- _index.md + |- _index.md + ``` + \ No newline at end of file diff --git a/living_documentation_generator/__init__.py b/living_documentation_regime/__init__.py similarity index 100% rename from living_documentation_generator/__init__.py rename to living_documentation_regime/__init__.py diff --git a/living_documentation_generator/action_inputs.py b/living_documentation_regime/action_inputs.py similarity index 65% rename from living_documentation_generator/action_inputs.py rename to living_documentation_regime/action_inputs.py index 26df0d7..2c0379d 100644 --- a/living_documentation_generator/action_inputs.py +++ b/living_documentation_regime/action_inputs.py @@ -21,19 +21,17 @@ import json import logging -import os import sys -from living_documentation_generator.model.config_repository import ConfigRepository -from living_documentation_generator.utils.utils import get_action_input, make_absolute_path, get_all_project_directories -from living_documentation_generator.utils.constants import ( +from living_documentation_regime.model.config_repository import ConfigRepository +from utils.utils import get_action_input +from utils.constants import ( GITHUB_TOKEN, + LIV_DOC_REGIME, PROJECT_STATE_MINING, REPOSITORIES, GROUP_OUTPUT_BY_TOPICS, STRUCTURED_OUTPUT, - OUTPUT_PATH, - DEFAULT_OUTPUT_PATH, ) logger = logging.getLogger(__name__) @@ -53,6 +51,14 @@ def get_github_token() -> str: """ return get_action_input(GITHUB_TOKEN) + @staticmethod + def get_liv_doc_regime() -> bool: + """ + Getter of the LivDoc regime switch. + @return: True if LivDoc regime is enabled, False otherwise. + """ + return get_action_input(LIV_DOC_REGIME, "false").lower() == "true" + @staticmethod def get_is_project_state_mining_enabled() -> bool: """ @@ -107,48 +113,15 @@ def get_repositories() -> list[ConfigRepository]: return repositories - @staticmethod - def get_output_directory() -> str: - """Getter of the output directory.""" - out_path = get_action_input(OUTPUT_PATH, default=DEFAULT_OUTPUT_PATH) - return make_absolute_path(out_path) - - @staticmethod - def validate_inputs(out_path: str) -> None: + def validate_inputs(self) -> None: """ Loads the inputs provided for the Living documentation generator. Logs any validation errors and exits if any are found. - @param out_path: The output path for the generated documentation. @return: None """ # Validate INPUT_REPOSITORIES - ActionInputs.get_repositories() - - # Validate INPUT_OUTPUT_PATH - if out_path == "": - logger.error("INPUT_OUTPUT_PATH can not be an empty string.") - sys.exit(1) - - # Check that the INPUT_OUTPUT_PATH is not a project directory - # Note: That would cause a rewriting project files - project_directories = get_all_project_directories() - abspath_user_output_path = os.path.abspath(ActionInputs.get_output_directory()) - - # Ensure project directories are absolute paths - project_abspath_directories = [os.path.abspath(d) for d in project_directories] - - if abspath_user_output_path in project_abspath_directories: - project_abspath_directories.remove(abspath_user_output_path) - - for project_directory in project_abspath_directories: - # Finds the common path between the absolute paths of out_path and project_directory - common_path = os.path.commonpath([os.path.abspath(out_path), os.path.abspath(project_directory)]) - - # Check if common path is equal to the absolute path of project_directory - if common_path == os.path.abspath(project_directory): - logger.error("INPUT_OUTPUT_PATH cannot be chosen as a part of any project folder.") - sys.exit(1) + self.get_repositories() logger.debug("Action inputs validation successfully completed.") diff --git a/living_documentation_generator/github_projects.py b/living_documentation_regime/github_projects.py similarity index 97% rename from living_documentation_generator/github_projects.py rename to living_documentation_regime/github_projects.py index a500be4..0486f7d 100644 --- a/living_documentation_generator/github_projects.py +++ b/living_documentation_regime/github_projects.py @@ -23,9 +23,9 @@ from github.Repository import Repository -from living_documentation_generator.model.github_project import GithubProject -from living_documentation_generator.model.project_issue import ProjectIssue -from living_documentation_generator.utils.github_project_queries import ( +from living_documentation_regime.model.github_project import GithubProject +from living_documentation_regime.model.project_issue import ProjectIssue +from utils.github_project_queries import ( get_projects_from_repo_query, get_project_field_options_query, get_issues_from_project_query, diff --git a/living_documentation_generator/generator.py b/living_documentation_regime/living_documentation_generator.py similarity index 94% rename from living_documentation_generator/generator.py rename to living_documentation_regime/living_documentation_generator.py index 8060f2d..a7140b7 100644 --- a/living_documentation_generator/generator.py +++ b/living_documentation_regime/living_documentation_generator.py @@ -28,21 +28,22 @@ from github import Github, Auth from github.Issue import Issue -from living_documentation_generator.action_inputs import ActionInputs -from living_documentation_generator.github_projects import GithubProjects -from living_documentation_generator.model.github_project import GithubProject -from living_documentation_generator.model.consolidated_issue import ConsolidatedIssue -from living_documentation_generator.model.project_issue import ProjectIssue -from living_documentation_generator.utils.decorators import safe_call_decorator -from living_documentation_generator.utils.github_rate_limiter import GithubRateLimiter -from living_documentation_generator.utils.utils import make_issue_key, generate_root_level_index_page, load_template -from living_documentation_generator.utils.constants import ( +from living_documentation_regime.action_inputs import ActionInputs +from living_documentation_regime.github_projects import GithubProjects +from living_documentation_regime.model.github_project import GithubProject +from living_documentation_regime.model.consolidated_issue import ConsolidatedIssue +from living_documentation_regime.model.project_issue import ProjectIssue +from utils.decorators import safe_call_decorator +from utils.github_rate_limiter import GithubRateLimiter +from utils.utils import make_issue_key, generate_root_level_index_page, load_template, make_absolute_path +from utils.constants import ( ISSUES_PER_PAGE_LIMIT, ISSUE_STATE_ALL, LINKED_TO_PROJECT_TRUE, LINKED_TO_PROJECT_FALSE, TABLE_HEADER_WITH_PROJECT_DATA, TABLE_HEADER_WITHOUT_PROJECT_DATA, + LIV_DOC_OUTPUT_PATH, ) logger = logging.getLogger(__name__) @@ -56,21 +57,14 @@ class LivingDocumentationGenerator: """ PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) + TEMPLATES_BASE_PATH = os.path.join(PROJECT_ROOT, os.pardir, "templates", "living_documentation_regime") - ISSUE_PAGE_TEMPLATE_FILE = os.path.join(PROJECT_ROOT, os.pardir, "templates", "issue_detail_page_template.md") - INDEX_NO_STRUCT_TEMPLATE_FILE = os.path.join( - PROJECT_ROOT, os.pardir, "templates", "_index_no_struct_page_template.md" - ) - INDEX_ROOT_LEVEL_TEMPLATE_FILE = os.path.join( - PROJECT_ROOT, os.pardir, "templates", "_index_root_level_page_template.md" - ) - INDEX_ORG_LEVEL_TEMPLATE_FILE = os.path.join( - PROJECT_ROOT, os.pardir, "templates", "_index_org_level_page_template.md" - ) - INDEX_DATA_LEVEL_TEMPLATE_FILE = os.path.join( - PROJECT_ROOT, os.pardir, "templates", "_index_data_level_page_template.md" - ) - INDEX_TOPIC_PAGE_TEMPLATE_FILE = os.path.join(PROJECT_ROOT, os.pardir, "templates", "_index_repo_page_template.md") + ISSUE_PAGE_TEMPLATE_FILE = os.path.join(TEMPLATES_BASE_PATH, "issue_detail_page_template.md") + INDEX_NO_STRUCT_TEMPLATE_FILE = os.path.join(TEMPLATES_BASE_PATH, "_index_no_struct_page_template.md") + INDEX_ROOT_LEVEL_TEMPLATE_FILE = os.path.join(TEMPLATES_BASE_PATH, "_index_root_level_page_template.md") + INDEX_ORG_LEVEL_TEMPLATE_FILE = os.path.join(TEMPLATES_BASE_PATH, "_index_org_level_page_template.md") + INDEX_DATA_LEVEL_TEMPLATE_FILE = os.path.join(TEMPLATES_BASE_PATH, "_index_data_level_page_template.md") + INDEX_TOPIC_PAGE_TEMPLATE_FILE = os.path.join(TEMPLATES_BASE_PATH, "_index_repo_page_template.md") def __init__(self): github_token = ActionInputs.get_github_token() @@ -120,7 +114,7 @@ def _clean_output_directory() -> None: @return: None """ - output_path = ActionInputs.get_output_directory() + output_path = make_absolute_path(LIV_DOC_OUTPUT_PATH) if os.path.exists(output_path): shutil.rmtree(output_path) @@ -294,7 +288,7 @@ def _generate_markdown_pages(self, issues: dict[str, ConsolidatedIssue]) -> None topics = set() is_structured_output = ActionInputs.get_is_structured_output_enabled() is_grouping_by_topics = ActionInputs.get_is_grouping_by_topics_enabled() - output_path = ActionInputs.get_output_directory() + output_path = make_absolute_path(LIV_DOC_OUTPUT_PATH) # Load the template files for generating the Markdown pages ( @@ -518,7 +512,7 @@ def _generate_sub_level_index_page(index_template: str, level: str, repository_i sub_level_index_page = index_template.format(**replacement) # Create a sub index page file - output_path = os.path.join(ActionInputs.get_output_directory(), index_level_dir) + output_path = os.path.join(make_absolute_path(LIV_DOC_OUTPUT_PATH), index_level_dir) with open(os.path.join(output_path, "_index.md"), "w", encoding="utf-8") as f: f.write(sub_level_index_page) @@ -654,7 +648,7 @@ def _generate_index_directory_path(repository_id: Optional[str], topic: Optional @param topic: The topic used for grouping issues. @return: The generated directory path. """ - output_path: str = ActionInputs.get_output_directory() + output_path: str = make_absolute_path(LIV_DOC_OUTPUT_PATH) if ActionInputs.get_is_structured_output_enabled() and repository_id: organization_name, repository_name = repository_id.split("/") diff --git a/living_documentation_generator/model/__init__.py b/living_documentation_regime/model/__init__.py similarity index 100% rename from living_documentation_generator/model/__init__.py rename to living_documentation_regime/model/__init__.py diff --git a/living_documentation_generator/model/config_repository.py b/living_documentation_regime/model/config_repository.py similarity index 100% rename from living_documentation_generator/model/config_repository.py rename to living_documentation_regime/model/config_repository.py diff --git a/living_documentation_generator/model/consolidated_issue.py b/living_documentation_regime/model/consolidated_issue.py similarity index 96% rename from living_documentation_generator/model/consolidated_issue.py rename to living_documentation_regime/model/consolidated_issue.py index f0ea63f..f01a05c 100644 --- a/living_documentation_generator/model/consolidated_issue.py +++ b/living_documentation_regime/model/consolidated_issue.py @@ -24,9 +24,10 @@ from github.Issue import Issue -from living_documentation_generator.action_inputs import ActionInputs -from living_documentation_generator.utils.utils import sanitize_filename -from living_documentation_generator.model.project_status import ProjectStatus +from living_documentation_regime.action_inputs import ActionInputs +from living_documentation_regime.model.project_status import ProjectStatus +from utils.constants import LIV_DOC_OUTPUT_PATH +from utils.utils import sanitize_filename, make_absolute_path logger = logging.getLogger(__name__) @@ -177,7 +178,7 @@ def generate_directory_path(self, issue_table: str) -> list[str]: @param issue_table: The consolidated issue summary table. @return: The list of generated directory paths. """ - output_path = ActionInputs.get_output_directory() + output_path: str = make_absolute_path(LIV_DOC_OUTPUT_PATH) # If structured output is enabled, create a directory path based on the repository if ActionInputs.get_is_structured_output_enabled() and self.repository_id: diff --git a/living_documentation_generator/model/github_project.py b/living_documentation_regime/model/github_project.py similarity index 94% rename from living_documentation_generator/model/github_project.py rename to living_documentation_regime/model/github_project.py index 23646ef..abe33fe 100644 --- a/living_documentation_generator/model/github_project.py +++ b/living_documentation_regime/model/github_project.py @@ -38,6 +38,12 @@ def __init__(self): self.__organization_name: str = "" self.__field_options: dict[str, str] = {} + def __repr__(self) -> str: + """String representation of the GitHub project.""" + return "\nGithubProject(id={}, number={}, title={}, organization_name={})".format( + self.id, self.number, self.title, self.organization_name + ) + @property def id(self) -> str: """Getter of the project ID.""" diff --git a/living_documentation_generator/model/project_issue.py b/living_documentation_regime/model/project_issue.py similarity index 87% rename from living_documentation_generator/model/project_issue.py rename to living_documentation_regime/model/project_issue.py index 25678d2..7624dde 100644 --- a/living_documentation_generator/model/project_issue.py +++ b/living_documentation_regime/model/project_issue.py @@ -21,8 +21,8 @@ from typing import Optional -from living_documentation_generator.model.github_project import GithubProject -from living_documentation_generator.model.project_status import ProjectStatus +from living_documentation_regime.model.github_project import GithubProject +from living_documentation_regime.model.project_status import ProjectStatus logger = logging.getLogger(__name__) @@ -39,6 +39,12 @@ def __init__(self): self.__repository_name: str = "" self.__project_status: ProjectStatus = ProjectStatus() + def __repr__(self) -> str: + """String representation of the ProjectIssue object.""" + return "\nProjectIssue(number={}, organization_name={}, repository_name={})".format( + self.number, self.organization_name, self.repository_name + ) + @property def number(self) -> int: """Getter of the project issue number.""" @@ -76,7 +82,7 @@ def loads(self, issue_json: dict, project: GithubProject) -> Optional["ProjectIs self.__repository_name = issue_json["content"]["repository"]["name"] self.__organization_name = issue_json["content"]["repository"]["owner"]["login"] except KeyError as e: - logger.debug("KeyError(%s) occurred while parsing issue json: %s.", str(e), issue_json) + logger.debug("An issue for key %s occurred while parsing issue json: %s.", str(e), issue_json) self.__project_status.project_title = project.title diff --git a/living_documentation_generator/model/project_status.py b/living_documentation_regime/model/project_status.py similarity index 96% rename from living_documentation_generator/model/project_status.py rename to living_documentation_regime/model/project_status.py index 64a0e8f..eced657 100644 --- a/living_documentation_generator/model/project_status.py +++ b/living_documentation_regime/model/project_status.py @@ -18,7 +18,7 @@ This module contains a data container for issue Project Status. """ -from living_documentation_generator.utils.constants import NO_PROJECT_DATA +from utils.constants import NO_PROJECT_DATA class ProjectStatus: diff --git a/main.py b/main.py index 3023a7d..e19fda0 100644 --- a/main.py +++ b/main.py @@ -21,11 +21,11 @@ import logging -from living_documentation_generator.action_inputs import ActionInputs -from living_documentation_generator.generator import LivingDocumentationGenerator -from living_documentation_generator.utils.constants import OUTPUT_PATH, DEFAULT_OUTPUT_PATH -from living_documentation_generator.utils.utils import set_action_output, get_action_input -from living_documentation_generator.utils.logging_config import setup_logging +from living_documentation_regime.action_inputs import ActionInputs +from living_documentation_regime.living_documentation_generator import LivingDocumentationGenerator +from utils.constants import OUTPUT_PATH +from utils.utils import set_action_output, make_absolute_path +from utils.logging_config import setup_logging def run() -> None: @@ -40,17 +40,26 @@ def run() -> None: logger.info("Starting Living Documentation generation.") # Validate the action inputs - out_path_from_config = get_action_input(OUTPUT_PATH, default=DEFAULT_OUTPUT_PATH) - ActionInputs.validate_inputs(out_path_from_config) + ActionInputs().validate_inputs() - # Create the Living Documentation Generator - generator = LivingDocumentationGenerator() + if ActionInputs.get_liv_doc_regime(): + logger.info("Living Documentation generation - Starting the `LivDoc` generation regime.") - # Generate the Living Documentation - generator.generate() + # Generate the Living documentation + LivingDocumentationGenerator().generate() + + logger.info("Living Documentation generation - `LivDoc` generation regime completed.") + + # elif ActionInputs.get_ci_regime(): + # logger.info("Living Documentation generation - Starting the `CI` generation regime.") + # + # # Generate the CI Documentation + # CiDocumentationGenerator().generate() + # + # logger.info("Living Documentation generation - `CI` generation regime completed.") # Set the output for the GitHub Action - output_path = ActionInputs.get_output_directory() + output_path: str = make_absolute_path(OUTPUT_PATH) set_action_output("output-path", output_path) logger.info("Living Documentation generation - output path set to `%s`.", output_path) diff --git a/templates/_index_data_level_page_template.md b/templates/living_documentation_regime/_index_data_level_page_template.md similarity index 100% rename from templates/_index_data_level_page_template.md rename to templates/living_documentation_regime/_index_data_level_page_template.md diff --git a/templates/_index_no_struct_page_template.md b/templates/living_documentation_regime/_index_no_struct_page_template.md similarity index 100% rename from templates/_index_no_struct_page_template.md rename to templates/living_documentation_regime/_index_no_struct_page_template.md diff --git a/templates/_index_org_level_page_template.md b/templates/living_documentation_regime/_index_org_level_page_template.md similarity index 100% rename from templates/_index_org_level_page_template.md rename to templates/living_documentation_regime/_index_org_level_page_template.md diff --git a/templates/_index_repo_page_template.md b/templates/living_documentation_regime/_index_repo_page_template.md similarity index 100% rename from templates/_index_repo_page_template.md rename to templates/living_documentation_regime/_index_repo_page_template.md diff --git a/templates/_index_root_level_page_template.md b/templates/living_documentation_regime/_index_root_level_page_template.md similarity index 100% rename from templates/_index_root_level_page_template.md rename to templates/living_documentation_regime/_index_root_level_page_template.md diff --git a/templates/issue_detail_page_template.md b/templates/living_documentation_regime/issue_detail_page_template.md similarity index 100% rename from templates/issue_detail_page_template.md rename to templates/living_documentation_regime/issue_detail_page_template.md diff --git a/tests/conftest.py b/tests/conftest.py index eb344ee..0eb6d0f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,12 +21,12 @@ from github.RateLimit import RateLimit from github.Repository import Repository -from living_documentation_generator.generator import LivingDocumentationGenerator -from living_documentation_generator.model.config_repository import ConfigRepository -from living_documentation_generator.model.consolidated_issue import ConsolidatedIssue -from living_documentation_generator.model.github_project import GithubProject -from living_documentation_generator.model.project_status import ProjectStatus -from living_documentation_generator.utils.github_rate_limiter import GithubRateLimiter +from living_documentation_regime.living_documentation_generator import LivingDocumentationGenerator +from living_documentation_regime.model.config_repository import ConfigRepository +from living_documentation_regime.model.consolidated_issue import ConsolidatedIssue +from living_documentation_regime.model.github_project import GithubProject +from living_documentation_regime.model.project_status import ProjectStatus +from utils.github_rate_limiter import GithubRateLimiter @pytest.fixture @@ -101,8 +101,8 @@ def load_all_templates_setup(mocker): @pytest.fixture -def generator(mocker): - mock_github_class = mocker.patch("living_documentation_generator.generator.Github") +def living_documentation_generator(mocker): + mock_github_class = mocker.patch("living_documentation_regime.living_documentation_generator.Github") mock_github_instance = mock_github_class.return_value mock_rate_limit = mocker.Mock() @@ -113,7 +113,8 @@ def generator(mocker): mock_github_instance.get_repo.return_value = mocker.Mock() mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_github_token", return_value="FakeGithubToken" + "living_documentation_regime.living_documentation_generator.ActionInputs.get_github_token", + return_value="FakeGithubToken", ) return LivingDocumentationGenerator() diff --git a/tests/living_documentation_regime/__init__.py b/tests/living_documentation_regime/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/living_documentation_generator/utils/__init__.py b/tests/living_documentation_regime/model/__init__.py similarity index 100% rename from living_documentation_generator/utils/__init__.py rename to tests/living_documentation_regime/model/__init__.py diff --git a/tests/model/test_config_repository.py b/tests/living_documentation_regime/model/test_config_repository.py similarity index 88% rename from tests/model/test_config_repository.py rename to tests/living_documentation_regime/model/test_config_repository.py index ae1e026..a79be92 100644 --- a/tests/model/test_config_repository.py +++ b/tests/living_documentation_regime/model/test_config_repository.py @@ -14,7 +14,7 @@ # limitations under the License. # -from living_documentation_generator.model.config_repository import ConfigRepository +from living_documentation_regime.model.config_repository import ConfigRepository def test_load_from_json_with_valid_input_loads_correctly(): @@ -43,7 +43,7 @@ def test_load_from_json_with_valid_input_loads_correctly(): def test_load_from_json_with_missing_key_logs_error(mocker): config_repository = ConfigRepository() - mock_log_error = mocker.patch("living_documentation_generator.model.config_repository.logger.error") + mock_log_error = mocker.patch("living_documentation_regime.model.config_repository.logger.error") repository_json = {"non-existent-key": "value"} actual = config_repository.load_from_json(repository_json) @@ -56,7 +56,7 @@ def test_load_from_json_with_missing_key_logs_error(mocker): def test_load_from_json_with_wrong_structure_input_logs_error(mocker): config_repository = ConfigRepository() - mock_log_error = mocker.patch("living_documentation_generator.model.config_repository.logger.error") + mock_log_error = mocker.patch("living_documentation_regime.model.config_repository.logger.error") repository_json = "not a dictionary" actual = config_repository.load_from_json(repository_json) diff --git a/tests/living_documentation_regime/model/test_consolidated_issue.py b/tests/living_documentation_regime/model/test_consolidated_issue.py new file mode 100644 index 0000000..9ea3eca --- /dev/null +++ b/tests/living_documentation_regime/model/test_consolidated_issue.py @@ -0,0 +1,193 @@ +# +# Copyright 2024 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. +# +from github.Issue import Issue + +from living_documentation_regime.model.consolidated_issue import ConsolidatedIssue + + +# generate_page_filename + + +def test_generate_page_filename_correct_behaviour(): + # Arrange + mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True) + consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue) + + # Act + actual = consolidated_issue.generate_page_filename() + + # Assert + assert "1_issue_title.md" == actual + + +def test_generate_page_filename_with_none_title(mocker): + # Arrange + mock_log_error = mocker.patch("living_documentation_regime.model.consolidated_issue.logger.error") + mock_issue = Issue(None, None, {"number": 1, "title": None}, completed=True) + mock_consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue) + + # Act + actual = mock_consolidated_issue.generate_page_filename() + + # Assert + assert "1.md" == actual + mock_log_error.assert_called_once_with( + "Issue page filename generation failed for Issue %s/%s (%s). Issue does not have a title.", + "organization", + "repository", + 1, + exc_info=True, + ) + + +# generate_directory_path + + +def test_generate_directory_path_structured_output_disabled_grouping_by_topics_disabled(mocker): + # Arrange + mocker.patch( + "living_documentation_regime.model.consolidated_issue.make_absolute_path", + return_value="/mocked/absolute/output/path/", + ) + mocker.patch( + "living_documentation_regime.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=False + ) + mocker.patch( + "living_documentation_regime.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", + return_value=False, + ) + mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True) + mock_consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue) + + # Act + actual = mock_consolidated_issue.generate_directory_path("issue table") + + # Assert + assert ["/mocked/absolute/output/path/"] == actual + + +def test_generate_directory_path_structured_output_enabled_grouping_by_topics_disabled(mocker): + # Arrange + mocker.patch( + "living_documentation_regime.model.consolidated_issue.make_absolute_path", + return_value="/mocked/absolute/output/path/", + ) + mocker.patch( + "living_documentation_regime.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=True + ) + mocker.patch( + "living_documentation_regime.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", + return_value=False, + ) + mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True) + mock_consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue) + + # Act + actual = mock_consolidated_issue.generate_directory_path("issue table") + + # Assert + assert ["/mocked/absolute/output/path/organization/repository"] == actual + + +def test_generate_directory_path_structured_output_disabled_grouping_by_topics_enabled_two_issue_topics(mocker): + # Arrange + mock_log_debug = mocker.patch("living_documentation_regime.model.consolidated_issue.logger.debug") + mocker.patch( + "living_documentation_regime.model.consolidated_issue.make_absolute_path", + return_value="/mocked/absolute/output/path/", + ) + mocker.patch( + "living_documentation_regime.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=False + ) + mocker.patch( + "living_documentation_regime.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True + ) + mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True) + mock_consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue) + + # Act + actual = mock_consolidated_issue.generate_directory_path("| Labels | feature, BETopic, FETopic |") + + # Assert + assert ["/mocked/absolute/output/path/BETopic", "/mocked/absolute/output/path/FETopic"] == actual + mock_log_debug.assert_called_once_with( + "Multiple Topic labels found for Issue #%s: %s (%s): %s", + 1, + "Issue Title", + "organization/repository", + "BETopic, FETopic", + ) + + +def test_generate_directory_path_structured_output_disabled_grouping_by_topics_enabled_no_issue_topics(mocker): + # Arrange + mock_log_error = mocker.patch("living_documentation_regime.model.consolidated_issue.logger.error") + mock_log_debug = mocker.patch("living_documentation_regime.model.consolidated_issue.logger.debug") + mocker.patch( + "living_documentation_regime.model.consolidated_issue.make_absolute_path", + return_value="/mocked/absolute/output/path/", + ) + mocker.patch( + "living_documentation_regime.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=False + ) + mocker.patch( + "living_documentation_regime.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True + ) + mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True) + mock_consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue) + + # Act + actual = mock_consolidated_issue.generate_directory_path("| Labels | feature, bug |") + + # Assert + assert ["/mocked/absolute/output/path/NoTopic"] == actual + mock_log_error.assert_called_once_with( + "No Topic label found for Issue #%i: %s (%s)", 1, "Issue Title", "organization/repository" + ) + mock_log_debug.assert_not_called() + + +def test_generate_directory_path_structured_output_enabled_grouping_by_topics_enabled_one_issue_topic(mocker): + # Arrange + mock_log_debug = mocker.patch("living_documentation_regime.model.consolidated_issue.logger.debug") + mocker.patch( + "living_documentation_regime.model.consolidated_issue.make_absolute_path", + return_value="/mocked/absolute/output/path/", + ) + mocker.patch( + "living_documentation_regime.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=True + ) + mocker.patch( + "living_documentation_regime.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True + ) + mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True) + mock_consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue) + + # Act + actual = mock_consolidated_issue.generate_directory_path("| Labels | feature, BETopic, FETopic |") + + # Assert + assert [ + "/mocked/absolute/output/path/organization/repository/BETopic", + "/mocked/absolute/output/path/organization/repository/FETopic", + ] == actual + mock_log_debug.assert_called_once_with( + "Multiple Topic labels found for Issue #%s: %s (%s): %s", + 1, + "Issue Title", + "organization/repository", + "BETopic, FETopic", + ) diff --git a/tests/model/test_github_project.py b/tests/living_documentation_regime/model/test_github_project.py similarity index 92% rename from tests/model/test_github_project.py rename to tests/living_documentation_regime/model/test_github_project.py index 7b9af13..3c24a1c 100644 --- a/tests/model/test_github_project.py +++ b/tests/living_documentation_regime/model/test_github_project.py @@ -15,7 +15,7 @@ # from github.Repository import Repository -from living_documentation_generator.model.github_project import GithubProject +from living_documentation_regime.model.github_project import GithubProject # loads @@ -47,7 +47,7 @@ def test_loads_with_valid_input_loads_correctly(mocker): def test_loads_with_missing_key(mocker): github_project = GithubProject() - mock_log_error = mocker.patch("living_documentation_generator.model.github_project.logger.error") + mock_log_error = mocker.patch("living_documentation_regime.model.github_project.logger.error") project_json = {"id": "123", "title": "Test Project", "unexpected_key": "unexpected_value"} repository = mocker.Mock(spec=Repository) repository.owner.login = "organizationABC" @@ -92,7 +92,7 @@ def test_update_field_options_with_valid_input(): def test_update_field_options_with_no_expected_response_structure(mocker): github_project = GithubProject() - mock_log_error = mocker.patch("living_documentation_generator.model.github_project.logger.error") + mock_log_error = mocker.patch("living_documentation_regime.model.github_project.logger.error") field_option_response = {"unexpected_structure": {"unexpected_key": "unexpected_value"}} github_project._update_field_options(field_option_response) diff --git a/tests/model/test_project_issue.py b/tests/living_documentation_regime/model/test_project_issue.py similarity index 86% rename from tests/model/test_project_issue.py rename to tests/living_documentation_regime/model/test_project_issue.py index d9070d0..0adc184 100644 --- a/tests/model/test_project_issue.py +++ b/tests/living_documentation_regime/model/test_project_issue.py @@ -13,8 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # -from living_documentation_generator.model.github_project import GithubProject -from living_documentation_generator.model.project_issue import ProjectIssue +from living_documentation_regime.model.github_project import GithubProject +from living_documentation_regime.model.project_issue import ProjectIssue # loads @@ -56,7 +56,7 @@ def test_loads_with_valid_input(github_project_setup): def test_loads_without_content_key_logs_debug(mocker): project_issue = ProjectIssue() - mock_log = mocker.patch("living_documentation_generator.model.project_issue.logger") + mock_log = mocker.patch("living_documentation_regime.model.project_issue.logger") issue_json = {} project = GithubProject() @@ -68,7 +68,7 @@ def test_loads_without_content_key_logs_debug(mocker): def test_loads_with_incorrect_json_structure_for_repository_name(mocker): project_issue = ProjectIssue() - mock_log = mocker.patch("living_documentation_generator.model.project_issue.logger") + mock_log = mocker.patch("living_documentation_regime.model.project_issue.logger") incorrect_json = {"content": {"number": 1}} project = GithubProject() @@ -78,5 +78,5 @@ def test_loads_with_incorrect_json_structure_for_repository_name(mocker): assert "" == actual.organization_name assert "" == actual.repository_name mock_log.debug.assert_called_once_with( - "KeyError(%s) occurred while parsing issue json: %s.", "'repository'", incorrect_json + "An issue for key %s occurred while parsing issue json: %s.", "'repository'", incorrect_json ) diff --git a/tests/test_action_inputs.py b/tests/living_documentation_regime/test_action_inputs.py similarity index 58% rename from tests/test_action_inputs.py rename to tests/living_documentation_regime/test_action_inputs.py index aa34ba6..35a2ecb 100644 --- a/tests/test_action_inputs.py +++ b/tests/living_documentation_regime/test_action_inputs.py @@ -16,8 +16,8 @@ import json import os -from living_documentation_generator.action_inputs import ActionInputs -from living_documentation_generator.model.config_repository import ConfigRepository +from living_documentation_regime.action_inputs import ActionInputs +from living_documentation_regime.model.config_repository import ConfigRepository # Check Action Inputs default values @@ -42,18 +42,6 @@ def test_verbose_logging_default(): assert not actual -def test_output_path_default(): - # Arrange - os.environ.pop("INPUT_OUTPUT_PATH", None) - expected = os.path.abspath("./output") - - # Act - actual = ActionInputs.get_output_directory() - - # Assert - assert expected == actual - - def test_structured_output_default(): # Arrange os.environ.pop("INPUT_STRUCTURED_OUTPUT", None) @@ -96,7 +84,7 @@ def test_get_repositories_correct_behaviour(mocker): }, ] mocker.patch( - "living_documentation_generator.action_inputs.get_action_input", return_value=json.dumps(repositories_json) + "living_documentation_regime.action_inputs.get_action_input", return_value=json.dumps(repositories_json) ) # Act @@ -118,8 +106,8 @@ def test_get_repositories_correct_behaviour(mocker): def test_get_repositories_default_value_as_json(mocker): # Arrange - mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error") - mocker.patch("living_documentation_generator.action_inputs.get_action_input", return_value="[]") + mock_log_error = mocker.patch("living_documentation_regime.action_inputs.logger.error") + mocker.patch("living_documentation_regime.action_inputs.get_action_input", return_value="[]") mock_exit = mocker.patch("sys.exit") # Act @@ -133,8 +121,8 @@ def test_get_repositories_default_value_as_json(mocker): def test_get_repositories_empty_object_as_input(mocker): # Arrange - mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error") - mocker.patch("living_documentation_generator.action_inputs.get_action_input", return_value="{}") + mock_log_error = mocker.patch("living_documentation_regime.action_inputs.logger.error") + mocker.patch("living_documentation_regime.action_inputs.get_action_input", return_value="{}") mock_exit = mocker.patch("sys.exit") # Act @@ -148,8 +136,8 @@ def test_get_repositories_empty_object_as_input(mocker): def test_get_repositories_error_with_loading_repository_json(mocker): # Arrange - mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error") - mocker.patch("living_documentation_generator.action_inputs.get_action_input", return_value="[{}]") + mock_log_error = mocker.patch("living_documentation_regime.action_inputs.logger.error") + mocker.patch("living_documentation_regime.action_inputs.get_action_input", return_value="[{}]") mocker.patch.object(ConfigRepository, "load_from_json", return_value=False) mock_exit = mocker.patch("sys.exit") @@ -163,8 +151,8 @@ def test_get_repositories_error_with_loading_repository_json(mocker): def test_get_repositories_number_instead_of_json(mocker): # Arrange - mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error") - mocker.patch("living_documentation_generator.action_inputs.get_action_input", return_value=1) + mock_log_error = mocker.patch("living_documentation_regime.action_inputs.logger.error") + mocker.patch("living_documentation_regime.action_inputs.get_action_input", return_value=1) mock_exit = mocker.patch("sys.exit") # Act @@ -177,8 +165,8 @@ def test_get_repositories_number_instead_of_json(mocker): def test_get_repositories_empty_string_as_input(mocker): # Arrange - mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error") - mocker.patch("living_documentation_generator.action_inputs.get_action_input", return_value="") + mock_log_error = mocker.patch("living_documentation_regime.action_inputs.logger.error") + mocker.patch("living_documentation_regime.action_inputs.get_action_input", return_value="") mock_exit = mocker.patch("sys.exit") # Act @@ -192,8 +180,8 @@ def test_get_repositories_empty_string_as_input(mocker): def test_get_repositories_invalid_string_as_input(mocker): # Arrange - mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error") - mocker.patch("living_documentation_generator.action_inputs.get_action_input", return_value="not a JSON string") + mock_log_error = mocker.patch("living_documentation_regime.action_inputs.logger.error") + mocker.patch("living_documentation_regime.action_inputs.get_action_input", return_value="not a JSON string") mock_exit = mocker.patch("sys.exit") # Act @@ -218,63 +206,18 @@ def test_validate_inputs_correct_behaviour(mocker): "projects-title-filter": [], } ] - mock_log_debug = mocker.patch("living_documentation_generator.action_inputs.logger.debug") - mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error") + mock_log_debug = mocker.patch("living_documentation_regime.action_inputs.logger.debug") + mock_log_error = mocker.patch("living_documentation_regime.action_inputs.logger.error") mocker.patch( - "living_documentation_generator.action_inputs.ActionInputs.get_repositories", return_value=repositories_json + "living_documentation_regime.action_inputs.ActionInputs.get_repositories", return_value=repositories_json ) mock_exit = mocker.patch("sys.exit") # Act - ActionInputs.validate_inputs("./output") + ActionInputs().validate_inputs() # Assert mock_exit.assert_not_called() mock_log_debug.assert_called_once_with("Action inputs validation successfully completed.") mock_log_error.assert_not_called() - - -def test_validate_inputs_error_output_path_as_empty_string(mocker): - # Arrange - mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error") - mock_exit = mocker.patch("sys.exit") - - # Act - ActionInputs.validate_inputs("") - - # Assert - mock_exit.assert_called_once_with(1) - mock_log_error.assert_called_once_with("INPUT_OUTPUT_PATH can not be an empty string.") - - -def test_validate_inputs_error_output_path_as_project_directory(mocker): - # Arrange - mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error") - mock_exit = mocker.patch("sys.exit") - - # Act - ActionInputs.validate_inputs("./templates/template_subfolder") - - # Assert - mock_exit.assert_called_once_with(1) - mock_log_error.assert_called_once_with("INPUT_OUTPUT_PATH cannot be chosen as a part of any project folder.") - - -def test_validate_inputs_absolute_output_path_with_relative_project_directories(mocker): - # Arrange - absolute_out_path = "/root/project/dir1/subfolder" - mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error") - mock_exit = mocker.patch("sys.exit") - mocker.patch( - "living_documentation_generator.action_inputs.get_all_project_directories", - return_value=["project/dir1", "project/dir2"], - ) - mocker.patch("os.path.abspath", side_effect=lambda path: f"/root/{path}" if not path.startswith("/") else path) - - # Act - ActionInputs.validate_inputs(absolute_out_path) - - # Assert - mock_exit.assert_called_once_with(1) - mock_log_error.assert_called_once_with("INPUT_OUTPUT_PATH cannot be chosen as a part of any project folder.") diff --git a/tests/test_github_projects.py b/tests/living_documentation_regime/test_github_projects.py similarity index 83% rename from tests/test_github_projects.py rename to tests/living_documentation_regime/test_github_projects.py index 5d905b4..a139583 100644 --- a/tests/test_github_projects.py +++ b/tests/living_documentation_regime/test_github_projects.py @@ -15,7 +15,7 @@ # import requests -from living_documentation_generator.github_projects import GithubProjects +from living_documentation_regime.github_projects import GithubProjects # _send_graphql_query @@ -47,7 +47,7 @@ def test_send_graphql_query_http_error(mocker): "_GithubProjects__initialize_request_session", lambda self: setattr(self, "_GithubProjects__session", mock_session), ) - mock_log_error = mocker.patch("living_documentation_generator.github_projects.logger.error") + mock_log_error = mocker.patch("living_documentation_regime.github_projects.logger.error") mock_session.post.side_effect = requests.HTTPError("HTTP error occurred") actual = GithubProjects("token123")._send_graphql_query("query") @@ -63,7 +63,7 @@ def test_send_graphql_query_request_exception(mocker): "_GithubProjects__initialize_request_session", lambda self: setattr(self, "_GithubProjects__session", mock_session), ) - mock_log_error = mocker.patch("living_documentation_generator.github_projects.logger.error") + mock_log_error = mocker.patch("living_documentation_regime.github_projects.logger.error") mock_session.post.side_effect = requests.RequestException("An error occurred.") actual = GithubProjects("token123")._send_graphql_query("query") @@ -79,14 +79,14 @@ def test_get_repository_projects_correct_behaviour(mocker, repository_setup): # Arrange mock_repository = repository_setup() projects_title_filter = ["Project A"] - mock_logger_debug = mocker.patch("living_documentation_generator.github_projects.logger.debug") + mock_logger_debug = mocker.patch("living_documentation_regime.github_projects.logger.debug") mocker.patch( - "living_documentation_generator.github_projects.get_projects_from_repo_query", + "living_documentation_regime.github_projects.get_projects_from_repo_query", return_value="mocked_projects_query", ) mocker.patch( - "living_documentation_generator.github_projects.get_project_field_options_query", + "living_documentation_regime.github_projects.get_project_field_options_query", return_value="mocked_project_field_options_query", ) mock_send_query = mocker.patch.object(GithubProjects, "_send_graphql_query") @@ -99,7 +99,7 @@ def test_get_repository_projects_correct_behaviour(mocker, repository_setup): {"data": {}}, ] - mock_github_project = mocker.patch("living_documentation_generator.github_projects.GithubProject") + mock_github_project = mocker.patch("living_documentation_regime.github_projects.GithubProject") mock_github_project_instance = mock_github_project.return_value mock_github_project_instance.loads.return_value = mock_github_project_instance @@ -120,14 +120,14 @@ def test_get_repository_projects_correct_behaviour(mocker, repository_setup): def test_get_repository_projects_response_none(mocker, repository_setup): # Arrange mock_repository = repository_setup() - mock_logger_warning = mocker.patch("living_documentation_generator.github_projects.logger.warning") + mock_logger_warning = mocker.patch("living_documentation_regime.github_projects.logger.warning") mocker.patch( - "living_documentation_generator.github_projects.get_projects_from_repo_query", + "living_documentation_regime.github_projects.get_projects_from_repo_query", return_value="mocked_projects_query", ) mocker.patch( - "living_documentation_generator.github_projects.get_project_field_options_query", + "living_documentation_regime.github_projects.get_project_field_options_query", return_value="mocked_project_field_options_query", ) mock_send_query = mocker.patch.object(GithubProjects, "_send_graphql_query", return_value=None) @@ -147,14 +147,14 @@ def test_get_repository_projects_response_none(mocker, repository_setup): def test_get_repository_projects_response_nodes_none(mocker, repository_setup): # Arrange mock_repository = repository_setup() - mock_logger_warning = mocker.patch("living_documentation_generator.github_projects.logger.warning") + mock_logger_warning = mocker.patch("living_documentation_regime.github_projects.logger.warning") mocker.patch( - "living_documentation_generator.github_projects.get_projects_from_repo_query", + "living_documentation_regime.github_projects.get_projects_from_repo_query", return_value="mocked_projects_query", ) mocker.patch( - "living_documentation_generator.github_projects.get_project_field_options_query", + "living_documentation_regime.github_projects.get_project_field_options_query", return_value="mocked_project_field_options_query", ) mock_send_query = mocker.patch.object( @@ -176,10 +176,10 @@ def test_get_repository_projects_response_nodes_none(mocker, repository_setup): def test_get_project_issues_correct_behaviour(mocker, github_project_setup): mock_project = github_project_setup() - mock_logger_debug = mocker.patch("living_documentation_generator.github_projects.logger.debug") + mock_logger_debug = mocker.patch("living_documentation_regime.github_projects.logger.debug") mocker.patch( - "living_documentation_generator.github_projects.get_issues_from_project_query", + "living_documentation_regime.github_projects.get_issues_from_project_query", return_value="mocked_issues_query", ) @@ -203,7 +203,7 @@ def test_get_project_issues_correct_behaviour(mocker, github_project_setup): }, ] - mock_project_issue = mocker.patch("living_documentation_generator.github_projects.ProjectIssue") + mock_project_issue = mocker.patch("living_documentation_regime.github_projects.ProjectIssue") mock_project_issue_instance = mock_project_issue.return_value mock_project_issue_instance.loads.side_effect = lambda issue_data, proj: issue_data if issue_data else None @@ -231,7 +231,7 @@ def test_get_project_issues_no_response(mocker, github_project_setup): mock_project = github_project_setup() mocker.patch( - "living_documentation_generator.github_projects.get_issues_from_project_query", + "living_documentation_regime.github_projects.get_issues_from_project_query", return_value="mocked_issues_query", ) mock_send_query = mocker.patch.object(GithubProjects, "_send_graphql_query", return_value=None) diff --git a/tests/test_generator.py b/tests/living_documentation_regime/test_living_documentation_generator.py similarity index 69% rename from tests/test_generator.py rename to tests/living_documentation_regime/test_living_documentation_generator.py index ea22845..916fcfe 100644 --- a/tests/test_generator.py +++ b/tests/living_documentation_regime/test_living_documentation_generator.py @@ -15,37 +15,41 @@ from github.Issue import Issue -from living_documentation_generator.generator import LivingDocumentationGenerator -from living_documentation_generator.model.consolidated_issue import ConsolidatedIssue -from living_documentation_generator.model.project_issue import ProjectIssue +from living_documentation_regime.living_documentation_generator import LivingDocumentationGenerator +from living_documentation_regime.model.consolidated_issue import ConsolidatedIssue +from living_documentation_regime.model.project_issue import ProjectIssue # generate -def test_generate_correct_behaviour(mocker, generator): +def test_generate_correct_behaviour(mocker, living_documentation_generator): # Arrange - mock_clean_output_directory = mocker.patch.object(generator, "_clean_output_directory") - mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info") - mock_logger_debug = mocker.patch("living_documentation_generator.generator.logger.debug") + mock_clean_output_directory = mocker.patch.object(living_documentation_generator, "_clean_output_directory") + mock_logger_info = mocker.patch("living_documentation_regime.living_documentation_generator.logger.info") + mock_logger_debug = mocker.patch("living_documentation_regime.living_documentation_generator.logger.debug") mock_issue = mocker.Mock() project_issue_mock = mocker.Mock() consolidated_issue_mock = mocker.Mock() mock_fetch_github_issues = mocker.patch.object( - generator, "_fetch_github_issues", return_value={"test_org/test_repo": [mock_issue]} + living_documentation_generator, "_fetch_github_issues", return_value={"test_org/test_repo": [mock_issue]} ) mock_fetch_github_project_issues = mocker.patch.object( - generator, "_fetch_github_project_issues", return_value={"test_org/test_repo#1": [project_issue_mock]} + living_documentation_generator, + "_fetch_github_project_issues", + return_value={"test_org/test_repo#1": [project_issue_mock]}, ) mock_consolidate_issues_data = mocker.patch.object( - generator, "_consolidate_issues_data", return_value={"test_org/test_repo#1": consolidated_issue_mock} + living_documentation_generator, + "_consolidate_issues_data", + return_value={"test_org/test_repo#1": consolidated_issue_mock}, ) - mock_generate_markdown_pages = mocker.patch.object(generator, "_generate_markdown_pages") + mock_generate_markdown_pages = mocker.patch.object(living_documentation_generator, "_generate_markdown_pages") # Act - generator.generate() + living_documentation_generator.generate() # Assert mock_clean_output_directory.assert_called_once() @@ -74,18 +78,19 @@ def test_generate_correct_behaviour(mocker, generator): # _clean_output_directory -def test_clean_output_directory_correct_behaviour(mocker, generator): +def test_clean_output_directory_correct_behaviour(mocker, living_documentation_generator): # Arrange mock_output_path = "/test/output/path" mock_get_output_directory = mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value=mock_output_path + "living_documentation_regime.living_documentation_generator.make_absolute_path", + return_value=mock_output_path, ) mock_exists = mocker.patch("os.path.exists", return_value=True) mock_rmtree = mocker.patch("shutil.rmtree") mock_makedirs = mocker.patch("os.makedirs") # Act - generator._clean_output_directory() + living_documentation_generator._clean_output_directory() # Assert mock_get_output_directory.assert_called_once() @@ -97,7 +102,7 @@ def test_clean_output_directory_correct_behaviour(mocker, generator): # _fetch_github_issues -def test_fetch_github_issues_no_query_labels(mocker, generator, config_repository): +def test_fetch_github_issues_no_query_labels(mocker, living_documentation_generator, config_repository): # Arrange config_repository.query_labels = [] repo = mocker.Mock() @@ -107,18 +112,19 @@ def test_fetch_github_issues_no_query_labels(mocker, generator, config_repositor issue3 = mocker.Mock() expected_issues = {"test_org/test_repo": [issue1, issue2, issue3]} - mock_get_repo = generator._LivingDocumentationGenerator__github_instance.get_repo + mock_get_repo = living_documentation_generator._LivingDocumentationGenerator__github_instance.get_repo mock_get_repo.return_value = repo mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_repositories", return_value=[config_repository] + "living_documentation_regime.living_documentation_generator.ActionInputs.get_repositories", + return_value=[config_repository], ) - mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info") - mock_logger_debug = mocker.patch("living_documentation_generator.generator.logger.debug") + mock_logger_info = mocker.patch("living_documentation_regime.living_documentation_generator.logger.info") + mock_logger_debug = mocker.patch("living_documentation_regime.living_documentation_generator.logger.debug") mock_get_issues = mocker.patch.object(repo, "get_issues", return_value=[issue1, issue2, issue3]) # Act - actual = generator._fetch_github_issues() + actual = living_documentation_generator._fetch_github_issues() # Assert assert expected_issues == actual @@ -145,7 +151,7 @@ def test_fetch_github_issues_no_query_labels(mocker, generator, config_repositor ) -def test_fetch_github_issues_with_given_query_labels(mocker, generator, config_repository): +def test_fetch_github_issues_with_given_query_labels(mocker, living_documentation_generator, config_repository): # Arrange config_repository.query_labels = ["bug", "enhancement"] repo = mocker.Mock() @@ -154,18 +160,19 @@ def test_fetch_github_issues_with_given_query_labels(mocker, generator, config_r issue2 = mocker.Mock() expected_issues = {"test_org/test_repo": [issue1, issue2]} - mock_get_repo = generator._LivingDocumentationGenerator__github_instance.get_repo + mock_get_repo = living_documentation_generator._LivingDocumentationGenerator__github_instance.get_repo mock_get_repo.return_value = repo mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_repositories", return_value=[config_repository] + "living_documentation_regime.living_documentation_generator.ActionInputs.get_repositories", + return_value=[config_repository], ) - mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info") - mock_logger_debug = mocker.patch("living_documentation_generator.generator.logger.debug") + mock_logger_info = mocker.patch("living_documentation_regime.living_documentation_generator.logger.info") + mock_logger_debug = mocker.patch("living_documentation_regime.living_documentation_generator.logger.debug") mock_get_issues = mocker.patch.object(repo, "get_issues", side_effect=[[issue1], [issue2]]) # Act - actual = generator._fetch_github_issues() + actual = living_documentation_generator._fetch_github_issues() # Assert assert expected_issues == actual @@ -193,16 +200,17 @@ def test_fetch_github_issues_with_given_query_labels(mocker, generator, config_r ) -def test_fetch_github_issues_repository_none(mocker, generator, config_repository): +def test_fetch_github_issues_repository_none(mocker, living_documentation_generator, config_repository): # Arrange - mock_get_repo = generator._LivingDocumentationGenerator__github_instance.get_repo + mock_get_repo = living_documentation_generator._LivingDocumentationGenerator__github_instance.get_repo mock_get_repo.return_value = None mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_repositories", return_value=[config_repository] + "living_documentation_regime.living_documentation_generator.ActionInputs.get_repositories", + return_value=[config_repository], ) # Act - actual = generator._fetch_github_issues() + actual = living_documentation_generator._fetch_github_issues() # Assert assert {} == actual @@ -212,13 +220,14 @@ def test_fetch_github_issues_repository_none(mocker, generator, config_repositor # _fetch_github_project_issues -def test_fetch_github_project_issues_correct_behaviour(mocker, generator): +def test_fetch_github_project_issues_correct_behaviour(mocker, living_documentation_generator): # Arrange mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_project_state_mining_enabled", + return_value=True, ) - mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info") - mock_logger_debug = mocker.patch("living_documentation_generator.generator.logger.debug") + mock_logger_info = mocker.patch("living_documentation_regime.living_documentation_generator.logger.info") + mock_logger_debug = mocker.patch("living_documentation_regime.living_documentation_generator.logger.debug") repository_1 = mocker.Mock() repository_1.organization_name = "OrgA" @@ -231,19 +240,22 @@ def test_fetch_github_project_issues_correct_behaviour(mocker, generator): repository_2.projects_title_filter = "ProjectB" mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_repositories", + "living_documentation_regime.living_documentation_generator.ActionInputs.get_repositories", return_value=[repository_1, repository_2], ) mock_github_projects_instance = mocker.patch.object( - generator, "_LivingDocumentationGenerator__github_projects_instance" + living_documentation_generator, "_LivingDocumentationGenerator__github_projects_instance" ) repo_a = mocker.Mock() repo_a.full_name = "OrgA/RepoA" repo_b = mocker.Mock() repo_b.full_name = "OrgA/RepoB" - generator._LivingDocumentationGenerator__github_instance.get_repo.side_effect = [repo_a, repo_b] + living_documentation_generator._LivingDocumentationGenerator__github_instance.get_repo.side_effect = [ + repo_a, + repo_b, + ] project_a = mocker.Mock(title="Project A") project_b = mocker.Mock(title="Project B") @@ -272,12 +284,12 @@ def test_fetch_github_project_issues_correct_behaviour(mocker, generator): mock_github_projects_instance.get_project_issues.side_effect = [[project_issue_1], [project_issue_2]] mock_make_issue_key = mocker.patch( - "living_documentation_generator.generator.make_issue_key", + "living_documentation_regime.living_documentation_generator.make_issue_key", side_effect=lambda org, repo, num: f"{org}/{repo}#{num}", ) # Act - actual = generator._fetch_github_project_issues() + actual = living_documentation_generator._fetch_github_project_issues() # Assert assert mock_make_issue_key.call_count == 2 @@ -285,8 +297,8 @@ def test_fetch_github_project_issues_correct_behaviour(mocker, generator): assert "OrgA/RepoA#1" in actual assert actual["OrgA/RepoA#1"] == [project_issue_1, project_issue_2] - generator._LivingDocumentationGenerator__github_instance.get_repo.assert_any_call("OrgA/RepoA") - generator._LivingDocumentationGenerator__github_instance.get_repo.assert_any_call("OrgA/RepoB") + living_documentation_generator._LivingDocumentationGenerator__github_instance.get_repo.assert_any_call("OrgA/RepoA") + living_documentation_generator._LivingDocumentationGenerator__github_instance.get_repo.assert_any_call("OrgA/RepoB") mock_github_projects_instance.get_repository_projects.assert_any_call(repository=repo_a, projects_title_filter="") mock_github_projects_instance.get_repository_projects.assert_any_call( repository=repo_b, projects_title_filter="ProjectB" @@ -316,15 +328,16 @@ def test_fetch_github_project_issues_correct_behaviour(mocker, generator): ) -def test_fetch_github_project_issues_project_mining_disabled(mocker, generator): +def test_fetch_github_project_issues_project_mining_disabled(mocker, living_documentation_generator): # Arrange mock_get_project_mining_enabled = mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=False + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_project_state_mining_enabled", + return_value=False, ) - mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info") + mock_logger_info = mocker.patch("living_documentation_regime.living_documentation_generator.logger.info") # Act - actual = generator._fetch_github_project_issues() + actual = living_documentation_generator._fetch_github_project_issues() # Assert assert {} == actual @@ -332,46 +345,52 @@ def test_fetch_github_project_issues_project_mining_disabled(mocker, generator): mock_logger_info.assert_called_once_with("Fetching GitHub project data - project mining is not allowed.") -def test_fetch_github_project_issues_no_repositories(mocker, generator, config_repository): +def test_fetch_github_project_issues_no_repositories(mocker, living_documentation_generator, config_repository): # Arrange - mock_get_repo = generator._LivingDocumentationGenerator__github_instance.get_repo + mock_get_repo = living_documentation_generator._LivingDocumentationGenerator__github_instance.get_repo mock_get_repo.return_value = None mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_project_state_mining_enabled", + return_value=True, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_repositories", return_value=[config_repository] + "living_documentation_regime.living_documentation_generator.ActionInputs.get_repositories", + return_value=[config_repository], ) # Act - actual = generator._fetch_github_project_issues() + actual = living_documentation_generator._fetch_github_project_issues() # Assert assert {} == actual mock_get_repo.assert_called_once_with("test_org/test_repo") -def test_fetch_github_project_issues_with_no_projects(mocker, generator, config_repository): +def test_fetch_github_project_issues_with_no_projects(mocker, living_documentation_generator, config_repository): # Arrange - mock_get_repo = generator._LivingDocumentationGenerator__github_instance.get_repo + mock_get_repo = living_documentation_generator._LivingDocumentationGenerator__github_instance.get_repo repo_a = mocker.Mock() repo_a.full_name = "test_org/test_repo" mock_get_repo.return_value = repo_a mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_project_state_mining_enabled", + return_value=True, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_repositories", return_value=[config_repository] + "living_documentation_regime.living_documentation_generator.ActionInputs.get_repositories", + return_value=[config_repository], ) - mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info") + mock_logger_info = mocker.patch("living_documentation_regime.living_documentation_generator.logger.info") mock_get_repository_projects = mocker.patch.object( - generator._LivingDocumentationGenerator__github_projects_instance, "get_repository_projects", return_value=[] + living_documentation_generator._LivingDocumentationGenerator__github_projects_instance, + "get_repository_projects", + return_value=[], ) # Act - actual = generator._fetch_github_project_issues() + actual = living_documentation_generator._fetch_github_project_issues() # Assert assert {} == actual @@ -385,7 +404,7 @@ def test_fetch_github_project_issues_with_no_projects(mocker, generator, config_ # _consolidate_issues_data -def test_consolidate_issues_data_correct_behaviour(mocker, generator): +def test_consolidate_issues_data_correct_behaviour(mocker, living_documentation_generator): # Arrange consolidated_issue_mock_1 = mocker.Mock(spec=ConsolidatedIssue) consolidated_issue_mock_2 = mocker.Mock(spec=ConsolidatedIssue) @@ -395,19 +414,19 @@ def test_consolidate_issues_data_correct_behaviour(mocker, generator): "TestOrg/TestRepo#2": [mocker.Mock(spec=ProjectIssue, project_status="Done")], } - mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info") - mock_logger_debug = mocker.patch("living_documentation_generator.generator.logger.debug") + mock_logger_info = mocker.patch("living_documentation_regime.living_documentation_generator.logger.info") + mock_logger_debug = mocker.patch("living_documentation_regime.living_documentation_generator.logger.debug") mock_make_issue_key = mocker.patch( - "living_documentation_generator.generator.make_issue_key", + "living_documentation_regime.living_documentation_generator.make_issue_key", side_effect=lambda org, repo, num: f"{org}/{repo}#{num}", ) mock_consolidated_issue_class = mocker.patch( - "living_documentation_generator.generator.ConsolidatedIssue", + "living_documentation_regime.living_documentation_generator.ConsolidatedIssue", side_effect=[consolidated_issue_mock_1, consolidated_issue_mock_2], ) # Act - actual = generator._consolidate_issues_data(repository_issues, project_issues) + actual = living_documentation_generator._consolidate_issues_data(repository_issues, project_issues) # Assert assert 2 == len(actual) @@ -427,7 +446,7 @@ def test_consolidate_issues_data_correct_behaviour(mocker, generator): def test_generate_markdown_pages_with_structured_output_and_topic_grouping_enabled( - mocker, generator, consolidated_issue, load_all_templates_setup + mocker, living_documentation_generator, consolidated_issue, load_all_templates_setup ): # Arrange mock_load_all_templates = load_all_templates_setup @@ -435,26 +454,29 @@ def test_generate_markdown_pages_with_structured_output_and_topic_grouping_enabl issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue} mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_structured_output_enabled", + return_value=True, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_grouping_by_topics_enabled", + return_value=True, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output" + "living_documentation_regime.living_documentation_generator.make_absolute_path", + return_value="/base/output", ) mock_generate_root_level_index_page = mocker.patch( - "living_documentation_generator.generator.generate_root_level_index_page" + "living_documentation_regime.living_documentation_generator.generate_root_level_index_page" ) mock_generate_structured_index_pages = mocker.patch.object( LivingDocumentationGenerator, "_generate_structured_index_pages" ) mock_generate_index_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_index_page") - mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info") + mock_logger_info = mocker.patch("living_documentation_regime.living_documentation_generator.logger.info") mock_generate_md_issue_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_md_issue_page") # Act - generator._generate_markdown_pages(issues) + living_documentation_generator._generate_markdown_pages(issues) # Assert assert 2 == mock_generate_md_issue_page.call_count @@ -469,7 +491,7 @@ def test_generate_markdown_pages_with_structured_output_and_topic_grouping_enabl def test_generate_markdown_pages_with_structured_output_enabled_and_topic_grouping_disabled( - mocker, generator, consolidated_issue, load_all_templates_setup + mocker, living_documentation_generator, consolidated_issue, load_all_templates_setup ): # Arrange topics = {"documentationTopic", "FETopic"} @@ -477,26 +499,29 @@ def test_generate_markdown_pages_with_structured_output_enabled_and_topic_groupi issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue, "issue_3": consolidated_issue} mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_structured_output_enabled", + return_value=True, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_grouping_by_topics_enabled", + return_value=False, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output" + "living_documentation_regime.living_documentation_generator.make_absolute_path", + return_value="/base/output", ) mock_generate_md_issue_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_md_issue_page") mock_generate_root_level_index_page = mocker.patch( - "living_documentation_generator.generator.generate_root_level_index_page" + "living_documentation_regime.living_documentation_generator.generate_root_level_index_page" ) mock_generate_structured_index_pages = mocker.patch.object( LivingDocumentationGenerator, "_generate_structured_index_pages" ) mock_generate_index_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_index_page") - mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info") + mock_logger_info = mocker.patch("living_documentation_regime.living_documentation_generator.logger.info") # Act - generator._generate_markdown_pages(issues) + living_documentation_generator._generate_markdown_pages(issues) # Assert assert 3 == mock_generate_md_issue_page.call_count @@ -510,32 +535,35 @@ def test_generate_markdown_pages_with_structured_output_enabled_and_topic_groupi def test_generate_markdown_pages_with_structured_output_and_topic_grouping_disabled( - mocker, generator, consolidated_issue, load_all_templates_setup + mocker, living_documentation_generator, consolidated_issue, load_all_templates_setup ): # Arrange issues = {"issue_1": consolidated_issue} mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=False + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_structured_output_enabled", + return_value=False, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_grouping_by_topics_enabled", + return_value=False, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output" + "living_documentation_regime.living_documentation_generator.make_absolute_path", + return_value="/base/output", ) mock_generate_md_issue_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_md_issue_page") mock_generate_root_level_index_page = mocker.patch( - "living_documentation_generator.generator.generate_root_level_index_page" + "living_documentation_regime.living_documentation_generator.generate_root_level_index_page" ) mock_generate_structured_index_pages = mocker.patch.object( LivingDocumentationGenerator, "_generate_structured_index_pages" ) mock_generate_index_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_index_page") - mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info") + mock_logger_info = mocker.patch("living_documentation_regime.living_documentation_generator.logger.info") # Act - generator._generate_markdown_pages(issues) + living_documentation_generator._generate_markdown_pages(issues) # Assert load_all_templates_setup.assert_called_once() @@ -549,24 +577,27 @@ def test_generate_markdown_pages_with_structured_output_and_topic_grouping_disab def test_generate_markdown_pages_with_topic_grouping_enabled_and_structured_output_disabled( - mocker, generator, consolidated_issue, load_all_templates_setup + mocker, living_documentation_generator, consolidated_issue, load_all_templates_setup ): # Arrange consolidated_issue.topics = ["documentationTopic", "FETopic"] issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue} mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=False + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_structured_output_enabled", + return_value=False, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_grouping_by_topics_enabled", + return_value=True, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output" + "living_documentation_regime.living_documentation_generator.make_absolute_path", + return_value="/base/output", ) mock_generate_md_issue_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_md_issue_page") mock_generate_root_level_index_page = mocker.patch( - "living_documentation_generator.generator.generate_root_level_index_page" + "living_documentation_regime.living_documentation_generator.generate_root_level_index_page" ) mock_generate_structured_index_pages = mocker.patch.object( LivingDocumentationGenerator, "_generate_structured_index_pages" @@ -574,7 +605,7 @@ def test_generate_markdown_pages_with_topic_grouping_enabled_and_structured_outp mock_generate_index_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_index_page") # Act - generator._generate_markdown_pages(issues) + living_documentation_generator._generate_markdown_pages(issues) # Assert assert 2 == mock_generate_md_issue_page.call_count @@ -591,7 +622,7 @@ def test_generate_markdown_pages_with_topic_grouping_enabled_and_structured_outp # _generate_md_issue_page -def test_generate_md_issue_page(mocker, generator, consolidated_issue): +def test_generate_md_issue_page(mocker, living_documentation_generator, consolidated_issue): # Arrange issue_page_template = "Title: {title}\nDate: {date}\nSummary:\n{issue_summary_table}\nContent:\n{issue_content}" consolidated_issue.generate_directory_path = mocker.Mock(return_value=["/base/output/org/repo/issues"]) @@ -606,7 +637,7 @@ def test_generate_md_issue_page(mocker, generator, consolidated_issue): mock_open = mocker.patch("builtins.open", mocker.mock_open()) # Act - generator._generate_md_issue_page(issue_page_template, consolidated_issue) + living_documentation_generator._generate_md_issue_page(issue_page_template, consolidated_issue) # Assert mock_generate_issue_summary_table.assert_called_once_with(consolidated_issue) @@ -618,7 +649,9 @@ def test_generate_md_issue_page(mocker, generator, consolidated_issue): # _generate_structured_index_pages -def test_generate_structured_index_pages_with_topic_grouping_enabled(mocker, generator, consolidated_issue): +def test_generate_structured_index_pages_with_topic_grouping_enabled( + mocker, living_documentation_generator, consolidated_issue +): # Arrange index_data_level_template = "Data Level Template" index_repo_level_template = "Repo Level Template" @@ -627,17 +660,18 @@ def test_generate_structured_index_pages_with_topic_grouping_enabled(mocker, gen consolidated_issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue} mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_grouping_by_topics_enabled", + return_value=True, ) mock_generate_sub_level_index_page = mocker.patch.object( LivingDocumentationGenerator, "_generate_sub_level_index_page" ) mock_generate_index_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_index_page") - mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info") - mock_logger_debug = mocker.patch("living_documentation_generator.generator.logger.debug") + mock_logger_info = mocker.patch("living_documentation_regime.living_documentation_generator.logger.info") + mock_logger_debug = mocker.patch("living_documentation_regime.living_documentation_generator.logger.debug") # Act - generator._generate_structured_index_pages( + living_documentation_generator._generate_structured_index_pages( index_data_level_template, index_repo_level_template, index_org_level_template, @@ -661,7 +695,9 @@ def test_generate_structured_index_pages_with_topic_grouping_enabled(mocker, gen ) -def test_generate_structured_index_pages_with_topic_grouping_disabled(mocker, generator, consolidated_issue): +def test_generate_structured_index_pages_with_topic_grouping_disabled( + mocker, living_documentation_generator, consolidated_issue +): # Arrange index_data_level_template = "Data Level Template" index_repo_level_template = "Repo Level Template" @@ -671,16 +707,17 @@ def test_generate_structured_index_pages_with_topic_grouping_disabled(mocker, ge consolidated_issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue} mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_grouping_by_topics_enabled", + return_value=False, ) mock_generate_sub_level_index_page = mocker.patch.object( LivingDocumentationGenerator, "_generate_sub_level_index_page" ) mock_generate_index_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_index_page") - mock_logger_debug = mocker.patch("living_documentation_generator.generator.logger.debug") + mock_logger_debug = mocker.patch("living_documentation_regime.living_documentation_generator.logger.debug") # Act - generator._generate_structured_index_pages( + living_documentation_generator._generate_structured_index_pages( index_data_level_template, index_repo_level_template, index_org_level_template, @@ -704,7 +741,9 @@ def test_generate_structured_index_pages_with_topic_grouping_disabled(mocker, ge """ -def test_generate_index_page_with_all_features_enabled(mocker, generator, consolidated_issue, project_status): +def test_generate_index_page_with_all_features_enabled( + mocker, living_documentation_generator, consolidated_issue, project_status +): # Arrange issue_index_page_template = "Date: {date}\nIssues:\n{issue_overview_table}\nData Level: {data_level_name}" consolidated_issue.linked_to_project = True @@ -724,13 +763,16 @@ def test_generate_index_page_with_all_features_enabled(mocker, generator, consol expected_output_path = "/base/output/org/repo/topic/_index.md" mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_project_state_mining_enabled", + return_value=True, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_grouping_by_topics_enabled", + return_value=True, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_structured_output_enabled", + return_value=True, ) mock_generate_index_directory_path = mocker.patch.object( LivingDocumentationGenerator, "_generate_index_directory_path", return_value="/base/output/org/repo/topic" @@ -739,7 +781,9 @@ def test_generate_index_page_with_all_features_enabled(mocker, generator, consol mocker.patch("os.makedirs") # Act - generator._generate_index_page(issue_index_page_template, consolidated_issues, repository_id, grouping_topic) + living_documentation_generator._generate_index_page( + issue_index_page_template, consolidated_issues, repository_id, grouping_topic + ) # Assert mock_generate_index_directory_path.assert_called_once_with(repository_id, grouping_topic) @@ -748,7 +792,7 @@ def test_generate_index_page_with_all_features_enabled(mocker, generator, consol def test_generate_index_page_with_topic_grouping_disabled_structured_output_project_mining_enabled( - mocker, generator, consolidated_issue, project_status + mocker, living_documentation_generator, consolidated_issue, project_status ): # Arrange issue_index_page_template = "Date: {date}\nIssues:\n{issue_overview_table}\nData Level: {data_level_name}" @@ -769,13 +813,16 @@ def test_generate_index_page_with_topic_grouping_disabled_structured_output_proj expected_output_path = "/base/output/org/repo/_index.md" mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_project_state_mining_enabled", + return_value=True, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_grouping_by_topics_enabled", + return_value=False, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_structured_output_enabled", + return_value=True, ) mock_generate_index_directory_path = mocker.patch.object( LivingDocumentationGenerator, "_generate_index_directory_path", return_value="/base/output/org/repo" @@ -784,7 +831,9 @@ def test_generate_index_page_with_topic_grouping_disabled_structured_output_proj mocker.patch("os.makedirs") # Act - generator._generate_index_page(issue_index_page_template, consolidated_issues, repository_id, grouping_topic) + living_documentation_generator._generate_index_page( + issue_index_page_template, consolidated_issues, repository_id, grouping_topic + ) # Assert mock_generate_index_directory_path.assert_called_once_with(repository_id, grouping_topic) @@ -793,7 +842,7 @@ def test_generate_index_page_with_topic_grouping_disabled_structured_output_proj def test_generate_index_page_with_topic_grouping_and_structured_output_disabled_project_mining_enabled( - mocker, generator, consolidated_issue, project_status + mocker, living_documentation_generator, consolidated_issue, project_status ): # Arrange issue_index_page_template = "Date: {date}\nIssues:\n{issue_overview_table}\n" @@ -810,13 +859,16 @@ def test_generate_index_page_with_topic_grouping_and_structured_output_disabled_ expected_output_path = "/base/output/_index.md" mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_project_state_mining_enabled", + return_value=True, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_grouping_by_topics_enabled", + return_value=False, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=False + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_structured_output_enabled", + return_value=False, ) mock_generate_index_directory_path = mocker.patch.object( LivingDocumentationGenerator, "_generate_index_directory_path", return_value="/base/output" @@ -825,7 +877,9 @@ def test_generate_index_page_with_topic_grouping_and_structured_output_disabled_ mocker.patch("os.makedirs") # Act - generator._generate_index_page(issue_index_page_template, consolidated_issues, repository_id, grouping_topic) + living_documentation_generator._generate_index_page( + issue_index_page_template, consolidated_issues, repository_id, grouping_topic + ) # Assert mock_generate_index_directory_path.assert_called_once_with(repository_id, grouping_topic) @@ -836,7 +890,7 @@ def test_generate_index_page_with_topic_grouping_and_structured_output_disabled_ # _generate_sub_level_index_page -def test_generate_sub_level_index_page_for_org_level(mocker, generator): +def test_generate_sub_level_index_page_for_org_level(mocker, living_documentation_generator): # Arrange index_template = "Organization: {organization_name}, Date: {date}" level = "org" @@ -845,13 +899,14 @@ def test_generate_sub_level_index_page_for_org_level(mocker, generator): expected_output_path = "/base/output/TestOrg/_index.md" mock_get_output_directory = mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output" + "living_documentation_regime.living_documentation_generator.make_absolute_path", + return_value="/base/output", ) mock_open = mocker.patch("builtins.open", mocker.mock_open()) mocker.patch("os.makedirs") # Act - generator._generate_sub_level_index_page(index_template, level, repository_id) + living_documentation_generator._generate_sub_level_index_page(index_template, level, repository_id) # Assert mock_get_output_directory.assert_called_once() @@ -859,7 +914,7 @@ def test_generate_sub_level_index_page_for_org_level(mocker, generator): mock_open().write.assert_called_once_with(expected_replacement_content) -def test_generate_sub_level_index_page_for_repo_level(mocker, generator): +def test_generate_sub_level_index_page_for_repo_level(mocker, living_documentation_generator): # Arrange index_template = "Repository: {repository_name}, Date: {date}" level = "repo" @@ -868,13 +923,14 @@ def test_generate_sub_level_index_page_for_repo_level(mocker, generator): expected_output_path = "/base/output/TestOrg/TestRepo/_index.md" mock_get_output_directory = mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output" + "living_documentation_regime.living_documentation_generator.make_absolute_path", + return_value="/base/output", ) mock_open = mocker.patch("builtins.open", mocker.mock_open()) mocker.patch("os.makedirs") # Act - generator._generate_sub_level_index_page(index_template, level, repository_id) + living_documentation_generator._generate_sub_level_index_page(index_template, level, repository_id) # Assert mock_get_output_directory.assert_called_once() @@ -897,7 +953,8 @@ def test_generate_markdown_line_with_project_state_mining_enabled_linked_to_proj ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_project_state_mining_enabled", + return_value=True, ) # Act @@ -918,7 +975,8 @@ def test_generate_markdown_line_with_project_state_mining_enabled_linked_to_proj ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_project_state_mining_enabled", + return_value=True, ) # Act @@ -937,7 +995,8 @@ def test_generate_markdown_line_with_project_state_mining_disabled(mocker, conso ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=False + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_project_state_mining_enabled", + return_value=False, ) # Act @@ -950,7 +1009,9 @@ def test_generate_markdown_line_with_project_state_mining_disabled(mocker, conso # _generate_issue_summary_table -def test_generate_issue_summary_table_without_project_state_mining(mocker, generator, consolidated_issue): +def test_generate_issue_summary_table_without_project_state_mining( + mocker, living_documentation_generator, consolidated_issue +): # Arrange expected_issue_info = ( "| Attribute | Content |\n" @@ -967,10 +1028,11 @@ def test_generate_issue_summary_table_without_project_state_mining(mocker, gener ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=False + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_project_state_mining_enabled", + return_value=False, ) - actual = generator._generate_issue_summary_table(consolidated_issue) + actual = living_documentation_generator._generate_issue_summary_table(consolidated_issue) assert expected_issue_info == actual @@ -1006,7 +1068,8 @@ def test_generate_issue_summary_table_with_project_state_mining_and_multiple_pro ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_project_state_mining_enabled", + return_value=True, ) # Act @@ -1037,7 +1100,8 @@ def test_generate_issue_summary_table_with_project_state_mining_but_no_linked_pr ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_project_state_mining_enabled", + return_value=True, ) # Act @@ -1056,13 +1120,16 @@ def test_generate_index_directory_path_with_structured_output_grouped_by_topics( expected_path = "/base/output/org123/repo456/documentation" mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output" + "living_documentation_regime.living_documentation_generator.make_absolute_path", + return_value="/base/output", ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_structured_output_enabled", + return_value=True, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_grouping_by_topics_enabled", + return_value=True, ) mocker.patch("os.makedirs") @@ -1080,13 +1147,16 @@ def test_generate_index_directory_path_with_structured_output_not_grouped_by_top expected_path = "/base/output/org123/repo456" mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output" + "living_documentation_regime.living_documentation_generator.make_absolute_path", + return_value="/base/output", ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_structured_output_enabled", + return_value=True, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_grouping_by_topics_enabled", + return_value=False, ) mocker.patch("os.makedirs") @@ -1104,13 +1174,16 @@ def test_generate_index_directory_path_with_only_grouping_by_topic_no_structured expected_path = "/base/output/documentation" mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output" + "living_documentation_regime.living_documentation_generator.make_absolute_path", + return_value="/base/output", ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=False + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_structured_output_enabled", + return_value=False, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_grouping_by_topics_enabled", + return_value=True, ) mocker.patch("os.makedirs") @@ -1127,13 +1200,16 @@ def test_generate_index_directory_path_with_no_structured_output_and_no_grouping expected_path = "/base/output" mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output" + "living_documentation_regime.living_documentation_generator.make_absolute_path", + return_value="/base/output", ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=False + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_structured_output_enabled", + return_value=False, ) mocker.patch( - "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False + "living_documentation_regime.living_documentation_generator.ActionInputs.get_is_grouping_by_topics_enabled", + return_value=False, ) mocker.patch("os.makedirs") @@ -1158,7 +1234,7 @@ def test_load_all_templates_loads_correctly(mocker): "Data Level Template Content", ) - load_template_mock = mocker.patch("living_documentation_generator.generator.load_template") + load_template_mock = mocker.patch("living_documentation_regime.living_documentation_generator.load_template") load_template_mock.side_effect = [ "Issue Page Template Content", "Index Page Template Content", @@ -1186,7 +1262,7 @@ def test_load_all_templates_loads_just_some_templates(mocker): "Data Level Template Content", ) - load_template_mock = mocker.patch("living_documentation_generator.generator.load_template") + load_template_mock = mocker.patch("living_documentation_regime.living_documentation_generator.load_template") load_template_mock.side_effect = [ None, None, diff --git a/tests/model/test_consolidated_issue.py b/tests/model/test_consolidated_issue.py deleted file mode 100644 index d56f990..0000000 --- a/tests/model/test_consolidated_issue.py +++ /dev/null @@ -1,172 +0,0 @@ -# -# Copyright 2024 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. -# -from github.Issue import Issue - -from living_documentation_generator.model.consolidated_issue import ConsolidatedIssue - - -# generate_page_filename - - -def test_generate_page_filename_correct_behaviour(): - mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True) - consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue) - - actual = consolidated_issue.generate_page_filename() - - assert "1_issue_title.md" == actual - - -def test_generate_page_filename_with_none_title(mocker): - mock_log_error = mocker.patch("living_documentation_generator.model.consolidated_issue.logger.error") - mock_issue = Issue(None, None, {"number": 1, "title": None}, completed=True) - consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue) - - actual = consolidated_issue.generate_page_filename() - - assert "1.md" == actual - mock_log_error.assert_called_once_with( - "Issue page filename generation failed for Issue %s/%s (%s). Issue does not have a title.", - "organization", - "repository", - 1, - exc_info=True, - ) - - -# generate_directory_path - - -def test_generate_directory_path_structured_output_disabled_grouping_by_topics_disabled(mocker): - mocker.patch( - "living_documentation_generator.action_inputs.ActionInputs.get_output_directory", - return_value="/base/output/path", - ) - mocker.patch( - "living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=False - ) - mocker.patch( - "living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", - return_value=False, - ) - mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True) - consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue) - - actual = consolidated_issue.generate_directory_path("issue table") - - assert ["/base/output/path"] == actual - - -def test_generate_directory_path_structured_output_enabled_grouping_by_topics_disabled(mocker): - mocker.patch( - "living_documentation_generator.action_inputs.ActionInputs.get_output_directory", - return_value="/base/output/path", - ) - mocker.patch( - "living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=True - ) - mocker.patch( - "living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", - return_value=False, - ) - mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True) - consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue) - - actual = consolidated_issue.generate_directory_path("issue table") - - assert ["/base/output/path/organization/repository"] == actual - - -def test_generate_directory_path_structured_output_disabled_grouping_by_topics_enabled_two_issue_topics(mocker): - mock_log_debug = mocker.patch("living_documentation_generator.model.consolidated_issue.logger.debug") - mocker.patch( - "living_documentation_generator.action_inputs.ActionInputs.get_output_directory", - return_value="/base/output/path", - ) - mocker.patch( - "living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=False - ) - mocker.patch( - "living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True - ) - mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True) - consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue) - - actual = consolidated_issue.generate_directory_path("| Labels | feature, BETopic, FETopic |") - - assert ["/base/output/path/BETopic", "/base/output/path/FETopic"] == actual - mock_log_debug.assert_called_once_with( - "Multiple Topic labels found for Issue #%s: %s (%s): %s", - 1, - "Issue Title", - "organization/repository", - "BETopic, FETopic", - ) - - -def test_generate_directory_path_structured_output_disabled_grouping_by_topics_enabled_no_issue_topics(mocker): - mock_log_error = mocker.patch("living_documentation_generator.model.consolidated_issue.logger.error") - mock_log_debug = mocker.patch("living_documentation_generator.model.consolidated_issue.logger.debug") - mocker.patch( - "living_documentation_generator.action_inputs.ActionInputs.get_output_directory", - return_value="/base/output/path", - ) - mocker.patch( - "living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=False - ) - mocker.patch( - "living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True - ) - mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True) - consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue) - - actual = consolidated_issue.generate_directory_path("| Labels | feature, bug |") - - assert ["/base/output/path/NoTopic"] == actual - mock_log_error.assert_called_once_with( - "No Topic label found for Issue #%i: %s (%s)", 1, "Issue Title", "organization/repository" - ) - mock_log_debug.assert_not_called() - - -def test_generate_directory_path_structured_output_enabled_grouping_by_topics_enabled_one_issue_topic(mocker): - mock_log_debug = mocker.patch("living_documentation_generator.model.consolidated_issue.logger.debug") - mocker.patch( - "living_documentation_generator.action_inputs.ActionInputs.get_output_directory", - return_value="/base/output/path", - ) - mocker.patch( - "living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=True - ) - mocker.patch( - "living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True - ) - mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True) - consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue) - - actual = consolidated_issue.generate_directory_path("| Labels | feature, BETopic, FETopic |") - - assert [ - "/base/output/path/organization/repository/BETopic", - "/base/output/path/organization/repository/FETopic", - ] == actual - mock_log_debug.assert_called_once_with( - "Multiple Topic labels found for Issue #%s: %s (%s): %s", - 1, - "Issue Title", - "organization/repository", - "BETopic, FETopic", - ) diff --git a/tests/test_main.py b/tests/test_main.py index a292ab5..e882736 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -15,32 +15,61 @@ # import os -from living_documentation_generator.generator import LivingDocumentationGenerator from main import run +from utils.constants import OUTPUT_PATH # run -def test_run_correct_behaviour(mocker): +def test_run_correct_behaviour_with_all_regimes_enabled(mocker): # Arrange + expected_output_path = os.path.abspath(OUTPUT_PATH) mock_log_info = mocker.patch("logging.getLogger").return_value.info - mock_get_action_input = mocker.patch("main.get_action_input") - mock_get_action_input.side_effect = lambda first_arg, **kwargs: ( - "./user/output/path" if first_arg == "OUTPUT_PATH" else None + mock_living_doc_generator = mocker.patch("main.LivingDocumentationGenerator") + mocker.patch.dict( + os.environ, + { + "INPUT_GITHUB_TOKEN": "fake_token", + "INPUT_LIV_DOC_REGIME": "true", + "INPUT_OUTPUT_PATH": "./user/output/path", + }, ) - mocker.patch("main.ActionInputs.get_output_directory", return_value="./user/output/path") - mocker.patch.dict(os.environ, {"INPUT_GITHUB_TOKEN": "fake_token"}) - mocker.patch.object(LivingDocumentationGenerator, "generate") # Act run() # Assert + mock_living_doc_generator.assert_called_once() mock_log_info.assert_has_calls( [ mocker.call("Starting Living Documentation generation."), - mocker.call("Living Documentation generation - output path set to `%s`.", "./user/output/path"), + mocker.call("Living Documentation generation - Starting the `LivDoc` generation regime."), + mocker.call("Living Documentation generation - `LivDoc` generation regime completed."), + mocker.call("Living Documentation generation - output path set to `%s`.", expected_output_path), mocker.call("Living Documentation generation completed."), - ] + ], + any_order=False, + ) + + +def test_run_with_zero_regimes_enabled(mocker): + # Arrange + mock_log_info = mocker.patch("logging.getLogger").return_value.info + mocker.patch.dict(os.environ, {"INPUT_GITHUB_TOKEN": "fake_token", "INPUT_LIV_DOC_REGIME": "false"}) + mock_living_doc_generator = mocker.patch("main.LivingDocumentationGenerator") + expected_output_path = os.path.abspath("./output") # Adding the default value + + # Act + run() + + # Assert + mock_living_doc_generator.assert_not_called() + mock_log_info.assert_has_calls( + [ + mocker.call("Starting Living Documentation generation."), + mocker.call("Living Documentation generation - output path set to `%s`.", expected_output_path), + mocker.call("Living Documentation generation completed."), + ], + any_order=False, ) diff --git a/tests/utils/test_decorators.py b/tests/utils/test_decorators.py index c2ca273..8d2fe8a 100644 --- a/tests/utils/test_decorators.py +++ b/tests/utils/test_decorators.py @@ -17,7 +17,7 @@ from github import GithubException from requests import RequestException -from living_documentation_generator.utils.decorators import debug_log_decorator, safe_call_decorator +from utils.decorators import debug_log_decorator, safe_call_decorator # sample function to be decorated @@ -30,7 +30,7 @@ def sample_function(x, y): def test_debug_log_decorator(mocker): # Mock logging - mock_log_debug = mocker.patch("living_documentation_generator.utils.decorators.logger.debug") + mock_log_debug = mocker.patch("utils.decorators.logger.debug") decorated_function = debug_log_decorator(sample_function) expected_call = [ @@ -58,7 +58,7 @@ def sample_method(x, y): def test_safe_call_decorator_network_error(rate_limiter, mocker): - mock_log_error = mocker.patch("living_documentation_generator.utils.decorators.logger.error") + mock_log_error = mocker.patch("utils.decorators.logger.error") @safe_call_decorator(rate_limiter) def sample_method(): @@ -77,7 +77,7 @@ def sample_method(): def test_safe_call_decorator_github_api_error(rate_limiter, mocker): - mock_log_error = mocker.patch("living_documentation_generator.utils.decorators.logger.error") + mock_log_error = mocker.patch("utils.decorators.logger.error") @safe_call_decorator(rate_limiter) def sample_method(): @@ -102,7 +102,7 @@ def sample_method(): def test_safe_call_decorator_http_error(mocker, rate_limiter): - mock_log_error = mocker.patch("living_documentation_generator.utils.decorators.logger.error") + mock_log_error = mocker.patch("utils.decorators.logger.error") @safe_call_decorator(rate_limiter) def sample_method(): @@ -121,7 +121,7 @@ def sample_method(): def test_safe_call_decorator_exception(rate_limiter, mocker): - mock_log_error = mocker.patch("living_documentation_generator.utils.decorators.logger.error") + mock_log_error = mocker.patch("utils.decorators.logger.error") @safe_call_decorator(rate_limiter) def sample_method(x, y): diff --git a/tests/utils/test_github_project_queries.py b/tests/utils/test_github_project_queries.py index a490fce..73f4b44 100644 --- a/tests/utils/test_github_project_queries.py +++ b/tests/utils/test_github_project_queries.py @@ -16,13 +16,13 @@ import re -from living_documentation_generator.utils.constants import ( +from utils.constants import ( PROJECTS_FROM_REPO_QUERY, ISSUES_FROM_PROJECT_QUERY, PROJECT_FIELD_OPTIONS_QUERY, ISSUES_PER_PAGE_LIMIT, ) -from living_documentation_generator.utils.github_project_queries import ( +from utils.github_project_queries import ( get_projects_from_repo_query, get_issues_from_project_query, get_project_field_options_query, diff --git a/tests/utils/test_logging_config.py b/tests/utils/test_logging_config.py index b455c6a..cb18470 100644 --- a/tests/utils/test_logging_config.py +++ b/tests/utils/test_logging_config.py @@ -19,7 +19,7 @@ import sys from logging import StreamHandler -from living_documentation_generator.utils.logging_config import setup_logging +from utils.logging_config import setup_logging def validate_logging_config(mock_logging_setup, caplog, expected_level, expected_message): diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index e7864ed..cf17031 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -16,7 +16,7 @@ import pytest -from living_documentation_generator.utils.utils import ( +from utils.utils import ( make_issue_key, sanitize_filename, validate_query_format, @@ -86,7 +86,7 @@ def test_get_input_without_hyphen(mocker): def test_validate_query_format_right_behaviour(mocker): mock_exit = mocker.patch("sys.exit", return_value=None) - mock_log_error = mocker.patch("living_documentation_generator.utils.utils.logger.error") + mock_log_error = mocker.patch("utils.utils.logger.error") # Test case where there are no missing or extra placeholders query_string = "This is a query with placeholders {placeholder1} and {placeholder2}" @@ -98,7 +98,7 @@ def test_validate_query_format_right_behaviour(mocker): def test_validate_query_format_missing_placeholder(mocker): mock_exit = mocker.patch("sys.exit", return_value=None) - mock_log_error = mocker.patch("living_documentation_generator.utils.utils.logger.error") + mock_log_error = mocker.patch("utils.utils.logger.error") # Test case where there are missing placeholders query_string = "This is a query with placeholders {placeholder1} and {placeholder2}" @@ -115,7 +115,7 @@ def test_validate_query_format_missing_placeholder(mocker): def test_validate_query_format_extra_placeholder(mocker): mock_exit = mocker.patch("sys.exit", return_value=None) - mock_log_error = mocker.patch("living_documentation_generator.utils.utils.logger.error") + mock_log_error = mocker.patch("utils.utils.logger.error") # Test case where there are extra placeholders query_string = "This is a query with placeholders {placeholder1} and {placeholder2}" diff --git a/tests/model/__init__.py b/utils/__init__.py similarity index 100% rename from tests/model/__init__.py rename to utils/__init__.py diff --git a/living_documentation_generator/utils/constants.py b/utils/constants.py similarity index 90% rename from living_documentation_generator/utils/constants.py rename to utils/constants.py index e7d13ca..35bf080 100644 --- a/living_documentation_generator/utils/constants.py +++ b/utils/constants.py @@ -17,15 +17,21 @@ """ This module contains all constants used across the project. """ - -# Action inputs environment variables +# Action inputs GITHUB_TOKEN = "GITHUB_TOKEN" -PROJECT_STATE_MINING = "PROJECT_STATE_MINING" -REPOSITORIES = "REPOSITORIES" -OUTPUT_PATH = "OUTPUT_PATH" -DEFAULT_OUTPUT_PATH = "./output" -STRUCTURED_OUTPUT = "STRUCTURED_OUTPUT" -GROUP_OUTPUT_BY_TOPICS = "GROUP_OUTPUT_BY_TOPICS" +PROJECT_STATE_MINING = "LIV_DOC_PROJECT_STATE_MINING" +REPOSITORIES = "LIV_DOC_REPOSITORIES" +STRUCTURED_OUTPUT = "LIV_DOC_STRUCTURED_OUTPUT" +GROUP_OUTPUT_BY_TOPICS = "LIV_DOC_GROUP_OUTPUT_BY_TOPICS" + +# Mining regimes +LIV_DOC_REGIME = "LIV_DOC_REGIME" +# CI_REGIME = "CI_REGIME" + +# Regime output paths +OUTPUT_PATH = "./output" +LIV_DOC_OUTPUT_PATH = "./output/liv-doc-regime" +# CI_OUTPUT_PATH = "./output/ci-regime" # GitHub API constants ISSUES_PER_PAGE_LIMIT = 100 @@ -110,7 +116,6 @@ |-------------------|-----------------|------------------------|-------------|-----------| """ - # Symbol, when no project is attached to an issue NO_PROJECT_DATA = "---" diff --git a/living_documentation_generator/utils/decorators.py b/utils/decorators.py similarity index 96% rename from living_documentation_generator/utils/decorators.py rename to utils/decorators.py index 08fe737..fc6063a 100644 --- a/living_documentation_generator/utils/decorators.py +++ b/utils/decorators.py @@ -26,7 +26,7 @@ from github import GithubException from requests.exceptions import Timeout, RequestException -from living_documentation_generator.utils.github_rate_limiter import GithubRateLimiter +from utils.github_rate_limiter import GithubRateLimiter logger = logging.getLogger(__name__) diff --git a/living_documentation_generator/utils/github_project_queries.py b/utils/github_project_queries.py similarity index 93% rename from living_documentation_generator/utils/github_project_queries.py rename to utils/github_project_queries.py index 69dc1e8..d9ca792 100644 --- a/living_documentation_generator/utils/github_project_queries.py +++ b/utils/github_project_queries.py @@ -18,8 +18,8 @@ This module contains methods for formating the GitHub GraphQL queries. """ -from living_documentation_generator.utils.utils import validate_query_format -from living_documentation_generator.utils.constants import ( +from utils.utils import validate_query_format +from utils.constants import ( PROJECTS_FROM_REPO_QUERY, ISSUES_FROM_PROJECT_QUERY, PROJECT_FIELD_OPTIONS_QUERY, diff --git a/living_documentation_generator/utils/github_rate_limiter.py b/utils/github_rate_limiter.py similarity index 100% rename from living_documentation_generator/utils/github_rate_limiter.py rename to utils/github_rate_limiter.py diff --git a/living_documentation_generator/utils/logging_config.py b/utils/logging_config.py similarity index 100% rename from living_documentation_generator/utils/logging_config.py rename to utils/logging_config.py diff --git a/living_documentation_generator/utils/utils.py b/utils/utils.py similarity index 93% rename from living_documentation_generator/utils/utils.py rename to utils/utils.py index b7551e6..54a2288 100644 --- a/living_documentation_generator/utils/utils.py +++ b/utils/utils.py @@ -91,16 +91,6 @@ def validate_query_format(query_string, expected_placeholders) -> None: sys.exit(1) -def get_all_project_directories(path: str = ".") -> list[str]: - """ - Get all directories in the project starting from the specified path. - - @param path: The path to start searching for directories. - @return: A list of all directories in the project. - """ - return [os.path.join(path, d) for d in os.listdir(path) if os.path.isdir(os.path.join(path, d))] - - def generate_root_level_index_page(index_root_level_page: str, output_path: str) -> None: """ Generate the root level index page for the output living documentation.