mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 12:40:35 +00:00
[Feature] - OpenBB Platform CLI Unit tests (#6397)
* Unit test batch 1 * CLI controller * Test batch 3 * Test batch 4 * Test batch 5 * clean some workflows and setup actions * test * rename wfs * rename * update action * Skip * fix cli tests --------- Co-authored-by: Henrique Joaquim <henriquecjoaquim@gmail.com> Co-authored-by: Diogo Sousa <montezdesousa@gmail.com> Co-authored-by: montezdesousa <79287829+montezdesousa@users.noreply.github.com>
This commit is contained in:
@@ -9,10 +9,12 @@ PLATFORM_DIR = ROOT_DIR / "openbb_platform"
|
||||
PLATFORM_TESTS = [
|
||||
str(PLATFORM_DIR / p) for p in ["tests", "core", "providers", "extensions"]
|
||||
]
|
||||
CLI_DIR = ROOT_DIR / "cli"
|
||||
CLI_TESTS = CLI_DIR / "tests"
|
||||
|
||||
|
||||
@nox.session(python=["3.9", "3.10", "3.11"])
|
||||
def tests(session):
|
||||
def unit_test_platform(session):
|
||||
"""Run the test suite."""
|
||||
session.install("poetry", "toml")
|
||||
session.run(
|
||||
@@ -27,3 +29,19 @@ def tests(session):
|
||||
session.run(
|
||||
"pytest", *PLATFORM_TESTS, f"--cov={PLATFORM_DIR}", "-m", "not integration"
|
||||
)
|
||||
|
||||
|
||||
@nox.session(python=["3.9", "3.10", "3.11"])
|
||||
def unit_test_cli(session):
|
||||
"""Run the test suite."""
|
||||
session.install("poetry", "toml")
|
||||
session.run(
|
||||
"python",
|
||||
str(PLATFORM_DIR / "dev_install.py"),
|
||||
"-e",
|
||||
"all",
|
||||
external=True,
|
||||
)
|
||||
session.install("pytest")
|
||||
session.install("pytest-cov")
|
||||
session.run("pytest", CLI_TESTS, f"--cov={CLI_DIR}")
|
||||
|
||||
224
.github/workflows/README.md
vendored
224
.github/workflows/README.md
vendored
@@ -1,27 +1,9 @@
|
||||
# OpenBB Workflows
|
||||
|
||||
This directory contains the workflows for the OpenBB 🦋 Project. The workflows are:
|
||||
|
||||
| Workflows | Summary | Branches
|
||||
| :-------------------- |:------------------ | :------------------
|
||||
| branch-name-check.yml | Checks if the branch name is valid and follows the naming convention. | all branches
|
||||
| build-release.yml | Builds the project and runs the tests. | main, release/*
|
||||
| docker-build.yml | Builds the docker image and pushes it to the docker hub. | all branches (only pushes to docker hub on main)
|
||||
| draft-release.yml | Creates a draft release when a new tag is pushed. | -
|
||||
| gh-pages.yml | Builds the documentation and deploy to github pages. | main and release/*
|
||||
| integration-test.yml | Runs the integration tests. | all branches
|
||||
| labels-issue.yml | Creates an issue when a new bug is reported. | -
|
||||
| labels-PR.yml | Adds labels to the issues and pull requests. | -
|
||||
| linting.yml | Runs the linters. | all branches
|
||||
| macos-build.yml | Builds the project on M1 Macs. | develop, main, release/*
|
||||
| macos-ml.yml | Builds the project on Mac OS X Full Clean Build with ML. | main
|
||||
| nightly-build.yml | Builds the project and runs the integration tests every night on the `develop` branch. | develop
|
||||
| pypi.yml | Publishes the package to PyPI. | all branches (only pushes to PyPI on main)
|
||||
| pypi-nightly.yml | Publishes the package to PyPI every night on the `develop` branch. | develop
|
||||
| unit-test.yml | Runs the unit tests. | all branches
|
||||
| windows_ml.yml | Builds the project on Windows 10 Full Clean Build with ML. | main
|
||||
| windows10_build.yml | Builds the project on Windows 10. | all branches
|
||||
## Branch Name Check
|
||||
|
||||
## Branch Name Check Workflow
|
||||
Objective: To check if pull request branch names follow the GitFlow naming convention before merging.
|
||||
|
||||
Triggered by: A pull request event where the target branch is either develop or main.
|
||||
@@ -30,137 +12,22 @@ Branches checked: The source branch of a pull request and the target branch of a
|
||||
|
||||
Steps:
|
||||
|
||||
1. Extract branch names: Using the jq tool, the source and target branch names are extracted from the pull request event. The branch names are then stored in environment variables and printed as output.
|
||||
1. Extract branch names: Using the jq tool, the source and target branch names are extracted from the pull request event. The branch names are then stored in environment variables and printed as output.
|
||||
|
||||
2. Show Output result for source-branch and target-branch: The source and target branch names are printed to the console.
|
||||
2. Show Output result for source-branch and target-branch: The source and target branch names are printed to the console.
|
||||
|
||||
3. Check branch name for develop PRs: If the target branch is develop, then the source branch is checked against a regular expression to ensure that it follows the GitFlow naming convention. If the branch name is invalid, a message is printed to the console and the workflow exits with a status code of 1.
|
||||
3. Check branch name for develop PRs: If the target branch is develop, then the source branch is checked against a regular expression to ensure that it follows the GitFlow naming convention. If the branch name is invalid, a message is printed to the console and the workflow exits with a status code of 1.
|
||||
|
||||
4. Check branch name for main PRs: If the target branch is main, then the source branch is checked against a regular expression to ensure that it is either a hotfix or a release branch. If the branch name is invalid, a message is printed to the console and the workflow exits with a status code of 1.
|
||||
4. Check branch name for main PRs: If the target branch is main, then the source branch is checked against a regular expression to ensure that it is either a hotfix or a release branch. If the branch name is invalid, a message is printed to the console and the workflow exits with a status code of 1.
|
||||
|
||||
Note: The GitFlow naming convention for branches is as follows:
|
||||
|
||||
- Feature branches: feature/<feature-name>
|
||||
- Hotfix branches: hotfix/<hotfix-name>
|
||||
- Release branches: release/<major.minor.patch>(rc<number>)
|
||||
- Feature branches: feature/<feature-name>
|
||||
- Hotfix branches: hotfix/<hotfix-name>
|
||||
- Release branches: release/<major.minor.patch>(rc<number>)
|
||||
|
||||
## Build Release Workflow
|
||||
This GitHub Actions workflow is responsible for building and releasing software for multiple platforms (Windows, M1 MacOS, Intel MacOS, and Docker).
|
||||
The workflow has four jobs:
|
||||
## Deploy to PyPI - Nightly
|
||||
|
||||
- `trigger-windows-build`
|
||||
- `trigger-macos-build`
|
||||
- `trigger-intel-build`
|
||||
- `trigger-docker-build`
|
||||
|
||||
Each job uses the `aurelien-baudet/workflow-dispatch` action to trigger another workflow, respectively `windows10_build.yml`, `m1_macos_build.yml`, `intel_macos_build.yml`, and `docker.yml`. The `GITHUB_TOKEN` is passed as a secret so that the triggered workflows have access to the necessary permissions. The `wait-for-completion-timeout` is set to 2 hours, which is the maximum amount of time the job will wait for the triggered workflow to complete.
|
||||
|
||||
## Docker Workflow
|
||||
This GitHub Actions workflow is responsible for building and pushing the docker image to the itHub Container Registry. This workflow is triggered when a new change is pushed to the main branch of the repository, and the Docker image is published to the GitHub Container Registry.
|
||||
|
||||
Steps
|
||||
-----
|
||||
|
||||
1. Checkout Code: This step checks out the code from the GitHub repository.
|
||||
|
||||
2. Login to GitHub Container Registry: This step logs into the GitHub Container Registry using the GitHub Actions token.
|
||||
|
||||
3. Setup Commit Hash: This step sets the commit hash of the code that is being built.
|
||||
|
||||
4. Build Env File: This step builds the environment file for the Docker image.
|
||||
|
||||
5. Building the Docker Image: This step builds the Docker image using the scripts in the `build/docker` directory.
|
||||
|
||||
6. Publishing the Docker Image: This step publishes the Docker image to the GitHub Container Registry. The Docker image is only pushed to the registry if the branch being built is `main`.
|
||||
|
||||
## Release Drafter Workflow
|
||||
This GitHub Actions workflow is designed to automatically generate and update draft releases in a GitHub repository. The workflow is triggered when it is manually dispatched, allowing you to control when the draft releases are updated.
|
||||
|
||||
## GH Pages Workflow
|
||||
This GitHub Actions workflow is responsible for building the documentation and deploying it to GitHub Pages. This workflow is triggered when a new change is pushed to the `main` or `release` branch of the repository, and the documentation is published to GitHub Pages.
|
||||
|
||||
## Integration Test Workflow
|
||||
This GitHub Action is used to run integration tests on your code repository. It is triggered on pushes to the `release/*` or `main` branches, and it runs on the latest version of Ubuntu.
|
||||
|
||||
The workflow consists of the following steps:
|
||||
|
||||
1. Check out the code from the repository
|
||||
2. Set up Python 3.9
|
||||
3. Install Poetry, a package and dependency manager for Python
|
||||
4. Load a cached virtual environment created by Poetry, to speed up the process if possible
|
||||
5. Install dependencies specified in the `poetry.lock` file
|
||||
6. Run the integration tests using the `terminal.py` script
|
||||
7. Upload a summary of the test results to Slack
|
||||
|
||||
The results of the tests are captured in a file called `result.txt`. The summary of the tests, including information about failed tests, is then uploaded to Slack using the `adrey/slack-file-upload-action` GitHub Action.
|
||||
|
||||
## Linting Workflow
|
||||
This GitHub Actions workflow is responsible for running linting checks on the codebase. This workflow is triggered on pull request events such as `opened`, `synchronize`, and `edited`, and push events on branches with names that start with `feature/`, `hotfix/`, or `release/`. The workflow also sets a number of environment variables and uses Github Actions caching to improve performance.
|
||||
|
||||
It consists of two jobs: `code-linting` and `markdown-link-check`.
|
||||
|
||||
The first job, `code-linting`, runs on an Ubuntu machine and performs several linting tasks on the code in the repository, including:
|
||||
|
||||
- Checking out the code from the repository
|
||||
- Setting up Python 3.9
|
||||
- Installing a number of Python packages necessary for the linting tasks
|
||||
- Running `bandit` to check for security vulnerabilities
|
||||
- Running `black` to check the code formatting
|
||||
- Running `codespell` to check the spelling of comments, strings, and variable names
|
||||
- Running `ruff` to check the use of Python
|
||||
- Running `mypy` to check the type annotations
|
||||
- Running `pyupgrade` to upgrade Python 2 code to Python 3
|
||||
- Running `pylint` to perform static analysis of the code
|
||||
|
||||
The second job, `markdown-link-check`, runs on an Ubuntu machine and performs linting of the markdown files in the repository. It uses a Docker container `avtodev/markdown-lint` to perform the linting.
|
||||
|
||||
## MacOS Build Workflow
|
||||
This GitHub Actions workflow is used to build a version of the OpenBB Platform CLI for M1 MacOS. The build process includes installing necessary dependencies, building the terminal application using PyInstaller, creating a DMG file for distribution, and running integration tests on the built application.
|
||||
|
||||
Jobs
|
||||
----
|
||||
|
||||
The workflow consists of a single job named `Build` which runs on self-hosted MacOS systems with ARM64 architecture. The job performs the following steps:
|
||||
|
||||
1. Checkout: The main branch of the repository is checked out, allowing for the commit hashes to line up.
|
||||
2. Git Log: The log of the Git repository is displayed.
|
||||
3. Install create-dmg: The `create-dmg` tool is installed using Homebrew.
|
||||
4. Clean Previous Path: The previous PATH environment variable is cleared and restored to its default values.
|
||||
5. Setup Conda Caching: The miniconda environment is set up using a caching mechanism for faster workflow execution after the first run.
|
||||
6. Setup Miniconda: Miniconda is set up using the `conda-3-9-env-full.yaml` environment file, with channels `conda-forge` and `defaults`, and with the `build_env` environment activated.
|
||||
7. Run Poetry: Poetry is used to install the dependencies for the project.
|
||||
8. Install PyInstaller: PyInstaller is installed using Poetry.
|
||||
9. Poetry Install Portfolio Optimization and Forecasting Toolkits: The portfolio optimization and forecasting toolkits are installed using Poetry.
|
||||
10. Install Specific Papermill: A specific version of Papermill is installed using pip.
|
||||
11. Build Bundle: The terminal application is built using PyInstaller, with icons and assets copied to the DMG directory.
|
||||
12. Create DMG: The DMG file is created using the `create-dmg` tool.
|
||||
13. Clean up Build Artifacts: The build artifacts such as the terminal directory and DMG directory are removed.
|
||||
14. Save Build Artifact DMG: The DMG file is saved as a build artifact.
|
||||
15. Convert & Mount DMG: The DMG file is converted and mounted.
|
||||
16. Directory Change: The current directory is changed to the mounted DMG file.
|
||||
17. Unmount DMG: The mounted DMG file is unmounted.
|
||||
18. Run Integration Tests: The built terminal application is run with integration tests, and the results are displayed.
|
||||
|
||||
Finally, the integration tests are run and the results are logged. The workflow is configured to run only when triggered by a workflow dispatch event and runs in a concurrent group, with the ability to cancel in-progress jobs.
|
||||
|
||||
## Nightly Build Workflow
|
||||
This code is a GitHub Actions workflow configuration file that is used to trigger other workflows when certain events occur. The main purpose of this workflow is to trigger builds on different platforms when a release is made or a pull request is made to the main branch.
|
||||
|
||||
This workflow is triggered at UTC+0 daily by the GitHub Action schedule event.
|
||||
|
||||
The job includes the following steps:
|
||||
|
||||
1. Trigger Windows Build: This step uses the `aurelien-baudet/workflow-dispatch` action to trigger the windows10_build.yml workflow.
|
||||
|
||||
2. Trigger macOS Build: This step uses the `aurelien-baudet/workflow-dispatch` action to trigger the m1_macos_build.yml workflow
|
||||
|
||||
3. Trigger Intel Build: This step uses the `aurelien-baudet/workflow-dispatch` action to trigger the intel_macos_build.yml workflow
|
||||
|
||||
4. Trigger Docker Build: This step uses the `aurelien-baudet/workflow-dispatch` action to trigger the docker.yml workflow
|
||||
|
||||
This workflow also uses a concurrency setting that groups the jobs by the workflow and ref, and cancels any in-progress jobs.
|
||||
|
||||
## Nightly PyPI Publish Workflow
|
||||
This workflow is used to publish the latest version of the OpenBB Platform CLI to PyPI. The workflow is triggered at UTC+0 daily by the GitHub Action schedule event.
|
||||
|
||||
It does this by first updating the `pyproject.toml` file with a pre-determined version string of the form `<currentVersion>.dev<date>`, where `<date>` represents the current day's date as a 8 digit number.
|
||||
@@ -169,12 +36,13 @@ Then, the code installs `pypa/build` and uses `python -m build` to create a bina
|
||||
|
||||
Finally, it uses the PyPA specific action `gh-action-pypi-publish` to publish the created files to PyPI.
|
||||
|
||||
## PYPI publish Workflow
|
||||
## Deploy the OpenBB Platform to Test PyPI
|
||||
|
||||
The Github Action code `Deploy to PyPI` is used to deploy a Python project to PyPI (Python Package Index) and TestPyPI, which is a separate package index for testing purposes. The code is triggered on two events:
|
||||
|
||||
1. Push event: The code is triggered whenever there is a push to the `release/*` and `main` branches.
|
||||
1. Push event: The code is triggered whenever there is a push to the `release/*` and `main` branches.
|
||||
|
||||
2. Workflow dispatch event: The code can be manually triggered by the workflow dispatch event.
|
||||
2. Workflow dispatch event: The code can be manually triggered by the workflow dispatch event.
|
||||
|
||||
The code sets the concurrency to the `group` and the option `cancel-in-progress` is set to `true` to ensure that the running jobs in the same `group` are cancelled in case another job is triggered.
|
||||
|
||||
@@ -186,47 +54,47 @@ Similarly, the `deploy-pypi` job is triggered only if the pushed branch starts w
|
||||
|
||||
Note: The code uses the `pypa/build` package for building the binary wheel and source tarball, and the `pypa/gh-action-pypi-publish@release/v1` Github Action for publishing the distributions to PyPI and TestPyPI.
|
||||
|
||||
## Unit Tests Workflow
|
||||
This workflow is used to run unit tests on the OpenBB Platform CLI. The workflow is triggered on the following events:
|
||||
The events this workflow will respond to are:
|
||||
## Draft release
|
||||
|
||||
1. Pull requests that are opened, synchronized, edited, or closed. The pull request must be made to the `develop` or `main` branches.
|
||||
This GitHub Actions workflow is designed to automatically generate and update draft releases in a GitHub repository. The workflow is triggered when it is manually dispatched, allowing you to control when the draft releases are updated.
|
||||
|
||||
2. Pushes to the `release/*` branches.
|
||||
## General Linting
|
||||
|
||||
Each job in the workflow specifies a set of steps that are executed in order.
|
||||
This GitHub Actions workflow is responsible for running linting checks on the codebase. This workflow is triggered on pull request events such as `opened`, `synchronize`, and `edited`, and push events on branches with names that start with `feature/`, `hotfix/`, or `release/`. The workflow also sets a number of environment variables and uses Github Actions caching to improve performance.
|
||||
|
||||
The first job, `check-files-changed`, checks whether there are any changes to certain file types in the repository, such as Python files and lockfiles. If there are changes, then the `check-changes` output variable is set to `true`.
|
||||
It consists of two jobs: `code-linting` and `markdown-link-check`.
|
||||
|
||||
The next job, `base-test`, runs a series of tests if `check-changes` is `true` and the base branch of the pull request is `develop`. This job sets up a Python 3.9 environment, installs Poetry, and then runs tests using `pytest`. Finally, it starts the terminal and exits.
|
||||
The first job, `code-linting`, runs on an Ubuntu machine and performs several linting tasks on the code in the repository, including:
|
||||
|
||||
The next job, `tests-python`, runs tests for different versions of Python (3.8, 3.9, and 3.10) on the `ubuntu-latest` operating system. It sets up the specified Python version, installs Poetry and dependencies, and then runs tests using `pytest`.
|
||||
- Checking out the code from the repository
|
||||
- Setting up Python 3.9
|
||||
- Installing a number of Python packages necessary for the linting tasks
|
||||
- Running `bandit` to check for security vulnerabilities
|
||||
- Running `black` to check the code formatting
|
||||
- Running `codespell` to check the spelling of comments, strings, and variable names
|
||||
- Running `ruff` to check the use of Python
|
||||
- Running `pylint` to perform static analysis of the code
|
||||
- Running `mypy` to check the type annotations
|
||||
- Running `pydocstyle` to check the docstrings
|
||||
|
||||
The next job, `full-test`, uses the GitHub Actions `checkout` action to checkout the code, followed by the `setup-python` action to set up the specified version of Python. Then, the `install-poetry` action is used to install the package manager Poetry, and a cache is set up using the `actions/cache` action to avoid re-installing dependencies. After that, the dependencies are installed using Poetry, and a list of installed packages is displayed. Then, the tests are run using `pytest`, and finally, the `terminal.py` script is started and exited.
|
||||
The second job, `markdown-link-check`, runs on an Ubuntu machine and performs linting of the markdown files in the repository. It uses a Docker container `avtodev/markdown-lint` to perform the linting.
|
||||
|
||||
The last job, `tests-conda`, sets up a Miniconda environment using the `setup-miniconda` action. The environment is specified using a YAML file and is activated. Then, the tests are run.
|
||||
## Deploy to GitHub Pages
|
||||
|
||||
## Windows 10 Build Workflow
|
||||
This is a GitHub Actions workflow file that automates the build and testing process for the OpenBB Platform CLI on Windows 10. The workflow consists of two jobs:
|
||||
This GitHub Actions workflow is responsible for building the documentation and deploying it to GitHub Pages. This workflow is triggered when a new change is pushed to the `main` or `release` branch of the repository, and the documentation is published to GitHub Pages.
|
||||
|
||||
1. Windows-Build
|
||||
2. Build-Exe
|
||||
## Pull Request Labels
|
||||
|
||||
- The Windows-Build job does the following:
|
||||
- Sets up the Windows Git configuration for long file paths.
|
||||
- Checks out the repository code.
|
||||
- Sets up Python 3.9 and creates an OpenBB environment using poetry.
|
||||
- Installs necessary packages and builds the terminal using PyInstaller.
|
||||
- Uploads the built artifact to GitHub as an artifact.
|
||||
- The Build-Exe job does the following:
|
||||
- Sets up the Windows Git configuration for long file paths.
|
||||
- Checks out the repository code.
|
||||
- Downloads the built artifact from the previous Windows-Build job.
|
||||
- Copies the files into an app folder for building the EXE file.
|
||||
- Builds the EXE file using NSIS.
|
||||
- Uploads the built EXE as an artifact to GitHub.
|
||||
- Runs integration tests on the terminal and saves the results to a text file.
|
||||
- Uploads the test results summary to Slack.
|
||||
- Cleans up previous build files and artifacts.
|
||||
Automatic labelling of pull requests.
|
||||
|
||||
This workflow is triggered by the `workflow_dispatch` event and runs in concurrency with other workflows in the same group, with the ability to cancel in-progress builds. The concurrency group is defined as `${{ github.workflow }}-${{ github.ref }}`.
|
||||
## 🚉 Integration test Platform (API)
|
||||
|
||||
Run `openbb_platform` API integration tests,
|
||||
|
||||
## 🖥️ Unit test CLI
|
||||
|
||||
Run `cli` directory unit tests.
|
||||
|
||||
## 🚉 Unit test Platform
|
||||
|
||||
Run `openbb_platform` directory unit tests - providers, extensions, etc.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Deploy the OpenBB Platform and the OpenBBTerminal to Test PyPI
|
||||
name: Deploy the OpenBB Platform to Test PyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
2
.github/workflows/draft-release.yml
vendored
2
.github/workflows/draft-release.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Release Drafter
|
||||
name: Draft release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
name: General Linting
|
||||
|
||||
env:
|
||||
OPENBB_ENABLE_QUICK_EXIT: true
|
||||
OPENBB_LOG_COLLECT: false
|
||||
OPENBB_USE_PROMPT_TOOLKIT: false
|
||||
OPENBB_FILE_OVERWRITE: true
|
||||
PIP_DEFAULT_TIMEOUT: 100
|
||||
|
||||
on:
|
||||
27
.github/workflows/labels-issue.yml
vendored
27
.github/workflows/labels-issue.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: "Set Issue Label and Assignee"
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Naturalclar/issue-action@v2.0.2
|
||||
with:
|
||||
title-or-body: "both"
|
||||
parameters:
|
||||
'[ {"keywords": ["bug", "error", "issue"], "labels": ["bug"]},
|
||||
{"keywords": ["help", "guidance"], "labels": ["help-wanted"], "assignees": ["colin99d"]},
|
||||
{"keywords": ["portfolio"], "labels": ["portfolio"], "assignees": ["JerBouma", "montezdesousa"]},
|
||||
{"keywords": ["dashboards"], "labels": ["dashboards"], "assignees": ["colin99d"]},
|
||||
{"keywords": ["dependencies"], "labels": ["dependencies"], "assignees": ["piiq"]},
|
||||
{"keywords": ["build"], "labels": ["build"], "assignees": ["piiq"]},
|
||||
{"keywords": ["jupyter"], "labels": ["jupyterlab"], "assignees": ["piiq"]},
|
||||
{"keywords": ["reports"], "labels": ["notebookreports"], "assignees": ["piiq"]},
|
||||
{"keywords": ["installer"], "labels": ["installer"], "assignees": ["piiq", "andrewkenreich"]},
|
||||
{"keywords": ["pytest", "tests"], "labels": ["tests"], "assignees": ["Chavithra"]},
|
||||
{"keywords": ["guides"], "labels": ["guides"], "assignees": ["JerBouma"]},
|
||||
{"keywords": ["crypto"], "labels": ["crypto"], "assignees": ["minhhoang1023"]}
|
||||
]'
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
35
.github/workflows/nightly-build.yml
vendored
35
.github/workflows/nightly-build.yml
vendored
@@ -1,35 +0,0 @@
|
||||
name: Nightly Build
|
||||
|
||||
env:
|
||||
OPENBB_ENABLE_QUICK_EXIT: true
|
||||
OPENBB_LOG_COLLECT: false
|
||||
OPENBB_USE_PROMPT_TOOLKIT: false
|
||||
PIP_DEFAULT_TIMEOUT: 100
|
||||
OPENBB_FILE_OVERWRITE: true
|
||||
PYTHONNOUSERSITE: 1
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
trigger-pypi-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger PyPI Build
|
||||
uses: aurelien-baudet/workflow-dispatch@v2
|
||||
with:
|
||||
workflow: pypi-nightly.yml
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
trigger-api-integration-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger Platform API Integration Test
|
||||
uses: aurelien-baudet/workflow-dispatch@v2
|
||||
with:
|
||||
workflow: platform-api-integration-test.yml
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
78
.github/workflows/pypi.yml
vendored
78
.github/workflows/pypi.yml
vendored
@@ -1,78 +0,0 @@
|
||||
name: Deploy to PyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release/v3/*
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy-test-pypi:
|
||||
name: Build and publish 📦 to TestPyPI
|
||||
if: startsWith(github.ref, 'refs/heads/release/')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Python 3.9
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.9"
|
||||
|
||||
- name: Install pypa/build
|
||||
run: >-
|
||||
python -m
|
||||
pip install
|
||||
build
|
||||
--user
|
||||
- name: Build a binary wheel and a source tarball
|
||||
run: >-
|
||||
python -m
|
||||
build
|
||||
--sdist
|
||||
--wheel
|
||||
--outdir dist/
|
||||
.
|
||||
|
||||
- name: Publish distribution 📦 to Test PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
||||
repository_url: https://test.pypi.org/legacy/
|
||||
|
||||
deploy-pypi:
|
||||
name: Build and publish 📦 to PyPI
|
||||
if: startsWith(github.ref, 'refs/heads/main')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Python 3.9
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.9"
|
||||
|
||||
- name: Install pypa/build
|
||||
run: >-
|
||||
python -m
|
||||
pip install
|
||||
build
|
||||
--user
|
||||
- name: Build a binary wheel and a source tarball
|
||||
run: >-
|
||||
python -m
|
||||
build
|
||||
--sdist
|
||||
--wheel
|
||||
--outdir dist/
|
||||
.
|
||||
|
||||
- name: Publish distribution 📦 to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
@@ -1,4 +1,4 @@
|
||||
name: API Integration Tests
|
||||
name: 🚉 Integration test Platform (API)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
46
.github/workflows/test-unit-cli.yml
vendored
Normal file
46
.github/workflows/test-unit-cli.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: 🖥️ Unit test CLI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- 'cli/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
unit_tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
matrix:
|
||||
python_version:
|
||||
["3.9", "3.10", "3.11"]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Install Python ${{ matrix.python_version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python_version }}
|
||||
allow-prereleases: true
|
||||
cache: "pip"
|
||||
|
||||
- name: Cache pip packages
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('cli/poetry.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
pip install nox
|
||||
nox -f .github/scripts/noxfile.py -s unit_test_cli --python ${{ matrix.python_version }}
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Test Platform V4
|
||||
name: 🚉 Unit test Platform
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -43,4 +43,4 @@ jobs:
|
||||
- name: Run tests
|
||||
run: |
|
||||
pip install nox
|
||||
nox -f .github/scripts/noxfile.py -s tests --python ${{ matrix.python_version }}
|
||||
nox -f .github/scripts/noxfile.py -s unit_test_platform --python ${{ matrix.python_version }}
|
||||
72
.github/workflows/unit-test.yml
vendored
72
.github/workflows/unit-test.yml
vendored
@@ -1,72 +0,0 @@
|
||||
name: Unit Test
|
||||
|
||||
env:
|
||||
OPENBB_ENABLE_QUICK_EXIT: true
|
||||
OPENBB_LOG_COLLECT: false
|
||||
OPENBB_USE_PROMPT_TOOLKIT: false
|
||||
OPENBB_FILE_OVERWRITE: true
|
||||
OPENBB_ENABLE_CHECK_API: false
|
||||
OPENBB_PREVIOUS_USE: true
|
||||
OPENBB_USE_INTERACTIVE_DF: false
|
||||
PIP_DEFAULT_TIMEOUT: 100
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
types: [opened, synchronize, edited, closed, labeled]
|
||||
push:
|
||||
branches:
|
||||
- release/*
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-files-changed:
|
||||
name: Check for changes
|
||||
runs-on: ubuntu-latest
|
||||
# Run this job only if the PR is not merged, PR != draft and the event is not a push
|
||||
if: github.event.pull_request.merged == false && github.event_name != 'push' && github.event.pull_request.draft == false
|
||||
outputs:
|
||||
check-changes: ${{ steps.check-changes.outputs.check-changes }}
|
||||
check-platform-changes: ${{ steps.check-platform-changes.outputs.check-platform-changes }} # New output for openbb_platform changes
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1 # v1 is used to preserve the git history and works with the git diff command
|
||||
with:
|
||||
fetch-depth: 100
|
||||
# The GitHub token is preserved by default but this job doesn't need
|
||||
# to be able to push to GitHub.
|
||||
|
||||
# Check for changes to python files, lockfiles and the openbb_terminal folder
|
||||
- name: Check for changes to files to trigger unit test
|
||||
id: check-changes
|
||||
run: |
|
||||
current_branch=$(jq -r .pull_request.base.ref "$GITHUB_EVENT_PATH")
|
||||
|
||||
if git diff --name-only origin/$current_branch HEAD | grep -E ".py$|openbb_terminal\/.*|pyproject.toml|poetry.lock|requirements.txt|requirements-full.txt"; then
|
||||
echo "check-changes=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "check-changes=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Check for changes to openbb_platform
|
||||
- name: Check for changes to openbb_platform
|
||||
id: check-platform-changes
|
||||
run: |
|
||||
current_branch=$(jq -r .pull_request.base.ref "$GITHUB_EVENT_PATH")
|
||||
|
||||
if git diff --name-only origin/$current_branch HEAD | grep -E "openbb_platform\/.*"; then
|
||||
echo "check-platform-changes=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "check-platform-changes=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Show output of previous step
|
||||
run: |
|
||||
echo "check-changes=${{ steps.check-changes.outputs.check-changes }}"
|
||||
echo "check-platform-changes=${{ steps.check-platform-changes.outputs.check-platform-changes }}"
|
||||
2
.pre-commit-config.yaml
vendored
2
.pre-commit-config.yaml
vendored
@@ -39,7 +39,7 @@ repos:
|
||||
"--ignore-words-list=VAI,MIS,shs,gard,te,commun,parana,ro,zar,vie,hel,jewl,zlot,ba,buil,coo,ether,hist,hsi,mape,navagation,operatio,pres,ser,yeld,shold,ist,varian,datas,ake,creat,statics,ket,toke,certi,buidl,ot,fo",
|
||||
"--quiet-level=2",
|
||||
"--skip=./**/tests/**,./**/test_*.py,.git,*.css,*.csv,*.html,*.ini,*.ipynb,*.js,*.json,*.lock,*.scss,*.txt,*.yaml,build/pyinstaller/*,./website/config.toml",
|
||||
"-x=.github/workflows/linting.yml"
|
||||
"-x=.github/workflows/general-linting.yml"
|
||||
]
|
||||
- repo: local
|
||||
hooks:
|
||||
|
||||
@@ -106,20 +106,6 @@ class MenuText:
|
||||
else:
|
||||
self.menu_text += text
|
||||
|
||||
def add_section(
|
||||
self, text: str, description: str = "", leading_new_line: bool = False
|
||||
):
|
||||
"""Append raw text (without translation)."""
|
||||
spacing = (self.CMD_NAME_LENGTH - len(text) + self.SECTION_SPACING) * " "
|
||||
if description:
|
||||
text = f"{text}{spacing}{description}\n"
|
||||
|
||||
if leading_new_line:
|
||||
self.menu_text += "\n" + text
|
||||
|
||||
else:
|
||||
self.menu_text += text
|
||||
|
||||
def add_info(self, text: str):
|
||||
"""Append information text (after translation)."""
|
||||
self.menu_text += f"[info]{text}:[/info]\n"
|
||||
|
||||
94
cli/tests/test_argparse_translator.py
Normal file
94
cli/tests/test_argparse_translator.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""Test the Argparse Translator."""
|
||||
|
||||
from argparse import ArgumentParser
|
||||
|
||||
import pytest
|
||||
from openbb_cli.argparse_translator.argparse_translator import (
|
||||
ArgparseTranslator,
|
||||
CustomArgument,
|
||||
CustomArgumentGroup,
|
||||
)
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
|
||||
def test_custom_argument_action_validation():
|
||||
"""Test that CustomArgument raises an error for invalid actions."""
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
CustomArgument(
|
||||
name="test",
|
||||
type=bool,
|
||||
dest="test",
|
||||
default=False,
|
||||
required=True,
|
||||
action="store",
|
||||
help="Test argument",
|
||||
nargs=None,
|
||||
choices=None,
|
||||
)
|
||||
assert 'action must be "store_true"' in str(excinfo.value)
|
||||
|
||||
|
||||
def test_custom_argument_remove_props_on_store_true():
|
||||
"""Test that CustomArgument removes type, nargs, and choices on store_true."""
|
||||
argument = CustomArgument(
|
||||
name="verbose",
|
||||
type=None,
|
||||
dest="verbose",
|
||||
default=None,
|
||||
required=False,
|
||||
action="store_true",
|
||||
help="Verbose output",
|
||||
nargs=None,
|
||||
choices=None,
|
||||
)
|
||||
assert argument.type is None
|
||||
assert argument.nargs is None
|
||||
assert argument.choices is None
|
||||
|
||||
|
||||
def test_custom_argument_group():
|
||||
"""Test the CustomArgumentGroup class."""
|
||||
args = [
|
||||
CustomArgument(
|
||||
name="test",
|
||||
type=int,
|
||||
dest="test",
|
||||
default=1,
|
||||
required=True,
|
||||
action="store",
|
||||
help="Test argument",
|
||||
nargs=None,
|
||||
choices=None,
|
||||
)
|
||||
]
|
||||
group = CustomArgumentGroup(name="Test Group", arguments=args)
|
||||
assert group.name == "Test Group"
|
||||
assert len(group.arguments) == 1
|
||||
assert group.arguments[0].name == "test"
|
||||
|
||||
|
||||
def test_argparse_translator_setup():
|
||||
"""Test the ArgparseTranslator setup."""
|
||||
|
||||
def test_function(test_arg: int):
|
||||
"""A test function."""
|
||||
return test_arg * 2
|
||||
|
||||
translator = ArgparseTranslator(func=test_function)
|
||||
parser = translator.parser
|
||||
assert isinstance(parser, ArgumentParser)
|
||||
assert "--test_arg" in parser._option_string_actions
|
||||
|
||||
|
||||
def test_argparse_translator_execution():
|
||||
"""Test the ArgparseTranslator execution."""
|
||||
|
||||
def test_function(test_arg: int) -> int:
|
||||
"""A test function."""
|
||||
return test_arg * 2
|
||||
|
||||
translator = ArgparseTranslator(func=test_function)
|
||||
parsed_args = translator.parser.parse_args(["--test_arg", "3"])
|
||||
result = translator.execute_func(parsed_args)
|
||||
assert result == 6
|
||||
89
cli/tests/test_argparse_translator_obbject_registry.py
Normal file
89
cli/tests/test_argparse_translator_obbject_registry.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Test OBBject Registry."""
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from openbb_cli.argparse_translator.obbject_registry import Registry
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
|
||||
# pylint: disable=redefined-outer-name, protected-access
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def registry():
|
||||
"""Fixture to create a Registry instance for testing."""
|
||||
return Registry()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_obbject():
|
||||
"""Fixture to create a mock OBBject for testing."""
|
||||
|
||||
class MockModel:
|
||||
"""Mock model for testing."""
|
||||
|
||||
def __init__(self, value):
|
||||
self.mock_value = value
|
||||
self._model_json_schema = "mock_json_schema"
|
||||
|
||||
def model_json_schema(self):
|
||||
return self._model_json_schema
|
||||
|
||||
obb = Mock(spec=OBBject)
|
||||
obb.id = "123"
|
||||
obb.provider = "test_provider"
|
||||
obb.extra = {"command": "test_command"}
|
||||
obb._route = "/test/route"
|
||||
obb._standard_params = Mock()
|
||||
obb._standard_params.__dict__ = {}
|
||||
obb.results = [MockModel(1), MockModel(2)]
|
||||
return obb
|
||||
|
||||
|
||||
def test_listing_all_obbjects(registry, mock_obbject):
|
||||
"""Test listing all obbjects with additional properties."""
|
||||
registry.register(mock_obbject)
|
||||
|
||||
all_obbjects = registry.all
|
||||
assert len(all_obbjects) == 1
|
||||
assert all_obbjects[0]["command"] == "test_command"
|
||||
assert all_obbjects[0]["provider"] == "test_provider"
|
||||
|
||||
|
||||
def test_registry_initialization(registry):
|
||||
"""Test the Registry is initialized correctly."""
|
||||
assert registry.obbjects == []
|
||||
|
||||
|
||||
def test_register_new_obbject(registry, mock_obbject):
|
||||
"""Test registering a new OBBject."""
|
||||
registry.register(mock_obbject)
|
||||
assert mock_obbject in registry.obbjects
|
||||
|
||||
|
||||
def test_register_duplicate_obbject(registry, mock_obbject):
|
||||
"""Test that duplicate OBBjects are not added."""
|
||||
registry.register(mock_obbject)
|
||||
registry.register(mock_obbject)
|
||||
assert len(registry.obbjects) == 1
|
||||
|
||||
|
||||
def test_get_obbject_by_index(registry, mock_obbject):
|
||||
"""Test retrieving an obbject by its index."""
|
||||
registry.register(mock_obbject)
|
||||
retrieved = registry.get(0)
|
||||
assert retrieved == mock_obbject
|
||||
|
||||
|
||||
def test_remove_obbject_by_index(registry, mock_obbject):
|
||||
"""Test removing an obbject by index."""
|
||||
registry.register(mock_obbject)
|
||||
registry.remove(0)
|
||||
assert mock_obbject not in registry.obbjects
|
||||
|
||||
|
||||
def test_remove_last_obbject_by_default(registry, mock_obbject):
|
||||
"""Test removing the last obbject by default."""
|
||||
registry.register(mock_obbject)
|
||||
registry.remove()
|
||||
assert not registry.obbjects
|
||||
45
cli/tests/test_cli.py
Normal file
45
cli/tests/test_cli.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Test the CLI module."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from openbb_cli.cli import main
|
||||
|
||||
|
||||
@patch("openbb_cli.cli.bootstrap")
|
||||
@patch("openbb_cli.cli.launch")
|
||||
@patch("sys.argv", ["openbb", "--dev", "--debug"])
|
||||
def test_main_with_dev_and_debug(mock_launch, mock_bootstrap):
|
||||
"""Test the main function with dev and debug flags."""
|
||||
main()
|
||||
mock_bootstrap.assert_called_once()
|
||||
mock_launch.assert_called_once_with(True, True)
|
||||
|
||||
|
||||
@patch("openbb_cli.cli.bootstrap")
|
||||
@patch("openbb_cli.cli.launch")
|
||||
@patch("sys.argv", ["openbb"])
|
||||
def test_main_without_arguments(mock_launch, mock_bootstrap):
|
||||
"""Test the main function without arguments."""
|
||||
main()
|
||||
mock_bootstrap.assert_called_once()
|
||||
mock_launch.assert_called_once_with(False, False)
|
||||
|
||||
|
||||
@patch("openbb_cli.cli.bootstrap")
|
||||
@patch("openbb_cli.cli.launch")
|
||||
@patch("sys.argv", ["openbb", "--dev"])
|
||||
def test_main_with_dev_only(mock_launch, mock_bootstrap):
|
||||
"""Test the main function with dev flag only."""
|
||||
main()
|
||||
mock_bootstrap.assert_called_once()
|
||||
mock_launch.assert_called_once_with(True, False)
|
||||
|
||||
|
||||
@patch("openbb_cli.cli.bootstrap")
|
||||
@patch("openbb_cli.cli.launch")
|
||||
@patch("sys.argv", ["openbb", "--debug"])
|
||||
def test_main_with_debug_only(mock_launch, mock_bootstrap):
|
||||
"""Test the main function with debug flag only."""
|
||||
main()
|
||||
mock_bootstrap.assert_called_once()
|
||||
mock_launch.assert_called_once_with(False, True)
|
||||
78
cli/tests/test_config_completer.py
Normal file
78
cli/tests/test_config_completer.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Test the Config completer."""
|
||||
|
||||
import pytest
|
||||
from openbb_cli.config.completer import WordCompleter
|
||||
from prompt_toolkit.completion import CompleteEvent
|
||||
from prompt_toolkit.document import Document
|
||||
|
||||
# pylint: disable=redefined-outer-name, import-outside-toplevel
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def word_completer():
|
||||
"""Return a simple word completer."""
|
||||
words = ["test", "example", "demo"]
|
||||
return WordCompleter(words, ignore_case=True)
|
||||
|
||||
|
||||
def test_word_completer_simple(word_completer):
|
||||
"""Test the word completer with a simple word list."""
|
||||
doc = Document(text="ex", cursor_position=2)
|
||||
completions = list(word_completer.get_completions(doc, CompleteEvent()))
|
||||
assert len(completions) == 1
|
||||
assert completions[0].text == "example"
|
||||
|
||||
|
||||
def test_word_completer_case_insensitive(word_completer):
|
||||
"""Test the word completer with case-insensitive matching."""
|
||||
doc = Document(text="Ex", cursor_position=2)
|
||||
completions = list(word_completer.get_completions(doc, CompleteEvent()))
|
||||
assert len(completions) == 1
|
||||
assert completions[0].text == "example"
|
||||
|
||||
|
||||
def test_word_completer_no_match(word_completer):
|
||||
"""Test the word completer with no matches."""
|
||||
doc = Document(text="xyz", cursor_position=3)
|
||||
completions = list(word_completer.get_completions(doc, CompleteEvent()))
|
||||
assert len(completions) == 0
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def nested_completer():
|
||||
"""Return a nested completer."""
|
||||
from openbb_cli.config.completer import NestedCompleter
|
||||
|
||||
data = {
|
||||
"show": {
|
||||
"version": None,
|
||||
"interfaces": None,
|
||||
"clock": None,
|
||||
"ip": {"interface": {"brief": None}},
|
||||
},
|
||||
"exit": None,
|
||||
"enable": None,
|
||||
}
|
||||
return NestedCompleter.from_nested_dict(data)
|
||||
|
||||
|
||||
def test_nested_completer_root_command(nested_completer):
|
||||
"""Test the nested completer with a root command."""
|
||||
doc = Document(text="sh", cursor_position=2)
|
||||
completions = list(nested_completer.get_completions(doc, CompleteEvent()))
|
||||
assert "show" in [c.text for c in completions]
|
||||
|
||||
|
||||
def test_nested_completer_sub_command(nested_completer):
|
||||
"""Test the nested completer with a sub-command."""
|
||||
doc = Document(text="show ", cursor_position=5)
|
||||
completions = list(nested_completer.get_completions(doc, CompleteEvent()))
|
||||
assert "version" in [c.text for c in completions]
|
||||
assert "interfaces" in [c.text for c in completions]
|
||||
|
||||
|
||||
def test_nested_completer_no_match(nested_completer):
|
||||
"""Test the nested completer with no matches."""
|
||||
doc = Document(text="random ", cursor_position=7)
|
||||
completions = list(nested_completer.get_completions(doc, CompleteEvent()))
|
||||
assert len(completions) == 0
|
||||
47
cli/tests/test_config_console.py
Normal file
47
cli/tests/test_config_console.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Test Config Console."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from openbb_cli.config.console import Console
|
||||
from rich.text import Text
|
||||
|
||||
# pylint: disable=redefined-outer-name, unused-argument, unused-variable, protected-access
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_settings():
|
||||
"""Mock settings to inject into Console."""
|
||||
|
||||
class MockSettings:
|
||||
TEST_MODE = False
|
||||
ENABLE_RICH_PANEL = True
|
||||
SHOW_VERSION = True
|
||||
VERSION = "1.0"
|
||||
|
||||
return MockSettings()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def console(mock_settings):
|
||||
"""Create a Console instance with mocked settings."""
|
||||
with patch("rich.console.Console") as MockRichConsole: # noqa: F841
|
||||
return Console(settings=mock_settings)
|
||||
|
||||
|
||||
def test_print_without_panel(console, mock_settings):
|
||||
"""Test printing without a rich panel when disabled."""
|
||||
mock_settings.ENABLE_RICH_PANEL = False
|
||||
with patch.object(console._console, "print") as mock_print:
|
||||
console.print(text="Hello, world!", menu="Home Menu")
|
||||
mock_print.assert_called_once_with("Hello, world!")
|
||||
|
||||
|
||||
def test_blend_text():
|
||||
"""Test blending text colors."""
|
||||
message = "Hello"
|
||||
color1 = (255, 0, 0) # Red
|
||||
color2 = (0, 0, 255) # Blue
|
||||
blended_text = Console._blend_text(message, color1, color2)
|
||||
assert isinstance(blended_text, Text)
|
||||
assert "Hello" in blended_text.plain
|
||||
68
cli/tests/test_config_menu_text.py
Normal file
68
cli/tests/test_config_menu_text.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""Test Config Menu Text."""
|
||||
|
||||
import pytest
|
||||
from openbb_cli.config.menu_text import MenuText
|
||||
|
||||
# pylint: disable=redefined-outer-name, protected-access
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def menu_text():
|
||||
"""Fixture to create a MenuText instance for testing."""
|
||||
return MenuText(path="/test/path")
|
||||
|
||||
|
||||
def test_initialization(menu_text):
|
||||
"""Test initialization of the MenuText class."""
|
||||
assert menu_text.menu_text == ""
|
||||
assert menu_text.menu_path == "/test/path"
|
||||
assert menu_text.warnings == []
|
||||
|
||||
|
||||
def test_add_raw(menu_text):
|
||||
"""Test adding raw text."""
|
||||
menu_text.add_raw("Example raw text")
|
||||
assert "Example raw text" in menu_text.menu_text
|
||||
|
||||
|
||||
def test_add_info(menu_text):
|
||||
"""Test adding informational text."""
|
||||
menu_text.add_info("Info text")
|
||||
assert "[info]Info text:[/info]" in menu_text.menu_text
|
||||
|
||||
|
||||
def test_add_cmd(menu_text):
|
||||
"""Test adding a command."""
|
||||
menu_text.add_cmd("command", "Performs an action")
|
||||
assert "command" in menu_text.menu_text
|
||||
assert "Performs an action" in menu_text.menu_text
|
||||
|
||||
|
||||
def test_format_cmd_name(menu_text):
|
||||
"""Test formatting of command names that are too long."""
|
||||
long_name = "x" * 50 # Assuming CMD_NAME_LENGTH is 23
|
||||
formatted_name = menu_text._format_cmd_name(long_name)
|
||||
assert len(formatted_name) <= menu_text.CMD_NAME_LENGTH
|
||||
assert menu_text.warnings # Check that a warning was added
|
||||
|
||||
|
||||
def test_format_cmd_description(menu_text):
|
||||
"""Test truncation of long descriptions."""
|
||||
long_description = "y" * 100 # Assuming CMD_DESCRIPTION_LENGTH is 65
|
||||
formatted_description = menu_text._format_cmd_description("cmd", long_description)
|
||||
assert len(formatted_description) <= menu_text.CMD_DESCRIPTION_LENGTH
|
||||
|
||||
|
||||
def test_add_menu(menu_text):
|
||||
"""Test adding a menu item."""
|
||||
menu_text.add_menu("Settings", "Configure your settings")
|
||||
assert "Settings" in menu_text.menu_text
|
||||
assert "Configure your settings" in menu_text.menu_text
|
||||
|
||||
|
||||
def test_add_setting(menu_text):
|
||||
"""Test adding a setting."""
|
||||
menu_text.add_setting("Enable Feature", True, "Feature description")
|
||||
assert "Enable Feature" in menu_text.menu_text
|
||||
assert "Feature description" in menu_text.menu_text
|
||||
assert "[green]" in menu_text.menu_text
|
||||
49
cli/tests/test_config_setup.py
Normal file
49
cli/tests/test_config_setup.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Test the Config Setup."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from openbb_cli.config.setup import bootstrap
|
||||
|
||||
# pylint: disable=unused-variable
|
||||
|
||||
|
||||
def test_bootstrap_creates_directory_and_file():
|
||||
"""Test that bootstrap creates the settings directory and environment file."""
|
||||
with patch("pathlib.Path.mkdir") as mock_mkdir, patch(
|
||||
"pathlib.Path.touch"
|
||||
) as mock_touch:
|
||||
bootstrap()
|
||||
mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
|
||||
mock_touch.assert_called_once_with(exist_ok=True)
|
||||
|
||||
|
||||
def test_bootstrap_directory_exists():
|
||||
"""Test bootstrap when the directory already exists."""
|
||||
with patch("pathlib.Path.mkdir") as mock_mkdir, patch(
|
||||
"pathlib.Path.touch"
|
||||
) as mock_touch:
|
||||
bootstrap()
|
||||
mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
|
||||
mock_touch.assert_called_once_with(exist_ok=True)
|
||||
|
||||
|
||||
def test_bootstrap_file_exists():
|
||||
"""Test bootstrap when the environment file already exists."""
|
||||
with patch("pathlib.Path.mkdir") as mock_mkdir, patch(
|
||||
"pathlib.Path.touch"
|
||||
) as mock_touch:
|
||||
bootstrap()
|
||||
mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
|
||||
mock_touch.assert_called_once_with(exist_ok=True)
|
||||
|
||||
|
||||
def test_bootstrap_permission_error():
|
||||
"""Test bootstrap handles permission errors gracefully."""
|
||||
with patch("pathlib.Path.mkdir") as mock_mkdir, patch(
|
||||
"pathlib.Path.touch"
|
||||
) as mock_touch, pytest.raises( # noqa: F841
|
||||
PermissionError
|
||||
):
|
||||
mock_mkdir.side_effect = PermissionError("No permission to create directory")
|
||||
bootstrap() # Expecting to raise a PermissionError and be caught by pytest.raises
|
||||
60
cli/tests/test_config_style.py
Normal file
60
cli/tests/test_config_style.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""Test Config Style."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from openbb_cli.config.style import Style
|
||||
|
||||
# pylint: disable=redefined-outer-name, protected-access
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_style_directory(tmp_path):
|
||||
"""Fixture to create a mock styles directory."""
|
||||
(tmp_path / "styles" / "default").mkdir(parents=True, exist_ok=True)
|
||||
return tmp_path / "styles"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def style(mock_style_directory):
|
||||
"""Fixture to create a Style instance for testing."""
|
||||
return Style(directory=mock_style_directory)
|
||||
|
||||
|
||||
def test_initialization(style):
|
||||
"""Test that Style class initializes with default properties."""
|
||||
assert style.line_width == 1.5
|
||||
assert isinstance(style.console_style, dict)
|
||||
|
||||
|
||||
@patch("pathlib.Path.exists", MagicMock(return_value=True))
|
||||
@patch("pathlib.Path.rglob")
|
||||
def test_load_styles(mock_rglob, style, mock_style_directory):
|
||||
"""Test loading styles from directories."""
|
||||
mock_rglob.return_value = [mock_style_directory / "default" / "dark.richstyle.json"]
|
||||
style._load(mock_style_directory)
|
||||
assert "dark" in style.console_styles_available
|
||||
|
||||
|
||||
@patch("builtins.open", new_callable=MagicMock)
|
||||
@patch("json.load", MagicMock(return_value={"background": "black"}))
|
||||
def test_from_json(mock_open, style, mock_style_directory):
|
||||
"""Test loading style from a JSON file."""
|
||||
json_file = mock_style_directory / "dark.richstyle.json"
|
||||
result = style._from_json(json_file)
|
||||
assert result == {"background": "black"}
|
||||
mock_open.assert_called_once_with(json_file)
|
||||
|
||||
|
||||
def test_apply_invalid_style(style, mock_style_directory, capsys):
|
||||
"""Test applying an invalid style and falling back to default."""
|
||||
style.apply("nonexistent", mock_style_directory)
|
||||
captured = capsys.readouterr()
|
||||
assert "Invalid console style" in captured.out
|
||||
|
||||
|
||||
def test_available_styles(style):
|
||||
"""Test listing available styles."""
|
||||
style.console_styles_available = {"dark": Path("/path/to/dark.richstyle.json")}
|
||||
assert "dark" in style.available_styles
|
||||
79
cli/tests/test_controllers_base_controller.py
Normal file
79
cli/tests/test_controllers_base_controller.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""Test the base controller."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from openbb_cli.controllers.base_controller import BaseController
|
||||
|
||||
# pylint: disable=unused-argument, unused-variable
|
||||
|
||||
|
||||
class TestableBaseController(BaseController):
|
||||
"""Testable Base Controller."""
|
||||
|
||||
def __init__(self, queue=None):
|
||||
"""Initialize the TestableBaseController."""
|
||||
self.PATH = "/valid/path/"
|
||||
super().__init__(queue=queue)
|
||||
|
||||
def print_help(self):
|
||||
"""Print help."""
|
||||
|
||||
|
||||
def test_base_controller_initialization():
|
||||
"""Test the initialization of the base controller."""
|
||||
with patch.object(TestableBaseController, "check_path", return_value=None):
|
||||
controller = TestableBaseController()
|
||||
assert controller.path == ["valid", "path"] # Checking for correct path split
|
||||
|
||||
|
||||
def test_path_validation():
|
||||
"""Test the path validation method."""
|
||||
controller = TestableBaseController()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
controller.PATH = "invalid/path"
|
||||
controller.check_path()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
controller.PATH = "/invalid/path"
|
||||
controller.check_path()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
controller.PATH = "/Invalid/Path/"
|
||||
controller.check_path()
|
||||
|
||||
controller.PATH = "/valid/path/"
|
||||
|
||||
|
||||
def test_parse_input():
|
||||
"""Test the parse input method."""
|
||||
controller = TestableBaseController()
|
||||
input_str = "cmd1/cmd2/cmd3"
|
||||
expected = ["cmd1", "cmd2", "cmd3"]
|
||||
result = controller.parse_input(input_str)
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_switch():
|
||||
"""Test the switch method."""
|
||||
controller = TestableBaseController()
|
||||
with patch.object(controller, "call_exit", MagicMock()) as mock_exit:
|
||||
controller.queue = ["exit"]
|
||||
controller.switch("exit")
|
||||
mock_exit.assert_called_once()
|
||||
|
||||
|
||||
def test_call_help():
|
||||
"""Test the call help method."""
|
||||
controller = TestableBaseController()
|
||||
with patch("openbb_cli.controllers.base_controller.session.console.print"):
|
||||
controller.call_help(None)
|
||||
|
||||
|
||||
def test_call_exit():
|
||||
"""Test the call exit method."""
|
||||
controller = TestableBaseController()
|
||||
with patch.object(controller, "save_class", MagicMock()):
|
||||
controller.queue = ["quit"]
|
||||
controller.call_exit(None)
|
||||
70
cli/tests/test_controllers_base_platform_controller.py
Normal file
70
cli/tests/test_controllers_base_platform_controller.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Test the BasePlatformController."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from openbb_cli.controllers.base_platform_controller import PlatformController, Session
|
||||
|
||||
# pylint: disable=redefined-outer-name, protected-access, unused-argument, unused-variable
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session():
|
||||
"""Mock session fixture."""
|
||||
with patch(
|
||||
"openbb_cli.controllers.base_platform_controller.session",
|
||||
MagicMock(spec=Session),
|
||||
) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
def test_initialization_with_valid_params(mock_session):
|
||||
"""Test the initialization of the BasePlatformController."""
|
||||
translators = {"dummy_translator": MagicMock()}
|
||||
controller = PlatformController(
|
||||
name="test", parent_path=["parent"], translators=translators
|
||||
)
|
||||
assert controller._name == "test"
|
||||
assert controller.translators == translators
|
||||
|
||||
|
||||
def test_initialization_without_required_params():
|
||||
"""Test the initialization of the BasePlatformController without required params."""
|
||||
with pytest.raises(ValueError):
|
||||
PlatformController(name="test", parent_path=["parent"])
|
||||
|
||||
|
||||
def test_command_generation(mock_session):
|
||||
"""Test the command generation method."""
|
||||
translator = MagicMock()
|
||||
translators = {"test_command": translator}
|
||||
controller = PlatformController(
|
||||
name="test", parent_path=["parent"], translators=translators
|
||||
)
|
||||
|
||||
# Check if command function is correctly linked
|
||||
assert "test_command" in controller.translators
|
||||
|
||||
|
||||
def test_print_help(mock_session):
|
||||
"""Test the print help method."""
|
||||
translators = {"test_command": MagicMock()}
|
||||
controller = PlatformController(
|
||||
name="test", parent_path=["parent"], translators=translators
|
||||
)
|
||||
|
||||
with patch(
|
||||
"openbb_cli.controllers.base_platform_controller.MenuText"
|
||||
) as mock_menu_text:
|
||||
controller.print_help()
|
||||
mock_menu_text.assert_called_once_with("/parent/test/")
|
||||
|
||||
|
||||
def test_sub_controller_generation(mock_session):
|
||||
"""Test the sub controller generation method."""
|
||||
translators = {"test_menu_item": MagicMock()}
|
||||
controller = PlatformController(
|
||||
name="test", parent_path=["parent"], translators=translators
|
||||
)
|
||||
|
||||
assert "test_menu_item" in controller.translators
|
||||
51
cli/tests/test_controllers_choices.py
Normal file
51
cli/tests/test_controllers_choices.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""Test the choices controller."""
|
||||
|
||||
from argparse import ArgumentParser
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from openbb_cli.controllers.choices import (
|
||||
build_controller_choice_map,
|
||||
)
|
||||
|
||||
# pylint: disable=redefined-outer-name, protected-access, unused-argument, unused-variable
|
||||
|
||||
|
||||
class MockController:
|
||||
"""Mock controller class for testing."""
|
||||
|
||||
CHOICES_COMMANDS = ["test_command"]
|
||||
controller_choices = ["test_command", "help"]
|
||||
|
||||
def call_test_command(self, args):
|
||||
"""Mock function for test_command."""
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--example", choices=["option1", "option2"], help="Example argument."
|
||||
)
|
||||
return parser.parse_args(args)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_controller():
|
||||
"""Mock controller fixture."""
|
||||
return MockController()
|
||||
|
||||
|
||||
def test_build_command_choice_map(mock_controller):
|
||||
"""Test the building of a command choice map."""
|
||||
with patch(
|
||||
"openbb_cli.controllers.choices._get_argument_parser"
|
||||
) as mock_get_parser:
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--option", choices=["opt1", "opt2"], help="A choice option."
|
||||
)
|
||||
mock_get_parser.return_value = parser
|
||||
|
||||
choice_map = build_controller_choice_map(controller=mock_controller)
|
||||
|
||||
assert "test_command" in choice_map
|
||||
assert "--option" in choice_map["test_command"]
|
||||
assert "opt1" in choice_map["test_command"]["--option"]
|
||||
assert "opt2" in choice_map["test_command"]["--option"]
|
||||
79
cli/tests/test_controllers_cli_controller.py
Normal file
79
cli/tests/test_controllers_cli_controller.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""Test the CLI controller."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from openbb_cli.controllers.cli_controller import (
|
||||
CLIController,
|
||||
handle_job_cmds,
|
||||
parse_and_split_input,
|
||||
run_cli,
|
||||
)
|
||||
|
||||
# pylint: disable=redefined-outer-name, unused-argument
|
||||
|
||||
|
||||
def test_parse_and_split_input_custom_filters():
|
||||
"""Test the parse_and_split_input function with custom filters."""
|
||||
input_cmd = "query -q AAPL/P"
|
||||
result = parse_and_split_input(
|
||||
input_cmd, custom_filters=[r"((\ -q |\ --question|\ ).*?(/))"]
|
||||
)
|
||||
assert (
|
||||
"AAPL/P" not in result
|
||||
), "Should filter out terms that look like a sorting parameter"
|
||||
|
||||
|
||||
@patch("openbb_cli.controllers.cli_controller.CLIController.print_help")
|
||||
def test_cli_controller_print_help(mock_print_help):
|
||||
"""Test the CLIController print_help method."""
|
||||
controller = CLIController()
|
||||
controller.print_help()
|
||||
mock_print_help.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"controller_input, expected_output",
|
||||
[
|
||||
("settings", True),
|
||||
("random_command", False),
|
||||
],
|
||||
)
|
||||
def test_CLIController_has_command(controller_input, expected_output):
|
||||
"""Test the CLIController has_command method."""
|
||||
controller = CLIController()
|
||||
assert hasattr(controller, f"call_{controller_input}") == expected_output
|
||||
|
||||
|
||||
def test_handle_job_cmds_with_export_path():
|
||||
"""Test the handle_job_cmds function with an export path."""
|
||||
jobs_cmds = ["export /path/to/export some_command"]
|
||||
result = handle_job_cmds(jobs_cmds)
|
||||
expected = "some_command"
|
||||
assert expected in result[0] # type: ignore
|
||||
|
||||
|
||||
@patch("openbb_cli.controllers.cli_controller.CLIController.switch", return_value=[])
|
||||
@patch("openbb_cli.controllers.cli_controller.print_goodbye")
|
||||
def test_run_cli_quit_command(mock_print_goodbye, mock_switch):
|
||||
"""Test the run_cli function with the quit command."""
|
||||
run_cli(["quit"], test_mode=True)
|
||||
mock_print_goodbye.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.skip("This test is not working as expected")
|
||||
def test_execute_openbb_routine_with_mocked_requests():
|
||||
"""Test the call_exe function with mocked requests."""
|
||||
with patch("requests.get") as mock_get:
|
||||
response = MagicMock()
|
||||
response.status_code = 200
|
||||
response.json.return_value = {"script": "print('Hello World')"}
|
||||
mock_get.return_value = response
|
||||
# Here we need to call the correct function, assuming it's something like `call_exe` for URL-based scripts
|
||||
controller = CLIController()
|
||||
controller.call_exe(
|
||||
["--url", "https://my.openbb.co/u/test/routine/test.openbb"]
|
||||
)
|
||||
mock_get.assert_called_with(
|
||||
"https://my.openbb.co/u/test/routine/test.openbb?raw=true", timeout=10
|
||||
)
|
||||
61
cli/tests/test_controllers_controller_factory.py
Normal file
61
cli/tests/test_controllers_controller_factory.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Test the Controller Factory."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from openbb_cli.controllers.platform_controller_factory import (
|
||||
PlatformControllerFactory,
|
||||
)
|
||||
|
||||
# pylint: disable=redefined-outer-name, unused-argument
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_processor():
|
||||
"""Fixture to mock ArgparseClassProcessor."""
|
||||
with patch(
|
||||
"openbb_cli.controllers.platform_controller_factory.ArgparseClassProcessor"
|
||||
) as mock:
|
||||
instance = mock.return_value
|
||||
instance.paths = {"settings": "menu"}
|
||||
instance.translators = {"test_router_settings": MagicMock()}
|
||||
yield instance
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platform_router():
|
||||
"""Fixture to provide a mock platform_router class."""
|
||||
|
||||
class MockRouter:
|
||||
pass
|
||||
|
||||
return MockRouter
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def factory(platform_router, mock_processor):
|
||||
"""Fixture to create a PlatformControllerFactory with mocked dependencies."""
|
||||
return PlatformControllerFactory(
|
||||
platform_router=platform_router, reference={"test": "ref"}
|
||||
)
|
||||
|
||||
|
||||
def test_init(mock_processor):
|
||||
"""Test the initialization of the PlatformControllerFactory."""
|
||||
factory = PlatformControllerFactory(
|
||||
platform_router=MagicMock(), reference={"test": "ref"}
|
||||
)
|
||||
assert factory.router_name.lower() == "magicmock"
|
||||
assert factory.controller_name == "MagicmockController"
|
||||
|
||||
|
||||
def test_create_controller(factory):
|
||||
"""Test the creation of a controller class."""
|
||||
ControllerClass = factory.create()
|
||||
|
||||
assert "PlatformController" in [base.__name__ for base in ControllerClass.__bases__]
|
||||
assert ControllerClass.CHOICES_GENERATION
|
||||
assert "settings" in ControllerClass.CHOICES_MENUS
|
||||
assert "test_router_settings" not in [
|
||||
cmd.replace("test_router_", "") for cmd in ControllerClass.CHOICES_COMMANDS
|
||||
]
|
||||
106
cli/tests/test_controllers_script_parser.py
Normal file
106
cli/tests/test_controllers_script_parser.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""Test Script parser."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytest
|
||||
from openbb_cli.controllers.script_parser import (
|
||||
match_and_return_openbb_keyword_date,
|
||||
parse_openbb_script,
|
||||
)
|
||||
|
||||
# pylint: disable=import-outside-toplevel, unused-variable, line-too-long
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"command, expected",
|
||||
[
|
||||
("reset", True),
|
||||
("r", True),
|
||||
("r\n", True),
|
||||
("restart", False),
|
||||
],
|
||||
)
|
||||
def test_is_reset(command, expected):
|
||||
"""Test the is_reset function."""
|
||||
from openbb_cli.controllers.script_parser import is_reset
|
||||
|
||||
assert is_reset(command) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"keyword, expected_date",
|
||||
[
|
||||
(
|
||||
"$LASTFRIDAY",
|
||||
(
|
||||
datetime.now()
|
||||
- timedelta(days=((datetime.now().weekday() - 4) % 7 + 7) % 7)
|
||||
).strftime("%Y-%m-%d"),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_match_and_return_openbb_keyword_date(keyword, expected_date):
|
||||
"""Test the match_and_return_openbb_keyword_date function."""
|
||||
result = match_and_return_openbb_keyword_date(keyword)
|
||||
assert result == expected_date
|
||||
|
||||
|
||||
def test_parse_openbb_script_basic():
|
||||
"""Test the parse_openbb_script function."""
|
||||
raw_lines = ["echo 'Hello World'"]
|
||||
error, script = parse_openbb_script(raw_lines)
|
||||
assert error == ""
|
||||
assert script == "/echo 'Hello World'"
|
||||
|
||||
|
||||
def test_parse_openbb_script_with_variable():
|
||||
"""Test the parse_openbb_script function."""
|
||||
raw_lines = ["$VAR = 2022-01-01", "echo $VAR"]
|
||||
error, script = parse_openbb_script(raw_lines)
|
||||
assert error == ""
|
||||
assert script == "/echo 2022-01-01"
|
||||
|
||||
|
||||
def test_parse_openbb_script_with_foreach_loop():
|
||||
"""Test the parse_openbb_script function."""
|
||||
raw_lines = ["foreach $$DATE in 2022-01-01,2022-01-02", "echo $$DATE", "end"]
|
||||
error, script = parse_openbb_script(raw_lines)
|
||||
assert error == ""
|
||||
assert script == "/echo 2022-01-01/echo 2022-01-02"
|
||||
|
||||
|
||||
def test_parse_openbb_script_with_error():
|
||||
"""Test the parse_openbb_script function."""
|
||||
raw_lines = ["$VAR = ", "echo $VAR"]
|
||||
error, script = parse_openbb_script(raw_lines)
|
||||
assert "Variable $VAR not given" in error
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"line, expected",
|
||||
[
|
||||
(
|
||||
"foreach $$VAR in 2022-01-01",
|
||||
"[red]The script has a foreach loop that doesn't terminate. Add the keyword 'end' to explicitly terminate loop[/red]", # noqa: E501
|
||||
),
|
||||
("echo Hello World", ""),
|
||||
(
|
||||
"end",
|
||||
"[red]The script has a foreach loop that terminates before it gets started. Add the keyword 'foreach' to explicitly start loop[/red]", # noqa: E501
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_parse_openbb_script_foreach_errors(line, expected):
|
||||
"""Test the parse_openbb_script function."""
|
||||
error, script = parse_openbb_script([line])
|
||||
assert error == expected
|
||||
|
||||
|
||||
def test_date_keyword_last_friday():
|
||||
"""Test the match_and_return_openbb_keyword_date function."""
|
||||
today = datetime.now()
|
||||
last_friday = today - timedelta(days=(today.weekday() - 4 + 7) % 7)
|
||||
if last_friday > today:
|
||||
last_friday -= timedelta(days=7)
|
||||
expected_date = last_friday.strftime("%Y-%m-%d")
|
||||
assert match_and_return_openbb_keyword_date("$LASTFRIDAY") == expected_date
|
||||
135
cli/tests/test_controllers_settings_controller.py
Normal file
135
cli/tests/test_controllers_settings_controller.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""Test the SettingsController class."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from openbb_cli.controllers.settings_controller import SettingsController
|
||||
|
||||
# pylint: disable=redefined-outer-name, unused-argument
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session():
|
||||
with patch("openbb_cli.controllers.settings_controller.session") as mock:
|
||||
|
||||
mock.settings.USE_INTERACTIVE_DF = False
|
||||
mock.settings.ALLOWED_NUMBER_OF_ROWS = 20
|
||||
mock.settings.TIMEZONE = "UTC"
|
||||
|
||||
mock.settings.set_item = MagicMock()
|
||||
|
||||
yield mock
|
||||
|
||||
|
||||
def test_call_interactive(mock_session):
|
||||
controller = SettingsController()
|
||||
controller.call_interactive(None)
|
||||
mock_session.settings.set_item.assert_called_once_with("USE_INTERACTIVE_DF", True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"input_rows, expected",
|
||||
[
|
||||
(10, 10),
|
||||
(15, 15),
|
||||
],
|
||||
)
|
||||
def test_call_n_rows(input_rows, expected, mock_session):
|
||||
controller = SettingsController()
|
||||
args = ["--rows", str(input_rows)]
|
||||
controller.call_n_rows(args)
|
||||
mock_session.settings.set_item.assert_called_with(
|
||||
"ALLOWED_NUMBER_OF_ROWS", expected
|
||||
)
|
||||
|
||||
|
||||
def test_call_n_rows_no_args_provided(mock_session):
|
||||
controller = SettingsController()
|
||||
controller.call_n_rows([])
|
||||
mock_session.console.print.assert_called_with("Current number of rows: 20")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"timezone, valid",
|
||||
[
|
||||
("UTC", True),
|
||||
("Mars/Phobos", False),
|
||||
],
|
||||
)
|
||||
def test_call_timezone(timezone, valid, mock_session):
|
||||
with patch(
|
||||
"openbb_cli.controllers.settings_controller.is_timezone_valid",
|
||||
return_value=valid,
|
||||
):
|
||||
controller = SettingsController()
|
||||
args = ["--timezone", timezone]
|
||||
controller.call_timezone(args)
|
||||
if valid:
|
||||
mock_session.settings.set_item.assert_called_with("TIMEZONE", timezone)
|
||||
else:
|
||||
mock_session.settings.set_item.assert_not_called()
|
||||
|
||||
|
||||
def test_call_console_style(mock_session):
|
||||
controller = SettingsController()
|
||||
args = ["--style", "dark"]
|
||||
controller.call_console_style(args)
|
||||
mock_session.console.print.assert_called()
|
||||
|
||||
|
||||
def test_call_console_style_no_args(mock_session):
|
||||
mock_session.settings.RICH_STYLE = "default"
|
||||
controller = SettingsController()
|
||||
controller.call_console_style([])
|
||||
mock_session.console.print.assert_called_with("Current console style: default")
|
||||
|
||||
|
||||
def test_call_flair(mock_session):
|
||||
controller = SettingsController()
|
||||
args = ["--flair", "rocket"]
|
||||
controller.call_flair(args)
|
||||
|
||||
|
||||
def test_call_flair_no_args(mock_session):
|
||||
mock_session.settings.FLAIR = "star"
|
||||
controller = SettingsController()
|
||||
controller.call_flair([])
|
||||
mock_session.console.print.assert_called_with("Current flair: star")
|
||||
|
||||
|
||||
def test_call_obbject_display(mock_session):
|
||||
controller = SettingsController()
|
||||
args = ["--number", "5"]
|
||||
controller.call_obbject_display(args)
|
||||
mock_session.settings.set_item.assert_called_once_with(
|
||||
"N_TO_DISPLAY_OBBJECT_REGISTRY", 5
|
||||
)
|
||||
|
||||
|
||||
def test_call_obbject_display_no_args(mock_session):
|
||||
mock_session.settings.N_TO_DISPLAY_OBBJECT_REGISTRY = 10
|
||||
controller = SettingsController()
|
||||
controller.call_obbject_display([])
|
||||
mock_session.console.print.assert_called_with(
|
||||
"Current number of results to display from the OBBject registry: 10"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"args, expected",
|
||||
[
|
||||
(["--rows", "50"], 50),
|
||||
(["--rows", "100"], 100),
|
||||
([], 20),
|
||||
],
|
||||
)
|
||||
def test_call_n_rows_v2(args, expected, mock_session):
|
||||
mock_session.settings.ALLOWED_NUMBER_OF_ROWS = 20
|
||||
controller = SettingsController()
|
||||
controller.call_n_rows(args)
|
||||
if args:
|
||||
mock_session.settings.set_item.assert_called_with(
|
||||
"ALLOWED_NUMBER_OF_ROWS", expected
|
||||
)
|
||||
else:
|
||||
mock_session.console.print.assert_called_with("Current number of rows: 20")
|
||||
157
cli/tests/test_controllers_utils.py
Normal file
157
cli/tests/test_controllers_utils.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""Test the Controller utils."""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from openbb_cli.controllers.utils import (
|
||||
check_non_negative,
|
||||
check_positive,
|
||||
get_flair_and_username,
|
||||
get_user_agent,
|
||||
parse_and_split_input,
|
||||
print_goodbye,
|
||||
print_guest_block_msg,
|
||||
remove_file,
|
||||
reset,
|
||||
welcome_message,
|
||||
)
|
||||
|
||||
# pylint: disable=redefined-outer-name, unused-argument
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session():
|
||||
"""Mock the session and its dependencies."""
|
||||
with patch("openbb_cli.controllers.utils.Session", autospec=True) as mock:
|
||||
mock.return_value.console.print = MagicMock()
|
||||
mock.return_value.is_local = MagicMock(return_value=True)
|
||||
mock.return_value.settings.VERSION = "1.0"
|
||||
mock.return_value.user.profile.hub_session.username = "testuser"
|
||||
mock.return_value.settings.FLAIR = "rocket"
|
||||
yield mock
|
||||
|
||||
|
||||
def test_remove_file_existing_file():
|
||||
"""Test removing an existing file."""
|
||||
with patch("os.path.isfile", return_value=True), patch("os.remove") as mock_remove:
|
||||
assert remove_file(Path("/path/to/file"))
|
||||
mock_remove.assert_called_once()
|
||||
|
||||
|
||||
def test_remove_file_directory():
|
||||
"""Test removing a directory."""
|
||||
with patch("os.path.isfile", return_value=False), patch(
|
||||
"os.path.isdir", return_value=True
|
||||
), patch("shutil.rmtree") as mock_rmtree:
|
||||
assert remove_file(Path("/path/to/directory"))
|
||||
mock_rmtree.assert_called_once()
|
||||
|
||||
|
||||
def test_remove_file_failure(mock_session):
|
||||
"""Test removing a file that fails."""
|
||||
with patch("os.path.isfile", return_value=True), patch(
|
||||
"os.remove", side_effect=Exception("Error")
|
||||
):
|
||||
assert not remove_file(Path("/path/to/file"))
|
||||
mock_session.return_value.console.print.assert_called()
|
||||
|
||||
|
||||
def test_print_goodbye(mock_session):
|
||||
"""Test printing the goodbye message."""
|
||||
print_goodbye()
|
||||
mock_session.return_value.console.print.assert_called()
|
||||
|
||||
|
||||
def test_reset(mock_session):
|
||||
"""Test resetting the CLI."""
|
||||
with patch("openbb_cli.controllers.utils.remove_file"), patch(
|
||||
"sys.modules", new_callable=dict
|
||||
):
|
||||
reset()
|
||||
mock_session.return_value.console.print.assert_called()
|
||||
|
||||
|
||||
def test_parse_and_split_input():
|
||||
"""Test parsing and splitting user input."""
|
||||
user_input = "ls -f /home/user/docs/document.xlsx"
|
||||
result = parse_and_split_input(user_input, [])
|
||||
assert "ls" in result[0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"input_command, expected_output",
|
||||
[
|
||||
("/", ["home"]),
|
||||
("ls -f /path/to/file.txt", ["ls -f ", "path", "to", "file.txt"]),
|
||||
("rm -f /home/user/docs", ["rm -f ", "home", "user", "docs"]),
|
||||
],
|
||||
)
|
||||
def test_parse_and_split_input_special_cases(input_command, expected_output):
|
||||
"""Test parsing and splitting user input with special cases."""
|
||||
result = parse_and_split_input(input_command, [])
|
||||
assert result == expected_output
|
||||
|
||||
|
||||
def test_print_guest_block_msg(mock_session):
|
||||
"""Test printing the guest block message."""
|
||||
print_guest_block_msg()
|
||||
mock_session.return_value.console.print.assert_called()
|
||||
|
||||
|
||||
def test_welcome_message(mock_session):
|
||||
"""Test printing the welcome message."""
|
||||
welcome_message()
|
||||
mock_session.return_value.console.print.assert_called_with(
|
||||
"\nWelcome to OpenBB Platform CLI v1.0"
|
||||
)
|
||||
|
||||
|
||||
def test_get_flair_and_username(mock_session):
|
||||
"""Test getting the flair and username."""
|
||||
result = get_flair_and_username()
|
||||
assert "testuser" in result
|
||||
assert "rocket" in result #
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, expected",
|
||||
[
|
||||
("10", 10),
|
||||
("0", 0),
|
||||
("-1", pytest.raises(argparse.ArgumentTypeError)),
|
||||
("text", pytest.raises(ValueError)),
|
||||
],
|
||||
)
|
||||
def test_check_non_negative(value, expected):
|
||||
"""Test checking for a non-negative value."""
|
||||
if isinstance(expected, int):
|
||||
assert check_non_negative(value) == expected
|
||||
else:
|
||||
with expected:
|
||||
check_non_negative(value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value, expected",
|
||||
[
|
||||
("1", 1),
|
||||
("0", pytest.raises(argparse.ArgumentTypeError)),
|
||||
("-1", pytest.raises(argparse.ArgumentTypeError)),
|
||||
("text", pytest.raises(ValueError)),
|
||||
],
|
||||
)
|
||||
def test_check_positive(value, expected):
|
||||
"""Test checking for a positive value."""
|
||||
if isinstance(expected, int):
|
||||
assert check_positive(value) == expected
|
||||
else:
|
||||
with expected:
|
||||
check_positive(value)
|
||||
|
||||
|
||||
def test_get_user_agent():
|
||||
"""Test getting the user agent."""
|
||||
result = get_user_agent()
|
||||
assert result.startswith("Mozilla/5.0")
|
||||
72
cli/tests/test_models_settings.py
Normal file
72
cli/tests/test_models_settings.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Test the Models Settings module."""
|
||||
|
||||
from unittest.mock import mock_open, patch
|
||||
|
||||
from openbb_cli.models.settings import Settings
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
|
||||
def test_default_values():
|
||||
"""Test the default values of the settings model."""
|
||||
settings = Settings()
|
||||
assert settings.VERSION == "1.0.0"
|
||||
assert settings.TEST_MODE is False
|
||||
assert settings.DEBUG_MODE is False
|
||||
assert settings.DEV_BACKEND is False
|
||||
assert settings.FILE_OVERWRITE is False
|
||||
assert settings.SHOW_VERSION is True
|
||||
assert settings.USE_INTERACTIVE_DF is True
|
||||
assert settings.USE_CLEAR_AFTER_CMD is False
|
||||
assert settings.USE_DATETIME is True
|
||||
assert settings.USE_PROMPT_TOOLKIT is True
|
||||
assert settings.ENABLE_EXIT_AUTO_HELP is True
|
||||
assert settings.REMEMBER_CONTEXTS is True
|
||||
assert settings.ENABLE_RICH_PANEL is True
|
||||
assert settings.TOOLBAR_HINT is True
|
||||
assert settings.SHOW_MSG_OBBJECT_REGISTRY is False
|
||||
assert settings.TIMEZONE == "America/New_York"
|
||||
assert settings.FLAIR == ":openbb"
|
||||
assert settings.PREVIOUS_USE is False
|
||||
assert settings.N_TO_KEEP_OBBJECT_REGISTRY == 10
|
||||
assert settings.N_TO_DISPLAY_OBBJECT_REGISTRY == 5
|
||||
assert settings.RICH_STYLE == "dark"
|
||||
assert settings.ALLOWED_NUMBER_OF_ROWS == 20
|
||||
assert settings.ALLOWED_NUMBER_OF_COLUMNS == 5
|
||||
assert settings.HUB_URL == "https://my.openbb.co"
|
||||
assert settings.BASE_URL == "https://payments.openbb.co"
|
||||
|
||||
|
||||
# Test __repr__ output
|
||||
def test_repr():
|
||||
"""Test the __repr__ method of the settings model."""
|
||||
settings = Settings()
|
||||
repr_str = settings.__repr__() # pylint: disable=C2801
|
||||
assert "Settings\n\n" in repr_str
|
||||
assert "VERSION: 1.0.0" in repr_str
|
||||
|
||||
|
||||
# Test loading from environment variables
|
||||
@patch(
|
||||
"openbb_cli.models.settings.dotenv_values",
|
||||
return_value={"OPENBB_TEST_MODE": "True", "OPENBB_VERSION": "2.0.0"},
|
||||
)
|
||||
def test_from_env(mock_dotenv_values):
|
||||
"""Test loading settings from environment variables."""
|
||||
settings = Settings.from_env({}) # type: ignore
|
||||
assert settings["TEST_MODE"] == "True"
|
||||
assert settings["VERSION"] == "2.0.0"
|
||||
|
||||
|
||||
# Test setting an item and updating .env
|
||||
@patch("openbb_cli.models.settings.set_key")
|
||||
@patch(
|
||||
"openbb_cli.models.settings.open",
|
||||
new_callable=mock_open,
|
||||
read_data="TEST_MODE=False\n",
|
||||
)
|
||||
def test_set_item(mock_file, mock_set_key):
|
||||
"""Test setting an item and updating the .env file."""
|
||||
settings = Settings()
|
||||
settings.set_item("TEST_MODE", True)
|
||||
assert settings.TEST_MODE is True
|
||||
44
cli/tests/test_session.py
Normal file
44
cli/tests/test_session.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"Test the Session class."
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from openbb_cli.models.settings import Settings
|
||||
from openbb_cli.session import Session, sys
|
||||
|
||||
# pylint: disable=redefined-outer-name, unused-argument, protected-access
|
||||
|
||||
|
||||
def mock_isatty(return_value):
|
||||
"""Mock the isatty method."""
|
||||
original_isatty = sys.stdin.isatty
|
||||
sys.stdin.isatty = MagicMock(return_value=return_value) # type: ignore
|
||||
return original_isatty
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session():
|
||||
"""Session fixture."""
|
||||
return Session()
|
||||
|
||||
|
||||
def test_session_initialization(session):
|
||||
"""Test the initialization of the Session class."""
|
||||
assert session.settings is not None
|
||||
assert session.style is not None
|
||||
assert session.console is not None
|
||||
assert session.obbject_registry is not None
|
||||
assert isinstance(session.settings, Settings)
|
||||
|
||||
|
||||
@patch("sys.stdin.isatty", return_value=True)
|
||||
def test_get_prompt_session_true(mock_isatty, session):
|
||||
"Test get_prompt_session method."
|
||||
prompt_session = session._get_prompt_session()
|
||||
assert prompt_session is not None
|
||||
|
||||
|
||||
@patch("sys.stdin.isatty", return_value=False)
|
||||
def test_get_prompt_session_false(mock_isatty, session):
|
||||
"Test get_prompt_session method."
|
||||
prompt_session = session._get_prompt_session()
|
||||
assert prompt_session is None
|
||||
@@ -129,7 +129,7 @@ def install_local(_extras: bool = False):
|
||||
print("Restoring pyproject.toml and poetry.lock") # noqa: T201
|
||||
|
||||
finally:
|
||||
# Revert pyproject.toml and poetry.lock to their original state
|
||||
# Revert pyproject.toml and poetry.lock to their original state.
|
||||
with open(PYPROJECT, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(original_pyproject)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user