Merge branch 'develop' into ci/ccxt.pro
This commit is contained in:
@@ -318,6 +318,17 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mypy freqtrade scripts tests
|
mypy freqtrade scripts tests
|
||||||
|
|
||||||
|
- name: Run Pester tests (PowerShell)
|
||||||
|
run: |
|
||||||
|
$PSVersionTable
|
||||||
|
Set-PSRepository psgallery -InstallationPolicy trusted
|
||||||
|
Install-Module -Name Pester -RequiredVersion 5.3.1 -Confirm:$false -Force
|
||||||
|
$Error.clear()
|
||||||
|
Invoke-Pester -Path "tests" -CI
|
||||||
|
if ($Error.Length -gt 0) {exit 1}
|
||||||
|
|
||||||
|
shell: powershell
|
||||||
|
|
||||||
- name: Discord notification
|
- name: Discord notification
|
||||||
uses: rjstone/discord-webhook-notify@v1
|
uses: rjstone/discord-webhook-notify@v1
|
||||||
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
|
||||||
@@ -566,11 +577,11 @@ jobs:
|
|||||||
docker version -f '{{.Server.Experimental}}'
|
docker version -f '{{.Server.Experimental}}'
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Available platforms
|
- name: Available platforms
|
||||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ repos:
|
|||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.3.0.7
|
- types-cachetools==5.3.0.7
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.32.0.20240523
|
- types-requests==2.32.0.20240602
|
||||||
- types-tabulate==0.9.0.20240106
|
- types-tabulate==0.9.0.20240106
|
||||||
- types-python-dateutil==2.9.0.20240316
|
- types-python-dateutil==2.9.0.20240316
|
||||||
- SQLAlchemy==2.0.30
|
- SQLAlchemy==2.0.30
|
||||||
@@ -31,7 +31,7 @@ repos:
|
|||||||
|
|
||||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: 'v0.4.5'
|
rev: 'v0.4.7'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
markdown==3.6
|
markdown==3.6
|
||||||
mkdocs==1.6.0
|
mkdocs==1.6.0
|
||||||
mkdocs-material==9.5.24
|
mkdocs-material==9.5.25
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==10.8.1
|
pymdown-extensions==10.8.1
|
||||||
jinja2==3.1.4
|
jinja2==3.1.4
|
||||||
|
|||||||
@@ -5,6 +5,30 @@ We **strongly** recommend that Windows users use [Docker](docker_quickstart.md)
|
|||||||
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
|
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
|
||||||
Otherwise, please follow the instructions below.
|
Otherwise, please follow the instructions below.
|
||||||
|
|
||||||
|
All instructions assume that python 3.9+ is installed and available.
|
||||||
|
|
||||||
|
## Clone the git repository
|
||||||
|
|
||||||
|
First of all clone the repository by running:
|
||||||
|
|
||||||
|
``` powershell
|
||||||
|
git clone https://github.com/freqtrade/freqtrade.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, choose your installation method, either automatically via script (recommended) or manually following the corresponding instructions.
|
||||||
|
|
||||||
|
## Install freqtrade automatically
|
||||||
|
|
||||||
|
### Run the installation script
|
||||||
|
|
||||||
|
The script will ask you a few questions to determine which parts should be installed.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-ExecutionPolicy -ExecutionPolicy Bypass
|
||||||
|
cd freqtrade
|
||||||
|
. .\setup.ps1
|
||||||
|
```
|
||||||
|
|
||||||
## Install freqtrade manually
|
## Install freqtrade manually
|
||||||
|
|
||||||
!!! Note "64bit Python version"
|
!!! Note "64bit Python version"
|
||||||
@@ -14,13 +38,7 @@ Otherwise, please follow the instructions below.
|
|||||||
!!! Hint
|
!!! Hint
|
||||||
Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#installation-with-conda) in the documentation for more information.
|
Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#installation-with-conda) in the documentation for more information.
|
||||||
|
|
||||||
### 1. Clone the git repository
|
### Install ta-lib
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/freqtrade/freqtrade.git
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Install ta-lib
|
|
||||||
|
|
||||||
Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#windows).
|
Install ta-lib according to the [ta-lib documentation](https://github.com/TA-Lib/ta-lib-python#windows).
|
||||||
|
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ class Exchange:
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
self._api: ccxt.Exchange
|
self._api: ccxt.Exchange
|
||||||
self._api_async: ccxt_pro.Exchange = None
|
self._api_async: ccxt_pro.Exchange
|
||||||
self._ws_async: ccxt_pro.Exchange = None
|
self._ws_async: ccxt_pro.Exchange = None
|
||||||
self._exchange_ws: Optional[ExchangeWS] = None
|
self._exchange_ws: Optional[ExchangeWS] = None
|
||||||
self._markets: Dict = {}
|
self._markets: Dict = {}
|
||||||
@@ -252,7 +252,7 @@ class Exchange:
|
|||||||
self.required_candle_call_count = 1
|
self.required_candle_call_count = 1
|
||||||
if validate:
|
if validate:
|
||||||
# Initial markets load
|
# Initial markets load
|
||||||
self._load_markets()
|
self.reload_markets(True, load_leverage_tiers=False)
|
||||||
self.validate_config(config)
|
self.validate_config(config)
|
||||||
self._startup_candle_count: int = config.get("startup_candle_count", 0)
|
self._startup_candle_count: int = config.get("startup_candle_count", 0)
|
||||||
self.required_candle_call_count = self.validate_required_startup_candles(
|
self.required_candle_call_count = self.validate_required_startup_candles(
|
||||||
@@ -387,7 +387,7 @@ class Exchange:
|
|||||||
"""exchange ccxt markets"""
|
"""exchange ccxt markets"""
|
||||||
if not self._markets:
|
if not self._markets:
|
||||||
logger.info("Markets were not loaded. Loading them now..")
|
logger.info("Markets were not loaded. Loading them now..")
|
||||||
self._load_markets()
|
self.reload_markets(True)
|
||||||
return self._markets
|
return self._markets
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -570,30 +570,26 @@ class Exchange:
|
|||||||
if self._exchange_ws:
|
if self._exchange_ws:
|
||||||
self._exchange_ws.reset_connections()
|
self._exchange_ws.reset_connections()
|
||||||
|
|
||||||
def _load_async_markets(self, reload: bool = False) -> None:
|
def _load_async_markets(self, reload: bool = False) -> Dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
if self._api_async:
|
markets = self.loop.run_until_complete(
|
||||||
self.loop.run_until_complete(self._api_async.load_markets(reload=reload, params={}))
|
self._api_async.load_markets(reload=reload, params={})
|
||||||
|
)
|
||||||
|
|
||||||
except (asyncio.TimeoutError, ccxt.BaseError) as e:
|
if isinstance(markets, Exception):
|
||||||
logger.warning("Could not load async markets. Reason: %s", e)
|
raise markets
|
||||||
return
|
return markets
|
||||||
|
except asyncio.TimeoutError as e:
|
||||||
|
logger.warning("Could not load markets. Reason: %s", e)
|
||||||
|
raise TemporaryError from e
|
||||||
|
|
||||||
def _load_markets(self) -> None:
|
def reload_markets(self, force: bool = False, *, load_leverage_tiers: bool = True) -> None:
|
||||||
"""Initialize markets both sync and async"""
|
"""
|
||||||
try:
|
Reload / Initialize markets both sync and async if refresh interval has passed
|
||||||
self._markets = self._api.load_markets(params={})
|
|
||||||
self._load_async_markets()
|
|
||||||
self._last_markets_refresh = dt_ts()
|
|
||||||
if self._ft_has["needs_trading_fees"]:
|
|
||||||
self._trading_fees = self.fetch_trading_fees()
|
|
||||||
|
|
||||||
except ccxt.BaseError:
|
"""
|
||||||
logger.exception("Unable to initialize markets.")
|
|
||||||
|
|
||||||
def reload_markets(self, force: bool = False) -> None:
|
|
||||||
"""Reload markets both sync and async if refresh interval has passed"""
|
|
||||||
# Check whether markets have to be reloaded
|
# Check whether markets have to be reloaded
|
||||||
|
is_initial = self._last_markets_refresh == 0
|
||||||
if (
|
if (
|
||||||
not force
|
not force
|
||||||
and self._last_markets_refresh > 0
|
and self._last_markets_refresh > 0
|
||||||
@@ -602,16 +598,21 @@ class Exchange:
|
|||||||
return None
|
return None
|
||||||
logger.debug("Performing scheduled market reload..")
|
logger.debug("Performing scheduled market reload..")
|
||||||
try:
|
try:
|
||||||
self._markets = self._api.load_markets(reload=True, params={})
|
# Reload async markets, then assign them to sync api
|
||||||
# Also reload async markets to avoid issues with newly listed pairs
|
self._markets = self._load_async_markets(reload=True)
|
||||||
self._load_async_markets(reload=True)
|
self._api.set_markets(self._api_async.markets, self._api_async.currencies)
|
||||||
if self._exchange_ws:
|
if self._exchange_ws:
|
||||||
# Set markets to avoid reloading on websocket api
|
# Set markets to avoid reloading on websocket api
|
||||||
self._ws_async.set_markets(self._api.markets, self._api.currencies)
|
self._ws_async.set_markets(self._api.markets, self._api.currencies)
|
||||||
self._last_markets_refresh = dt_ts()
|
self._last_markets_refresh = dt_ts()
|
||||||
self.fill_leverage_tiers()
|
|
||||||
except ccxt.BaseError:
|
if is_initial and self._ft_has["needs_trading_fees"]:
|
||||||
logger.exception("Could not reload markets.")
|
self._trading_fees = self.fetch_trading_fees()
|
||||||
|
|
||||||
|
if load_leverage_tiers and self.trading_mode == TradingMode.FUTURES:
|
||||||
|
self.fill_leverage_tiers()
|
||||||
|
except (ccxt.BaseError, TemporaryError):
|
||||||
|
logger.exception("Could not load markets.")
|
||||||
|
|
||||||
def validate_stakecurrency(self, stake_currency: str) -> None:
|
def validate_stakecurrency(self, stake_currency: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
# Requirements for freqtrade client library
|
# Requirements for freqtrade client library
|
||||||
requests==2.32.2
|
requests==2.32.3
|
||||||
python-rapidjson==1.17
|
python-rapidjson==1.17
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
site_name: Freqtrade
|
site_name: Freqtrade
|
||||||
site_url: !ENV [READTHEDOCS_CANONICAL_URL, 'https://www.freqtrade.io/en/latest/']
|
site_url: !ENV [READTHEDOCS_CANONICAL_URL, 'https://www.freqtrade.io/en/latest/']
|
||||||
|
site_description: Freqtrade is a free and open source crypto trading bot written in Python, designed to support all major exchanges and be controlled via Telegram or builtin Web UI
|
||||||
repo_url: https://github.com/freqtrade/freqtrade
|
repo_url: https://github.com/freqtrade/freqtrade
|
||||||
edit_uri: edit/develop/docs/
|
edit_uri: edit/develop/docs/
|
||||||
use_directory_urls: True
|
use_directory_urls: True
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
-r docs/requirements-docs.txt
|
-r docs/requirements-docs.txt
|
||||||
|
|
||||||
coveralls==4.0.1
|
coveralls==4.0.1
|
||||||
ruff==0.4.5
|
ruff==0.4.7
|
||||||
mypy==1.10.0
|
mypy==1.10.0
|
||||||
pre-commit==3.7.1
|
pre-commit==3.7.1
|
||||||
pytest==8.2.1
|
pytest==8.2.1
|
||||||
@@ -27,6 +27,6 @@ nbconvert==7.16.4
|
|||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==5.3.0.7
|
types-cachetools==5.3.0.7
|
||||||
types-filelock==3.2.7
|
types-filelock==3.2.7
|
||||||
types-requests==2.32.0.20240523
|
types-requests==2.32.0.20240602
|
||||||
types-tabulate==0.9.0.20240106
|
types-tabulate==0.9.0.20240106
|
||||||
types-python-dateutil==2.9.0.20240316
|
types-python-dateutil==2.9.0.20240316
|
||||||
|
|||||||
+7
-5
@@ -1,8 +1,10 @@
|
|||||||
numpy==1.26.4
|
numpy==1.26.4
|
||||||
pandas==2.2.2
|
pandas==2.2.2
|
||||||
|
bottleneck==1.3.8
|
||||||
|
numexpr==2.10.0
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==4.3.35
|
ccxt==4.3.38
|
||||||
cryptography==42.0.7
|
cryptography==42.0.7
|
||||||
aiohttp==3.9.5
|
aiohttp==3.9.5
|
||||||
SQLAlchemy==2.0.30
|
SQLAlchemy==2.0.30
|
||||||
@@ -11,10 +13,10 @@ python-telegram-bot==21.2
|
|||||||
httpx>=0.24.1
|
httpx>=0.24.1
|
||||||
humanize==4.9.0
|
humanize==4.9.0
|
||||||
cachetools==5.3.3
|
cachetools==5.3.3
|
||||||
requests==2.32.2
|
requests==2.32.3
|
||||||
urllib3==2.2.1
|
urllib3==2.2.1
|
||||||
jsonschema==4.22.0
|
jsonschema==4.22.0
|
||||||
TA-Lib==0.4.29
|
TA-Lib==0.4.30
|
||||||
technical==1.4.3
|
technical==1.4.3
|
||||||
tabulate==0.9.0
|
tabulate==0.9.0
|
||||||
pycoingecko==3.1.0
|
pycoingecko==3.1.0
|
||||||
@@ -37,8 +39,8 @@ sdnotify==0.3.2
|
|||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.111.0
|
fastapi==0.111.0
|
||||||
pydantic==2.7.1
|
pydantic==2.7.2
|
||||||
uvicorn==0.29.0
|
uvicorn==0.30.1
|
||||||
pyjwt==2.8.0
|
pyjwt==2.8.0
|
||||||
aiofiles==23.2.1
|
aiofiles==23.2.1
|
||||||
psutil==5.9.8
|
psutil==5.9.8
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
Clear-Host
|
||||||
|
|
||||||
|
$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||||
|
$Global:LogFilePath = Join-Path $env:TEMP "script_log_$Timestamp.txt"
|
||||||
|
|
||||||
|
$RequirementFiles = @("requirements.txt", "requirements-dev.txt", "requirements-hyperopt.txt", "requirements-freqai.txt", "requirements-freqai-rl.txt", "requirements-plot.txt")
|
||||||
|
$VenvName = ".venv"
|
||||||
|
$VenvDir = Join-Path $PSScriptRoot $VenvName
|
||||||
|
|
||||||
|
function Write-Log {
|
||||||
|
param (
|
||||||
|
[string]$Message,
|
||||||
|
[string]$Level = 'INFO'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-Path -Path $LogFilePath)) {
|
||||||
|
New-Item -ItemType File -Path $LogFilePath -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($Level) {
|
||||||
|
'INFO' { Write-Host $Message -ForegroundColor Green }
|
||||||
|
'WARNING' { Write-Host $Message -ForegroundColor Yellow }
|
||||||
|
'ERROR' { Write-Host $Message -ForegroundColor Red }
|
||||||
|
'PROMPT' { Write-Host $Message -ForegroundColor Cyan }
|
||||||
|
}
|
||||||
|
|
||||||
|
"${Level}: $Message" | Out-File $LogFilePath -Append
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-UserSelection {
|
||||||
|
param (
|
||||||
|
[string]$Prompt,
|
||||||
|
[string[]]$Options,
|
||||||
|
[string]$DefaultChoice = 'A',
|
||||||
|
[bool]$AllowMultipleSelections = $true
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Log "$Prompt`n" -Level 'PROMPT'
|
||||||
|
for ($I = 0; $I -lt $Options.Length; $I++) {
|
||||||
|
Write-Log "$([char](65 + $I)). $($Options[$I])" -Level 'PROMPT'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($AllowMultipleSelections) {
|
||||||
|
Write-Log "`nSelect one or more options by typing the corresponding letters, separated by commas." -Level 'PROMPT'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log "`nSelect an option by typing the corresponding letter." -Level 'PROMPT'
|
||||||
|
}
|
||||||
|
|
||||||
|
[string]$UserInput = Read-Host
|
||||||
|
if ([string]::IsNullOrEmpty($UserInput)) {
|
||||||
|
$UserInput = $DefaultChoice
|
||||||
|
}
|
||||||
|
$UserInput = $UserInput.ToUpper()
|
||||||
|
|
||||||
|
if ($AllowMultipleSelections) {
|
||||||
|
$Selections = $UserInput.Split(',') | ForEach-Object { $_.Trim() }
|
||||||
|
$SelectedIndices = @()
|
||||||
|
foreach ($Selection in $Selections) {
|
||||||
|
if ($Selection -match '^[A-Z]$') {
|
||||||
|
$Index = [int][char]$Selection - [int][char]'A'
|
||||||
|
if ($Index -ge 0 -and $Index -lt $Options.Length) {
|
||||||
|
$SelectedIndices += $Index
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log "Invalid input: $Selection. Please enter letters within the valid range of options." -Level 'ERROR'
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log "Invalid input: $Selection. Please enter a letter between A and Z." -Level 'ERROR'
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $SelectedIndices
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ($UserInput -match '^[A-Z]$') {
|
||||||
|
$SelectedIndex = [int][char]$UserInput - [int][char]'A'
|
||||||
|
if ($SelectedIndex -ge 0 -and $SelectedIndex -lt $Options.Length) {
|
||||||
|
return $SelectedIndex
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log "Invalid input: $UserInput. Please enter a letter within the valid range of options." -Level 'ERROR'
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log "Invalid input: $UserInput. Please enter a letter between A and Z." -Level 'ERROR'
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Exit-Script {
|
||||||
|
param (
|
||||||
|
[int]$ExitCode,
|
||||||
|
[bool]$WaitForKeypress = $true
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($ExitCode -ne 0) {
|
||||||
|
Write-Log "Script failed. Would you like to open the log file? (Y/N)" -Level 'PROMPT'
|
||||||
|
$openLog = Read-Host
|
||||||
|
if ($openLog -eq 'Y' -or $openLog -eq 'y') {
|
||||||
|
Start-Process notepad.exe -ArgumentList $LogFilePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($WaitForKeypress) {
|
||||||
|
Write-Log "Press any key to exit..."
|
||||||
|
$host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ExitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-PythonExecutable {
|
||||||
|
param(
|
||||||
|
[string]$PythonExecutable
|
||||||
|
)
|
||||||
|
|
||||||
|
$DeactivateVenv = Join-Path $VenvDir "Scripts\Deactivate.bat"
|
||||||
|
if (Test-Path $DeactivateVenv) {
|
||||||
|
Write-Host "Deactivating virtual environment..." 2>&1 | Out-File $LogFilePath -Append
|
||||||
|
& $DeactivateVenv
|
||||||
|
Write-Host "Virtual environment deactivated." 2>&1 | Out-File $LogFilePath -Append
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "Deactivation script not found: $DeactivateVenv" 2>&1 | Out-File $LogFilePath -Append
|
||||||
|
}
|
||||||
|
|
||||||
|
$PythonCmd = Get-Command $PythonExecutable -ErrorAction SilentlyContinue
|
||||||
|
if ($PythonCmd) {
|
||||||
|
$VersionOutput = & $PythonCmd.Source --version 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
$Version = $VersionOutput | Select-String -Pattern "Python (\d+\.\d+\.\d+)" | ForEach-Object { $_.Matches.Groups[1].Value }
|
||||||
|
Write-Log "Python version $Version found using executable '$PythonExecutable'."
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log "Python executable '$PythonExecutable' not working correctly." -Level 'ERROR'
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log "Python executable '$PythonExecutable' not found." -Level 'ERROR'
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Find-PythonExecutable {
|
||||||
|
$PythonExecutables = @(
|
||||||
|
"python",
|
||||||
|
"python3.12",
|
||||||
|
"python3.11",
|
||||||
|
"python3.10",
|
||||||
|
"python3.9",
|
||||||
|
"python3",
|
||||||
|
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python312\python.exe",
|
||||||
|
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python311\python.exe",
|
||||||
|
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python310\python.exe",
|
||||||
|
"C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python39\python.exe",
|
||||||
|
"C:\Python312\python.exe",
|
||||||
|
"C:\Python311\python.exe",
|
||||||
|
"C:\Python310\python.exe",
|
||||||
|
"C:\Python39\python.exe"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
foreach ($Executable in $PythonExecutables) {
|
||||||
|
if (Test-PythonExecutable -PythonExecutable $Executable) {
|
||||||
|
return $Executable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
function Main {
|
||||||
|
"Starting the operations..." | Out-File $LogFilePath -Append
|
||||||
|
"Current directory: $(Get-Location)" | Out-File $LogFilePath -Append
|
||||||
|
|
||||||
|
# Exit on lower versions than Python 3.9 or when Python executable not found
|
||||||
|
$PythonExecutable = Find-PythonExecutable
|
||||||
|
if ($null -eq $PythonExecutable) {
|
||||||
|
Write-Log "No suitable Python executable found. Please ensure that Python 3.9 or higher is installed and available in the system PATH." -Level 'ERROR'
|
||||||
|
Exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Define the path to the Python executable in the virtual environment
|
||||||
|
$ActivateVenv = "$VenvDir\Scripts\Activate.ps1"
|
||||||
|
|
||||||
|
# Check if the virtual environment exists, if not, create it
|
||||||
|
if (-Not (Test-Path $ActivateVenv)) {
|
||||||
|
Write-Log "Virtual environment not found. Creating virtual environment..." -Level 'ERROR'
|
||||||
|
& $PythonExecutable -m venv $VenvName 2>&1 | Out-File $LogFilePath -Append
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Log "Failed to create virtual environment." -Level 'ERROR'
|
||||||
|
Exit-Script -exitCode 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log "Virtual environment created."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Activate the virtual environment and check if it was successful
|
||||||
|
Write-Log "Virtual environment found. Activating virtual environment..."
|
||||||
|
& $ActivateVenv 2>&1 | Out-File $LogFilePath -Append
|
||||||
|
# Check if virtual environment is activated
|
||||||
|
if ($env:VIRTUAL_ENV) {
|
||||||
|
Write-Log "Virtual environment is activated at: $($env:VIRTUAL_ENV)"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log "Failed to activate virtual environment." -Level 'ERROR'
|
||||||
|
Exit-Script -exitCode 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure pip
|
||||||
|
python -m ensurepip --default-pip 2>&1 | Out-File $LogFilePath -Append
|
||||||
|
|
||||||
|
# Pull latest updates only if the repository state is not dirty
|
||||||
|
Write-Log "Checking if the repository is clean..."
|
||||||
|
$Status = & "git" status --porcelain
|
||||||
|
if ($Status) {
|
||||||
|
Write-Log "Changes in local git repository. Skipping git pull."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log "Pulling latest updates..."
|
||||||
|
& "git" pull 2>&1 | Out-File $LogFilePath -Append
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Log "Failed to pull updates from Git." -Level 'ERROR'
|
||||||
|
Exit-Script -exitCode 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path "$VenvDir\Lib\site-packages\talib")) {
|
||||||
|
# Install TA-Lib using the virtual environment's pip
|
||||||
|
Write-Log "Installing TA-Lib using virtual environment's pip..."
|
||||||
|
python -m pip install --find-links=build_helpers\ --prefer-binary TA-Lib 2>&1 | Out-File $LogFilePath -Append
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Log "Failed to install TA-Lib." -Level 'ERROR'
|
||||||
|
Exit-Script -exitCode 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Present options for requirement files
|
||||||
|
$SelectedIndices = Get-UserSelection -prompt "Select which requirement files to install:" -options $RequirementFiles -defaultChoice 'A'
|
||||||
|
|
||||||
|
# Cache the selected requirement files
|
||||||
|
$SelectedRequirementFiles = @()
|
||||||
|
$PipInstallArguments = @()
|
||||||
|
foreach ($Index in $SelectedIndices) {
|
||||||
|
$RelativePath = $RequirementFiles[$Index]
|
||||||
|
if (Test-Path $RelativePath) {
|
||||||
|
$SelectedRequirementFiles += $RelativePath
|
||||||
|
$PipInstallArguments += "-r", $RelativePath # Add each flag and path as separate elements
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log "Requirement file not found: $RelativePath" -Level 'ERROR'
|
||||||
|
Exit-Script -exitCode 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($PipInstallArguments.Count -ne 0) {
|
||||||
|
& pip install @PipInstallArguments # Use array splatting to pass arguments correctly
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install freqtrade from setup using the virtual environment's Python
|
||||||
|
Write-Log "Installing freqtrade from setup..."
|
||||||
|
pip install -e . 2>&1 | Out-File $LogFilePath -Append
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Log "Failed to install freqtrade." -Level 'ERROR'
|
||||||
|
Exit-Script -exitCode 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log "Installing freqUI..."
|
||||||
|
python freqtrade install-ui 2>&1 | Out-File $LogFilePath -Append
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Log "Failed to install freqUI." -Level 'ERROR'
|
||||||
|
Exit-Script -exitCode 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log "Installation/Update complete!"
|
||||||
|
Exit-Script -exitCode 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call the Main function
|
||||||
|
Main
|
||||||
+1
-1
@@ -238,7 +238,6 @@ def patched_configuration_load_config_file(mocker, config) -> None:
|
|||||||
def patch_exchange(
|
def patch_exchange(
|
||||||
mocker, api_mock=None, id="binance", mock_markets=True, mock_supported_modes=True
|
mocker, api_mock=None, id="binance", mock_markets=True, mock_supported_modes=True
|
||||||
) -> None:
|
) -> None:
|
||||||
mocker.patch(f"{EXMS}._load_async_markets", return_value={})
|
|
||||||
mocker.patch(f"{EXMS}.validate_config", MagicMock())
|
mocker.patch(f"{EXMS}.validate_config", MagicMock())
|
||||||
mocker.patch(f"{EXMS}.validate_timeframes", MagicMock())
|
mocker.patch(f"{EXMS}.validate_timeframes", MagicMock())
|
||||||
mocker.patch(f"{EXMS}.id", PropertyMock(return_value=id))
|
mocker.patch(f"{EXMS}.id", PropertyMock(return_value=id))
|
||||||
@@ -248,6 +247,7 @@ def patch_exchange(
|
|||||||
mocker.patch("freqtrade.exchange.bybit.Bybit.cache_leverage_tiers")
|
mocker.patch("freqtrade.exchange.bybit.Bybit.cache_leverage_tiers")
|
||||||
|
|
||||||
if mock_markets:
|
if mock_markets:
|
||||||
|
mocker.patch(f"{EXMS}._load_async_markets", return_value={})
|
||||||
if isinstance(mock_markets, bool):
|
if isinstance(mock_markets, bool):
|
||||||
mock_markets = get_markets()
|
mock_markets = get_markets()
|
||||||
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=mock_markets))
|
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=mock_markets))
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ def test_remove_exchange_credentials(default_conf) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
||||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
mocker.patch(f"{EXMS}.reload_markets")
|
||||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||||
aei_mock = mocker.patch(f"{EXMS}.additional_exchange_init")
|
aei_mock = mocker.patch(f"{EXMS}.additional_exchange_init")
|
||||||
|
|
||||||
@@ -518,7 +518,7 @@ def test__load_async_markets(default_conf, mocker, caplog):
|
|||||||
mocker.patch(f"{EXMS}._init_ccxt")
|
mocker.patch(f"{EXMS}._init_ccxt")
|
||||||
mocker.patch(f"{EXMS}.validate_pairs")
|
mocker.patch(f"{EXMS}.validate_pairs")
|
||||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||||
mocker.patch(f"{EXMS}._load_markets")
|
mocker.patch(f"{EXMS}.reload_markets")
|
||||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||||
mocker.patch(f"{EXMS}.validate_pricing")
|
mocker.patch(f"{EXMS}.validate_pricing")
|
||||||
exchange = Exchange(default_conf)
|
exchange = Exchange(default_conf)
|
||||||
@@ -527,28 +527,26 @@ def test__load_async_markets(default_conf, mocker, caplog):
|
|||||||
assert exchange._api_async.load_markets.call_count == 1
|
assert exchange._api_async.load_markets.call_count == 1
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
|
|
||||||
exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef"))
|
exchange._api_async.load_markets = get_mock_coro(side_effect=ccxt.BaseError("deadbeef"))
|
||||||
exchange._load_async_markets()
|
with pytest.raises(ccxt.BaseError, match="deadbeef"):
|
||||||
|
exchange._load_async_markets()
|
||||||
assert log_has("Could not load async markets. Reason: deadbeef", caplog)
|
|
||||||
|
|
||||||
|
|
||||||
def test__load_markets(default_conf, mocker, caplog):
|
def test__load_markets(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError"))
|
api_mock.load_markets = get_mock_coro(side_effect=ccxt.BaseError("SomeError"))
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}.validate_pairs")
|
mocker.patch(f"{EXMS}.validate_pairs")
|
||||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||||
mocker.patch(f"{EXMS}._load_async_markets")
|
|
||||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||||
mocker.patch(f"{EXMS}.validate_pricing")
|
mocker.patch(f"{EXMS}.validate_pricing")
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
assert log_has("Unable to initialize markets.", caplog)
|
assert log_has("Could not load markets.", caplog)
|
||||||
|
|
||||||
expected_return = {"ETH/BTC": "available"}
|
expected_return = {"ETH/BTC": "available"}
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.load_markets = MagicMock(return_value=expected_return)
|
api_mock.load_markets = get_mock_coro(return_value=expected_return)
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
default_conf["exchange"]["pair_whitelist"] = ["ETH/BTC"]
|
default_conf["exchange"]["pair_whitelist"] = ["ETH/BTC"]
|
||||||
ex = Exchange(default_conf)
|
ex = Exchange(default_conf)
|
||||||
@@ -563,12 +561,12 @@ def test_reload_markets(default_conf, mocker, caplog, time_machine):
|
|||||||
start_dt = dt_now()
|
start_dt = dt_now()
|
||||||
time_machine.move_to(start_dt, tick=False)
|
time_machine.move_to(start_dt, tick=False)
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.load_markets = MagicMock(return_value=initial_markets)
|
api_mock.load_markets = get_mock_coro(return_value=initial_markets)
|
||||||
default_conf["exchange"]["markets_refresh_interval"] = 10
|
default_conf["exchange"]["markets_refresh_interval"] = 10
|
||||||
exchange = get_patched_exchange(
|
exchange = get_patched_exchange(
|
||||||
mocker, default_conf, api_mock, id="binance", mock_markets=False
|
mocker, default_conf, api_mock, id="binance", mock_markets=False
|
||||||
)
|
)
|
||||||
exchange._load_async_markets = MagicMock()
|
lam_spy = mocker.spy(exchange, "_load_async_markets")
|
||||||
assert exchange._last_markets_refresh == dt_ts()
|
assert exchange._last_markets_refresh == dt_ts()
|
||||||
|
|
||||||
assert exchange.markets == initial_markets
|
assert exchange.markets == initial_markets
|
||||||
@@ -577,42 +575,45 @@ def test_reload_markets(default_conf, mocker, caplog, time_machine):
|
|||||||
# less than 10 minutes have passed, no reload
|
# less than 10 minutes have passed, no reload
|
||||||
exchange.reload_markets()
|
exchange.reload_markets()
|
||||||
assert exchange.markets == initial_markets
|
assert exchange.markets == initial_markets
|
||||||
assert exchange._load_async_markets.call_count == 0
|
assert lam_spy.call_count == 0
|
||||||
|
|
||||||
api_mock.load_markets = MagicMock(return_value=updated_markets)
|
api_mock.load_markets = get_mock_coro(return_value=updated_markets)
|
||||||
# more than 10 minutes have passed, reload is executed
|
# more than 10 minutes have passed, reload is executed
|
||||||
time_machine.move_to(start_dt + timedelta(minutes=11), tick=False)
|
time_machine.move_to(start_dt + timedelta(minutes=11), tick=False)
|
||||||
exchange.reload_markets()
|
exchange.reload_markets()
|
||||||
assert exchange.markets == updated_markets
|
assert exchange.markets == updated_markets
|
||||||
assert exchange._load_async_markets.call_count == 1
|
assert lam_spy.call_count == 1
|
||||||
assert log_has("Performing scheduled market reload..", caplog)
|
assert log_has("Performing scheduled market reload..", caplog)
|
||||||
|
|
||||||
# Not called again
|
# Not called again
|
||||||
exchange._load_async_markets.reset_mock()
|
lam_spy.reset_mock()
|
||||||
|
|
||||||
exchange.reload_markets()
|
exchange.reload_markets()
|
||||||
assert exchange._load_async_markets.call_count == 0
|
assert lam_spy.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_reload_markets_exception(default_conf, mocker, caplog):
|
def test_reload_markets_exception(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
|
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError("LoadError"))
|
api_mock.load_markets = get_mock_coro(side_effect=ccxt.NetworkError("LoadError"))
|
||||||
default_conf["exchange"]["markets_refresh_interval"] = 10
|
default_conf["exchange"]["markets_refresh_interval"] = 10
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
|
exchange = get_patched_exchange(
|
||||||
|
mocker, default_conf, api_mock, id="binance", mock_markets=False
|
||||||
|
)
|
||||||
|
|
||||||
|
exchange._last_markets_refresh = 2
|
||||||
# less than 10 minutes have passed, no reload
|
# less than 10 minutes have passed, no reload
|
||||||
exchange.reload_markets()
|
exchange.reload_markets()
|
||||||
assert exchange._last_markets_refresh == 0
|
assert exchange._last_markets_refresh == 2
|
||||||
assert log_has_re(r"Could not reload markets.*", caplog)
|
assert log_has_re(r"Could not load markets\..*", caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("stake_currency", ["ETH", "BTC", "USDT"])
|
@pytest.mark.parametrize("stake_currency", ["ETH", "BTC", "USDT"])
|
||||||
def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
|
def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
|
||||||
default_conf["stake_currency"] = stake_currency
|
default_conf["stake_currency"] = stake_currency
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
type(api_mock).load_markets = MagicMock(
|
type(api_mock).load_markets = get_mock_coro(
|
||||||
return_value={
|
return_value={
|
||||||
"ETH/BTC": {"quote": "BTC"},
|
"ETH/BTC": {"quote": "BTC"},
|
||||||
"LTC/BTC": {"quote": "BTC"},
|
"LTC/BTC": {"quote": "BTC"},
|
||||||
@@ -623,7 +624,6 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
|
|||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}.validate_pairs")
|
mocker.patch(f"{EXMS}.validate_pairs")
|
||||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||||
mocker.patch(f"{EXMS}._load_async_markets")
|
|
||||||
mocker.patch(f"{EXMS}.validate_pricing")
|
mocker.patch(f"{EXMS}.validate_pricing")
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
@@ -631,7 +631,7 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
|
|||||||
def test_validate_stakecurrency_error(default_conf, mocker, caplog):
|
def test_validate_stakecurrency_error(default_conf, mocker, caplog):
|
||||||
default_conf["stake_currency"] = "XRP"
|
default_conf["stake_currency"] = "XRP"
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
type(api_mock).load_markets = MagicMock(
|
type(api_mock).load_markets = get_mock_coro(
|
||||||
return_value={
|
return_value={
|
||||||
"ETH/BTC": {"quote": "BTC"},
|
"ETH/BTC": {"quote": "BTC"},
|
||||||
"LTC/BTC": {"quote": "BTC"},
|
"LTC/BTC": {"quote": "BTC"},
|
||||||
@@ -642,14 +642,13 @@ def test_validate_stakecurrency_error(default_conf, mocker, caplog):
|
|||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}.validate_pairs")
|
mocker.patch(f"{EXMS}.validate_pairs")
|
||||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||||
mocker.patch(f"{EXMS}._load_async_markets")
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
ConfigurationError,
|
ConfigurationError,
|
||||||
match=r"XRP is not available as stake on .*Available currencies are: BTC, ETH, USDT",
|
match=r"XRP is not available as stake on .*Available currencies are: BTC, ETH, USDT",
|
||||||
):
|
):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
type(api_mock).load_markets = MagicMock(side_effect=ccxt.NetworkError("No connection."))
|
type(api_mock).load_markets = get_mock_coro(side_effect=ccxt.NetworkError("No connection."))
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
@@ -694,24 +693,26 @@ def test_get_pair_base_currency(default_conf, mocker, pair, expected):
|
|||||||
assert ex.get_pair_base_currency(pair) == expected
|
assert ex.get_pair_base_currency(pair) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly
|
def test_validate_pairs(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
type(api_mock).load_markets = MagicMock(
|
|
||||||
return_value={
|
|
||||||
"ETH/BTC": {"quote": "BTC"},
|
|
||||||
"LTC/BTC": {"quote": "BTC"},
|
|
||||||
"XRP/BTC": {"quote": "BTC"},
|
|
||||||
"NEO/BTC": {"quote": "BTC"},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
id_mock = PropertyMock(return_value="test_exchange")
|
id_mock = PropertyMock(return_value="test_exchange")
|
||||||
type(api_mock).id = id_mock
|
type(api_mock).id = id_mock
|
||||||
|
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||||
mocker.patch(f"{EXMS}._load_async_markets")
|
mocker.patch(
|
||||||
|
f"{EXMS}._load_async_markets",
|
||||||
|
return_value={
|
||||||
|
"ETH/BTC": {"quote": "BTC"},
|
||||||
|
"LTC/BTC": {"quote": "BTC"},
|
||||||
|
"XRP/BTC": {"quote": "BTC"},
|
||||||
|
"NEO/BTC": {"quote": "BTC"},
|
||||||
|
},
|
||||||
|
)
|
||||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||||
mocker.patch(f"{EXMS}.validate_pricing")
|
mocker.patch(f"{EXMS}.validate_pricing")
|
||||||
|
# test exchange.validate_pairs directly
|
||||||
|
# No assert - but this should not fail (!)
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
@@ -751,7 +752,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
|
|||||||
|
|
||||||
def test_validate_pairs_restricted(default_conf, mocker, caplog):
|
def test_validate_pairs_restricted(default_conf, mocker, caplog):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
type(api_mock).load_markets = MagicMock(
|
type(api_mock).load_markets = get_mock_coro(
|
||||||
return_value={
|
return_value={
|
||||||
"ETH/BTC": {"quote": "BTC"},
|
"ETH/BTC": {"quote": "BTC"},
|
||||||
"LTC/BTC": {"quote": "BTC"},
|
"LTC/BTC": {"quote": "BTC"},
|
||||||
@@ -761,7 +762,6 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
|
|||||||
)
|
)
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||||
mocker.patch(f"{EXMS}._load_async_markets")
|
|
||||||
mocker.patch(f"{EXMS}.validate_pricing")
|
mocker.patch(f"{EXMS}.validate_pricing")
|
||||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||||
|
|
||||||
@@ -774,9 +774,9 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
|
def test_validate_pairs_stakecompatibility(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
type(api_mock).load_markets = MagicMock(
|
type(api_mock).load_markets = get_mock_coro(
|
||||||
return_value={
|
return_value={
|
||||||
"ETH/BTC": {"quote": "BTC"},
|
"ETH/BTC": {"quote": "BTC"},
|
||||||
"LTC/BTC": {"quote": "BTC"},
|
"LTC/BTC": {"quote": "BTC"},
|
||||||
@@ -787,17 +787,16 @@ def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
|
|||||||
)
|
)
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||||
mocker.patch(f"{EXMS}._load_async_markets")
|
|
||||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||||
mocker.patch(f"{EXMS}.validate_pricing")
|
mocker.patch(f"{EXMS}.validate_pricing")
|
||||||
|
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, caplog):
|
def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
default_conf["stake_currency"] = ""
|
default_conf["stake_currency"] = ""
|
||||||
type(api_mock).load_markets = MagicMock(
|
type(api_mock).load_markets = get_mock_coro(
|
||||||
return_value={
|
return_value={
|
||||||
"ETH/BTC": {"quote": "BTC"},
|
"ETH/BTC": {"quote": "BTC"},
|
||||||
"LTC/BTC": {"quote": "BTC"},
|
"LTC/BTC": {"quote": "BTC"},
|
||||||
@@ -808,7 +807,6 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca
|
|||||||
)
|
)
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||||
mocker.patch(f"{EXMS}._load_async_markets")
|
|
||||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||||
mocker.patch(f"{EXMS}.validate_pricing")
|
mocker.patch(f"{EXMS}.validate_pricing")
|
||||||
|
|
||||||
@@ -816,10 +814,10 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca
|
|||||||
assert type(api_mock).load_markets.call_count == 1
|
assert type(api_mock).load_markets.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog):
|
def test_validate_pairs_stakecompatibility_fail(default_conf, mocker):
|
||||||
default_conf["exchange"]["pair_whitelist"].append("HELLO-WORLD")
|
default_conf["exchange"]["pair_whitelist"].append("HELLO-WORLD")
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
type(api_mock).load_markets = MagicMock(
|
type(api_mock).load_markets = get_mock_coro(
|
||||||
return_value={
|
return_value={
|
||||||
"ETH/BTC": {"quote": "BTC"},
|
"ETH/BTC": {"quote": "BTC"},
|
||||||
"LTC/BTC": {"quote": "BTC"},
|
"LTC/BTC": {"quote": "BTC"},
|
||||||
@@ -830,7 +828,6 @@ def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog):
|
|||||||
)
|
)
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||||
mocker.patch(f"{EXMS}._load_async_markets")
|
|
||||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r"Stake-currency 'BTC' not compatible with.*"):
|
with pytest.raises(OperationalException, match=r"Stake-currency 'BTC' not compatible with.*"):
|
||||||
@@ -847,7 +844,7 @@ def test_validate_timeframes(default_conf, mocker, timeframe):
|
|||||||
type(api_mock).timeframes = timeframes
|
type(api_mock).timeframes = timeframes
|
||||||
|
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
mocker.patch(f"{EXMS}.reload_markets")
|
||||||
mocker.patch(f"{EXMS}.validate_pairs")
|
mocker.patch(f"{EXMS}.validate_pairs")
|
||||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||||
mocker.patch(f"{EXMS}.validate_pricing")
|
mocker.patch(f"{EXMS}.validate_pricing")
|
||||||
@@ -865,7 +862,7 @@ def test_validate_timeframes_failed(default_conf, mocker):
|
|||||||
type(api_mock).timeframes = timeframes
|
type(api_mock).timeframes = timeframes
|
||||||
|
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
mocker.patch(f"{EXMS}.reload_markets")
|
||||||
mocker.patch(f"{EXMS}.validate_pairs")
|
mocker.patch(f"{EXMS}.validate_pairs")
|
||||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||||
mocker.patch(f"{EXMS}.validate_pricing")
|
mocker.patch(f"{EXMS}.validate_pricing")
|
||||||
@@ -895,7 +892,7 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
|
|||||||
del api_mock.timeframes
|
del api_mock.timeframes
|
||||||
|
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
mocker.patch(f"{EXMS}.reload_markets")
|
||||||
mocker.patch(f"{EXMS}.validate_pairs")
|
mocker.patch(f"{EXMS}.validate_pairs")
|
||||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
@@ -917,7 +914,7 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
|
|||||||
del api_mock.timeframes
|
del api_mock.timeframes
|
||||||
|
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={"timeframes": None}))
|
mocker.patch(f"{EXMS}.reload_markets")
|
||||||
mocker.patch(f"{EXMS}.validate_pairs", MagicMock())
|
mocker.patch(f"{EXMS}.validate_pairs", MagicMock())
|
||||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
@@ -939,7 +936,7 @@ def test_validate_timeframes_not_in_config(default_conf, mocker):
|
|||||||
type(api_mock).timeframes = timeframes
|
type(api_mock).timeframes = timeframes
|
||||||
|
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
mocker.patch(f"{EXMS}.reload_markets")
|
||||||
mocker.patch(f"{EXMS}.validate_pairs")
|
mocker.patch(f"{EXMS}.validate_pairs")
|
||||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||||
mocker.patch(f"{EXMS}.validate_pricing")
|
mocker.patch(f"{EXMS}.validate_pricing")
|
||||||
@@ -955,7 +952,7 @@ def test_validate_pricing(default_conf, mocker):
|
|||||||
}
|
}
|
||||||
type(api_mock).has = PropertyMock(return_value=has)
|
type(api_mock).has = PropertyMock(return_value=has)
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
mocker.patch(f"{EXMS}.reload_markets")
|
||||||
mocker.patch(f"{EXMS}.validate_trading_mode_and_margin_mode")
|
mocker.patch(f"{EXMS}.validate_trading_mode_and_margin_mode")
|
||||||
mocker.patch(f"{EXMS}.validate_pairs")
|
mocker.patch(f"{EXMS}.validate_pairs")
|
||||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||||
@@ -991,7 +988,7 @@ def test_validate_ordertypes(default_conf, mocker):
|
|||||||
|
|
||||||
type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True})
|
type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True})
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
mocker.patch(f"{EXMS}.reload_markets")
|
||||||
mocker.patch(f"{EXMS}.validate_pairs")
|
mocker.patch(f"{EXMS}.validate_pairs")
|
||||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||||
@@ -1050,7 +1047,7 @@ def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name,
|
|||||||
default_conf["margin_mode"] = MarginMode.ISOLATED
|
default_conf["margin_mode"] = MarginMode.ISOLATED
|
||||||
type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True})
|
type(api_mock).has = PropertyMock(return_value={"createMarketOrder": True})
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
mocker.patch(f"{EXMS}.reload_markets")
|
||||||
mocker.patch(f"{EXMS}.validate_pairs")
|
mocker.patch(f"{EXMS}.validate_pairs")
|
||||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||||
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
mocker.patch(f"{EXMS}.validate_stakecurrency")
|
||||||
@@ -1075,7 +1072,7 @@ def test_validate_ordertypes_stop_advanced(default_conf, mocker, exchange_name,
|
|||||||
def test_validate_order_types_not_in_config(default_conf, mocker):
|
def test_validate_order_types_not_in_config(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
mocker.patch(f"{EXMS}._init_ccxt", MagicMock(return_value=api_mock))
|
||||||
mocker.patch(f"{EXMS}._load_markets", MagicMock(return_value={}))
|
mocker.patch(f"{EXMS}.reload_markets")
|
||||||
mocker.patch(f"{EXMS}.validate_pairs")
|
mocker.patch(f"{EXMS}.validate_pairs")
|
||||||
mocker.patch(f"{EXMS}.validate_timeframes")
|
mocker.patch(f"{EXMS}.validate_timeframes")
|
||||||
mocker.patch(f"{EXMS}.validate_pricing")
|
mocker.patch(f"{EXMS}.validate_pricing")
|
||||||
@@ -1947,7 +1944,9 @@ def test_fetch_trading_fees(default_conf, mocker):
|
|||||||
assert api_mock.fetch_trading_fees.call_count == 1
|
assert api_mock.fetch_trading_fees.call_count == 1
|
||||||
|
|
||||||
api_mock.fetch_trading_fees.reset_mock()
|
api_mock.fetch_trading_fees.reset_mock()
|
||||||
|
# Reload-markets calls fetch_trading_fees, too - so the explicit calls in the below
|
||||||
|
# exception test would be called twice.
|
||||||
|
mocker.patch(f"{EXMS}.reload_markets")
|
||||||
ccxt_exceptionhandlers(
|
ccxt_exceptionhandlers(
|
||||||
mocker, default_conf, api_mock, exchange_name, "fetch_trading_fees", "fetch_trading_fees"
|
mocker, default_conf, api_mock, exchange_name, "fetch_trading_fees", "fetch_trading_fees"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -699,18 +699,20 @@ def test_process_trade_creation(
|
|||||||
|
|
||||||
|
|
||||||
def test_process_exchange_failures(default_conf_usdt, ticker_usdt, mocker) -> None:
|
def test_process_exchange_failures(default_conf_usdt, ticker_usdt, mocker) -> None:
|
||||||
|
# TODO: Move this test to test_worker
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
EXMS,
|
EXMS,
|
||||||
fetch_ticker=ticker_usdt,
|
fetch_ticker=ticker_usdt,
|
||||||
reload_markets=MagicMock(side_effect=TemporaryError),
|
reload_markets=MagicMock(),
|
||||||
create_order=MagicMock(side_effect=TemporaryError),
|
create_order=MagicMock(side_effect=TemporaryError),
|
||||||
)
|
)
|
||||||
sleep_mock = mocker.patch("time.sleep")
|
sleep_mock = mocker.patch("time.sleep")
|
||||||
|
|
||||||
worker = Worker(args=None, config=default_conf_usdt)
|
worker = Worker(args=None, config=default_conf_usdt)
|
||||||
patch_get_signal(worker.freqtrade)
|
patch_get_signal(worker.freqtrade)
|
||||||
|
mocker.patch(f"{EXMS}.reload_markets", MagicMock(side_effect=TemporaryError))
|
||||||
|
|
||||||
worker._process_running()
|
worker._process_running()
|
||||||
assert sleep_mock.called is True
|
assert sleep_mock.called is True
|
||||||
|
|||||||
@@ -1063,7 +1063,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmp_path, fee) -> None
|
|||||||
def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmp_path, fee) -> None:
|
def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmp_path, fee) -> None:
|
||||||
mocker.patch(f"{EXMS}.validate_config", MagicMock())
|
mocker.patch(f"{EXMS}.validate_config", MagicMock())
|
||||||
mocker.patch(f"{EXMS}.get_fee", fee)
|
mocker.patch(f"{EXMS}.get_fee", fee)
|
||||||
mocker.patch(f"{EXMS}._load_markets")
|
mocker.patch(f"{EXMS}.reload_markets")
|
||||||
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=get_markets()))
|
mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=get_markets()))
|
||||||
(tmp_path / "hyperopt_results").mkdir(parents=True)
|
(tmp_path / "hyperopt_results").mkdir(parents=True)
|
||||||
# Dummy-reduce points to ensure scikit-learn is forced to generate new values
|
# Dummy-reduce points to ensure scikit-learn is forced to generate new values
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
|
||||||
|
Describe "Setup and Tests" {
|
||||||
|
BeforeAll {
|
||||||
|
# Setup variables
|
||||||
|
$SetupScriptPath = Join-Path $PSScriptRoot "..\setup.ps1"
|
||||||
|
$Global:LogFilePath = Join-Path $env:TEMP "script_log.txt"
|
||||||
|
|
||||||
|
# Check if the setup script exists
|
||||||
|
if (-Not (Test-Path -Path $SetupScriptPath)) {
|
||||||
|
Write-Host "Error: setup.ps1 script not found at path: $SetupScriptPath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mock main to prevent it from running
|
||||||
|
Mock Main {}
|
||||||
|
|
||||||
|
. $SetupScriptPath
|
||||||
|
}
|
||||||
|
|
||||||
|
Context "Write-Log Tests" -Tag "Unit" {
|
||||||
|
It "should write INFO level log" {
|
||||||
|
if (Test-Path $Global:LogFilePath){
|
||||||
|
Remove-Item $Global:LogFilePath -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Message "Test Info Message" -Level "INFO"
|
||||||
|
$Global:LogFilePath | Should -Exist
|
||||||
|
|
||||||
|
$LogContent = Get-Content $Global:LogFilePath
|
||||||
|
$LogContent | Should -Contain "INFO: Test Info Message"
|
||||||
|
}
|
||||||
|
|
||||||
|
It "should write ERROR level log" {
|
||||||
|
if (Test-Path $Global:LogFilePath){
|
||||||
|
Remove-Item $Global:LogFilePath -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log -Message "Test Error Message" -Level "ERROR"
|
||||||
|
$Global:LogFilePath | Should -Exist
|
||||||
|
|
||||||
|
$LogContent = Get-Content $Global:LogFilePath
|
||||||
|
$LogContent | Should -Contain "ERROR: Test Error Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe "Get-UserSelection Tests" {
|
||||||
|
Context "Valid input" {
|
||||||
|
It "Should return the correct index for a valid single selection" {
|
||||||
|
$Options = @("Option1", "Option2", "Option3")
|
||||||
|
Mock Read-Host { return "B" }
|
||||||
|
$Result = Get-UserSelection -prompt "Select an option" -options $Options
|
||||||
|
$Result | Should -Be 1
|
||||||
|
}
|
||||||
|
|
||||||
|
It "Should return the correct index for a valid single selection" {
|
||||||
|
$Options = @("Option1", "Option2", "Option3")
|
||||||
|
Mock Read-Host { return "b" }
|
||||||
|
$Result = Get-UserSelection -prompt "Select an option" -options $Options
|
||||||
|
$Result | Should -Be 1
|
||||||
|
}
|
||||||
|
|
||||||
|
It "Should return the default choice when no input is provided" {
|
||||||
|
$Options = @("Option1", "Option2", "Option3")
|
||||||
|
Mock Read-Host { return "" }
|
||||||
|
$Result = Get-UserSelection -prompt "Select an option" -options $Options -defaultChoice "C"
|
||||||
|
$Result | Should -Be 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Context "Invalid input" {
|
||||||
|
It "Should return -1 for an invalid letter selection" {
|
||||||
|
$Options = @("Option1", "Option2", "Option3")
|
||||||
|
Mock Read-Host { return "X" }
|
||||||
|
$Result = Get-UserSelection -prompt "Select an option" -options $Options
|
||||||
|
$Result | Should -Be -1
|
||||||
|
}
|
||||||
|
|
||||||
|
It "Should return -1 for a selection outside the valid range" {
|
||||||
|
$Options = @("Option1", "Option2", "Option3")
|
||||||
|
Mock Read-Host { return "D" }
|
||||||
|
$Result = Get-UserSelection -prompt "Select an option" -options $Options
|
||||||
|
$Result | Should -Be -1
|
||||||
|
}
|
||||||
|
|
||||||
|
It "Should return -1 for a non-letter input" {
|
||||||
|
$Options = @("Option1", "Option2", "Option3")
|
||||||
|
Mock Read-Host { return "1" }
|
||||||
|
$Result = Get-UserSelection -prompt "Select an option" -options $Options
|
||||||
|
$Result | Should -Be -1
|
||||||
|
}
|
||||||
|
|
||||||
|
It "Should return -1 for mixed valid and invalid input" {
|
||||||
|
Mock Read-Host { return "A,X,B,Y,C,Z" }
|
||||||
|
$Options = @("Option1", "Option2", "Option3")
|
||||||
|
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A"
|
||||||
|
$Indices | Should -Be -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Context "Multiple selections" {
|
||||||
|
It "Should handle valid input correctly" {
|
||||||
|
Mock Read-Host { return "A, B, C" }
|
||||||
|
$Options = @("Option1", "Option2", "Option3")
|
||||||
|
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A"
|
||||||
|
$Indices | Should -Be @(0, 1, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
It "Should handle valid input without whitespace correctly" {
|
||||||
|
Mock Read-Host { return "A,B,C" }
|
||||||
|
$Options = @("Option1", "Option2", "Option3")
|
||||||
|
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A"
|
||||||
|
$Indices | Should -Be @(0, 1, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
It "Should return indices for selected options" {
|
||||||
|
Mock Read-Host { return "a,b" }
|
||||||
|
$Options = @("Option1", "Option2", "Option3")
|
||||||
|
$Indices = Get-UserSelection -prompt "Select options" -options $Options
|
||||||
|
$Indices | Should -Be @(0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
It "Should return default choice if no input" {
|
||||||
|
Mock Read-Host { return "" }
|
||||||
|
$Options = @("Option1", "Option2", "Option3")
|
||||||
|
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "C"
|
||||||
|
$Indices | Should -Be @(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
It "Should handle invalid input gracefully" {
|
||||||
|
Mock Read-Host { return "x,y,z" }
|
||||||
|
$Options = @("Option1", "Option2", "Option3")
|
||||||
|
$Indices = Get-UserSelection -prompt "Select options" -options $Options -defaultChoice "A"
|
||||||
|
$Indices | Should -Be -1
|
||||||
|
}
|
||||||
|
|
||||||
|
It "Should handle input without whitespace" {
|
||||||
|
Mock Read-Host { return "a,b,c" }
|
||||||
|
$Options = @("Option1", "Option2", "Option3")
|
||||||
|
$Indices = Get-UserSelection -prompt "Select options" -options $Options
|
||||||
|
$Indices | Should -Be @(0, 1, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe "Exit-Script Tests" -Tag "Unit" {
|
||||||
|
BeforeEach {
|
||||||
|
Mock Write-Log {}
|
||||||
|
Mock Start-Process {}
|
||||||
|
Mock Read-Host { return "Y" }
|
||||||
|
}
|
||||||
|
|
||||||
|
It "should exit with the given exit code without waiting for key press" {
|
||||||
|
$ExitCode = Exit-Script -ExitCode 0 -isSubShell $true -waitForKeypress $false
|
||||||
|
$ExitCode | Should -Be 0
|
||||||
|
}
|
||||||
|
|
||||||
|
It "should prompt to open log file on error" {
|
||||||
|
Exit-Script -ExitCode 1 -isSubShell $true -waitForKeypress $false
|
||||||
|
Assert-MockCalled Read-Host -Exactly 1
|
||||||
|
Assert-MockCalled Start-Process -Exactly 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Context 'Find-PythonExecutable' {
|
||||||
|
It 'Returns the first valid Python executable' {
|
||||||
|
Mock Test-PythonExecutable { $true } -ParameterFilter { $PythonExecutable -eq 'python' }
|
||||||
|
$Result = Find-PythonExecutable
|
||||||
|
$Result | Should -Be 'python'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Returns null if no valid Python executable is found' {
|
||||||
|
Mock Test-PythonExecutable { $false }
|
||||||
|
$Result = Find-PythonExecutable
|
||||||
|
$Result | Should -Be $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user