200 lines
5.9 KiB
Python
200 lines
5.9 KiB
Python
import os
|
|
import sys
|
|
import time
|
|
from typing import Optional
|
|
|
|
import pytest
|
|
import typer
|
|
from typing_extensions import Annotated
|
|
|
|
from deepeval.config.settings import get_settings
|
|
from deepeval.telemetry import capture_evaluation_run
|
|
from deepeval.test_run import (
|
|
TEMP_FILE_PATH,
|
|
global_test_run_manager,
|
|
invoke_test_run_end_hook,
|
|
)
|
|
from deepeval.test_run.cache import TEMP_CACHE_FILE_NAME
|
|
from deepeval.test_run.test_run import TestRunResultDisplay
|
|
from deepeval.utils import (
|
|
delete_file_if_exists,
|
|
set_identifier,
|
|
set_is_running_deepeval,
|
|
set_should_ignore_errors,
|
|
set_should_skip_on_missing_params,
|
|
set_should_use_cache,
|
|
set_test_run_official,
|
|
set_verbose_mode,
|
|
)
|
|
|
|
SETTINGS = get_settings()
|
|
app = typer.Typer(name="test")
|
|
|
|
|
|
def check_if_valid_file(test_file_or_directory: str):
|
|
if "::" in test_file_or_directory:
|
|
test_file_or_directory = test_file_or_directory.split("::", 1)[0]
|
|
if os.path.isfile(test_file_or_directory) or os.path.isdir(
|
|
test_file_or_directory
|
|
):
|
|
return
|
|
raise ValueError("Provided path is neither a valid file nor a directory.")
|
|
|
|
|
|
# Allow extra args and ignore unknown options allow extra args to be passed to pytest
|
|
@app.command(
|
|
context_settings={"allow_extra_args": True, "ignore_unknown_options": True}
|
|
)
|
|
def run(
|
|
ctx: typer.Context,
|
|
test_file_or_directory: str,
|
|
color: str = "yes",
|
|
durations: int = 10,
|
|
pdb: bool = False,
|
|
exit_on_first_failure: Annotated[
|
|
bool, typer.Option("--exit-on-first-failure", "-x/-X")
|
|
] = False,
|
|
show_warnings: Annotated[
|
|
bool, typer.Option("--show-warnings", "-w/-W")
|
|
] = False,
|
|
identifier: Optional[str] = typer.Option(
|
|
None,
|
|
"--identifier",
|
|
"-id",
|
|
help="Identify this test run with pytest",
|
|
),
|
|
num_processes: Optional[int] = typer.Option(
|
|
None,
|
|
"--num-processes",
|
|
"-n",
|
|
help="Number of processes to use with pytest",
|
|
),
|
|
repeat: Optional[int] = typer.Option(
|
|
None,
|
|
"--repeat",
|
|
"-r",
|
|
help="Number of times to rerun a test case",
|
|
),
|
|
use_cache: Optional[bool] = typer.Option(
|
|
False,
|
|
"--use-cache",
|
|
"-c",
|
|
help="Whether to use cached results or not",
|
|
),
|
|
ignore_errors: Optional[bool] = typer.Option(
|
|
False,
|
|
"--ignore-errors",
|
|
"-i",
|
|
help="Whether to ignore errors or not",
|
|
),
|
|
skip_on_missing_params: Optional[bool] = typer.Option(
|
|
False,
|
|
"--skip-on-missing-params",
|
|
"-s",
|
|
help="Whether to skip test cases with missing parameters",
|
|
),
|
|
verbose: Optional[bool] = typer.Option(
|
|
None,
|
|
"--verbose",
|
|
"-v",
|
|
help="Whether to turn on verbose mode for evaluation or not",
|
|
),
|
|
display: Optional[TestRunResultDisplay] = typer.Option(
|
|
TestRunResultDisplay.ALL.value,
|
|
"--display",
|
|
"-d",
|
|
help="Whether to display all test cases or just some in the end",
|
|
case_sensitive=False,
|
|
),
|
|
mark: Optional[str] = typer.Option(
|
|
None,
|
|
"--mark",
|
|
"-m",
|
|
help="List of marks to run the tests with.",
|
|
),
|
|
official: bool = typer.Option(
|
|
False,
|
|
"--official",
|
|
"-o",
|
|
help="Mark this test run as the official baseline on Confident AI.",
|
|
),
|
|
):
|
|
"""Run a test"""
|
|
delete_file_if_exists(TEMP_FILE_PATH)
|
|
delete_file_if_exists(TEMP_CACHE_FILE_NAME)
|
|
check_if_valid_file(test_file_or_directory)
|
|
set_is_running_deepeval(True)
|
|
|
|
if official and not SETTINGS.CONFIDENT_API_KEY:
|
|
print(
|
|
"Warning: --official requires a CONFIDENT_API_KEY environment variable to be set. Skipping."
|
|
)
|
|
official = False
|
|
|
|
should_use_cache = use_cache and repeat is None
|
|
set_should_use_cache(should_use_cache)
|
|
set_should_ignore_errors(ignore_errors)
|
|
set_should_skip_on_missing_params(skip_on_missing_params)
|
|
set_verbose_mode(verbose)
|
|
set_identifier(identifier)
|
|
set_test_run_official(official)
|
|
|
|
global_test_run_manager.reset()
|
|
|
|
pytest_args = [test_file_or_directory]
|
|
|
|
if exit_on_first_failure:
|
|
pytest_args.insert(0, "-x")
|
|
|
|
pytest_args.extend(
|
|
[
|
|
"--verbose" if verbose else "--quiet",
|
|
f"--color={color}",
|
|
f"--durations={durations}",
|
|
"-s",
|
|
]
|
|
)
|
|
|
|
if pdb:
|
|
pytest_args.append("--pdb")
|
|
if not show_warnings:
|
|
pytest_args.append("--disable-warnings")
|
|
if num_processes is not None:
|
|
pytest_args.extend(["-n", str(num_processes)])
|
|
|
|
if repeat is not None:
|
|
pytest_args.extend(["--count", str(repeat)])
|
|
if repeat < 1:
|
|
raise ValueError("The repeat argument must be at least 1.")
|
|
|
|
if mark:
|
|
pytest_args.extend(["-m", mark])
|
|
if identifier:
|
|
pytest_args.extend(["--identifier", identifier])
|
|
|
|
# Add the deepeval plugin file to pytest arguments
|
|
pytest_args.extend(["-p", "deepeval"])
|
|
# Append the extra arguments collected by allow_extra_args=True
|
|
# Pytest will raise its own error if the arguments are invalid (error:
|
|
if ctx.args:
|
|
pytest_args.extend(ctx.args)
|
|
|
|
start_time = time.perf_counter()
|
|
with capture_evaluation_run("deepeval test run"):
|
|
pytest_retcode = pytest.main(pytest_args)
|
|
end_time = time.perf_counter()
|
|
run_duration = end_time - start_time
|
|
global_test_run_manager.wrap_up_test_run(run_duration, True, display)
|
|
|
|
invoke_test_run_end_hook()
|
|
|
|
# Propagate any non-zero pytest exit code so failures surface in CI.
|
|
# pytest distinguishes: 1=tests failed, 2=interrupted (e.g. -x stop on
|
|
# first failure, which is how xdist/-n reports a halted run), 3=internal
|
|
# error, 4=usage error, 5=no tests collected. Typer ignores a command's
|
|
# return value, so we must sys.exit explicitly to set the process code.
|
|
if pytest_retcode != 0:
|
|
sys.exit(int(pytest_retcode))
|
|
|
|
return pytest_retcode
|