diff --git a/crates/ty/tests/cli.rs b/crates/ty/tests/cli.rs deleted file mode 100644 index 9fda2345da..0000000000 --- a/crates/ty/tests/cli.rs +++ /dev/null @@ -1,2207 +0,0 @@ -use anyhow::Context; -use insta::internals::SettingsBindDropGuard; -use insta_cmd::{assert_cmd_snapshot, get_cargo_bin}; -use ruff_python_ast::PythonVersion; -use std::fmt::Write; -use std::path::{Path, PathBuf}; -use std::process::Command; -use tempfile::TempDir; - -#[test] -fn test_run_in_sub_directory() -> anyhow::Result<()> { - let case = TestCase::with_files([("test.py", "~"), ("subdir/nothing", "")])?; - assert_cmd_snapshot!(case.command().current_dir(case.root().join("subdir")).arg(".."), @r" - success: false - exit_code: 1 - ----- stdout ----- - error[invalid-syntax] - --> /test.py:1:2 - | - 1 | ~ - | ^ Expected an expression - | - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - Ok(()) -} - -#[test] -fn test_include_hidden_files_by_default() -> anyhow::Result<()> { - let case = TestCase::with_files([(".test.py", "~")])?; - assert_cmd_snapshot!(case.command(), @r" - success: false - exit_code: 1 - ----- stdout ----- - error[invalid-syntax] - --> .test.py:1:2 - | - 1 | ~ - | ^ Expected an expression - | - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - Ok(()) -} - -#[test] -fn test_respect_ignore_files() -> anyhow::Result<()> { - // First test that the default option works correctly (the file is skipped) - let case = TestCase::with_files([(".ignore", "test.py"), ("test.py", "~")])?; - assert_cmd_snapshot!(case.command(), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - WARN No python files found under the given path(s) - "); - - // Test that we can set to false via CLI - assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r" - success: false - exit_code: 1 - ----- stdout ----- - error[invalid-syntax] - --> test.py:1:2 - | - 1 | ~ - | ^ Expected an expression - | - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - // Test that we can set to false via config file - case.write_file("ty.toml", "src.respect-ignore-files = false")?; - assert_cmd_snapshot!(case.command(), @r" - success: false - exit_code: 1 - ----- stdout ----- - error[invalid-syntax] - --> test.py:1:2 - | - 1 | ~ - | ^ Expected an expression - | - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - // Ensure CLI takes precedence - case.write_file("ty.toml", "src.respect-ignore-files = true")?; - assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r" - success: false - exit_code: 1 - ----- stdout ----- - error[invalid-syntax] - --> test.py:1:2 - | - 1 | ~ - | ^ Expected an expression - | - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - Ok(()) -} -/// Specifying an option on the CLI should take precedence over the same setting in the -/// project's configuration. Here, this is tested for the Python version. -#[test] -fn config_override_python_version() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "pyproject.toml", - r#" - [tool.ty.environment] - python-version = "3.11" - "#, - ), - ( - "test.py", - r#" - import sys - - # Access `sys.last_exc` that was only added in Python 3.12 - print(sys.last_exc) - "#, - ), - ])?; - - assert_cmd_snapshot!(case.command(), @r" - success: false - exit_code: 1 - ----- stdout ----- - error[unresolved-attribute]: Type `` has no attribute `last_exc` - --> test.py:5:7 - | - 4 | # Access `sys.last_exc` that was only added in Python 3.12 - 5 | print(sys.last_exc) - | ^^^^^^^^^^^^ - | - info: rule `unresolved-attribute` is enabled by default - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - assert_cmd_snapshot!(case.command().arg("--python-version").arg("3.12"), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -/// Same as above, but for the Python platform. -#[test] -fn config_override_python_platform() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "pyproject.toml", - r#" - [tool.ty.environment] - python-platform = "linux" - "#, - ), - ( - "test.py", - r#" - import sys - from typing_extensions import reveal_type - - reveal_type(sys.platform) - "#, - ), - ])?; - - assert_cmd_snapshot!(case.command(), @r#" - success: true - exit_code: 0 - ----- stdout ----- - info[revealed-type]: Revealed type - --> test.py:5:13 - | - 3 | from typing_extensions import reveal_type - 4 | - 5 | reveal_type(sys.platform) - | ^^^^^^^^^^^^ `Literal["linux"]` - | - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "#); - - assert_cmd_snapshot!(case.command().arg("--python-platform").arg("all"), @r" - success: true - exit_code: 0 - ----- stdout ----- - info[revealed-type]: Revealed type - --> test.py:5:13 - | - 3 | from typing_extensions import reveal_type - 4 | - 5 | reveal_type(sys.platform) - | ^^^^^^^^^^^^ `LiteralString` - | - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn config_file_annotation_showing_where_python_version_set_typing_error() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "pyproject.toml", - r#" - [tool.ty.environment] - python-version = "3.8" - "#, - ), - ( - "test.py", - r#" - aiter - "#, - ), - ])?; - - assert_cmd_snapshot!(case.command(), @r#" - success: false - exit_code: 1 - ----- stdout ----- - error[unresolved-reference]: Name `aiter` used when not defined - --> test.py:2:1 - | - 2 | aiter - | ^^^^^ - | - info: `aiter` was added as a builtin in Python 3.10 - info: Python 3.8 was assumed when resolving types - --> pyproject.toml:3:18 - | - 2 | [tool.ty.environment] - 3 | python-version = "3.8" - | ^^^^^ Python 3.8 assumed due to this configuration setting - | - info: rule `unresolved-reference` is enabled by default - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "#); - - assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r" - success: false - exit_code: 1 - ----- stdout ----- - error[unresolved-reference]: Name `aiter` used when not defined - --> test.py:2:1 - | - 2 | aiter - | ^^^^^ - | - info: `aiter` was added as a builtin in Python 3.10 - info: Python 3.9 was assumed when resolving types because it was specified on the command line - info: rule `unresolved-reference` is enabled by default - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "pyproject.toml", - r#" - [tool.ty.environment] - python = "venv" - "#, - ), - ( - "venv/pyvenv.cfg", - r#" - version = 3.8 - home = foo/bar/bin - "#, - ), - if cfg!(target_os = "windows") { - ("foo/bar/bin/python.exe", "") - } else { - ("foo/bar/bin/python", "") - }, - if cfg!(target_os = "windows") { - ("venv/Lib/site-packages/foo.py", "") - } else { - ("venv/lib/python3.8/site-packages/foo.py", "") - }, - ("test.py", "aiter"), - ])?; - - assert_cmd_snapshot!(case.command(), @r" - success: false - exit_code: 1 - ----- stdout ----- - error[unresolved-reference]: Name `aiter` used when not defined - --> test.py:1:1 - | - 1 | aiter - | ^^^^^ - | - info: `aiter` was added as a builtin in Python 3.10 - info: Python 3.8 was assumed when resolving types because of your virtual environment - --> venv/pyvenv.cfg:2:11 - | - 2 | version = 3.8 - | ^^^ Python version inferred from virtual environment metadata file - 3 | home = foo/bar/bin - | - info: No Python version was specified on the command line or in a configuration file - info: rule `unresolved-reference` is enabled by default - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "pyproject.toml", - r#" - [tool.ty.environment] - python = "venv" - "#, - ), - ( - "venv/pyvenv.cfg", - r#"home = foo/bar/bin - - - version = 3.8"#, - ), - if cfg!(target_os = "windows") { - ("foo/bar/bin/python.exe", "") - } else { - ("foo/bar/bin/python", "") - }, - if cfg!(target_os = "windows") { - ("venv/Lib/site-packages/foo.py", "") - } else { - ("venv/lib/python3.8/site-packages/foo.py", "") - }, - ("test.py", "aiter"), - ])?; - - assert_cmd_snapshot!(case.command(), @r" - success: false - exit_code: 1 - ----- stdout ----- - error[unresolved-reference]: Name `aiter` used when not defined - --> test.py:1:1 - | - 1 | aiter - | ^^^^^ - | - info: `aiter` was added as a builtin in Python 3.10 - info: Python 3.8 was assumed when resolving types because of your virtual environment - --> venv/pyvenv.cfg:4:23 - | - 4 | version = 3.8 - | ^^^ Python version inferred from virtual environment metadata file - | - info: No Python version was specified on the command line or in a configuration file - info: rule `unresolved-reference` is enabled by default - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn config_file_annotation_showing_where_python_version_set_syntax_error() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "pyproject.toml", - r#" - [project] - requires-python = ">=3.8" - "#, - ), - ( - "test.py", - r#" - match object(): - case int(): - pass - case _: - pass - "#, - ), - ])?; - - assert_cmd_snapshot!(case.command(), @r#" - success: false - exit_code: 1 - ----- stdout ----- - error[invalid-syntax] - --> test.py:2:1 - | - 2 | match object(): - | ^^^^^ Cannot use `match` statement on Python 3.8 (syntax was added in Python 3.10) - 3 | case int(): - 4 | pass - | - info: Python 3.8 was assumed when parsing syntax - --> pyproject.toml:3:19 - | - 2 | [project] - 3 | requires-python = ">=3.8" - | ^^^^^^^ Python 3.8 assumed due to this configuration setting - | - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "#); - - assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r" - success: false - exit_code: 1 - ----- stdout ----- - error[invalid-syntax] - --> test.py:2:1 - | - 2 | match object(): - | ^^^^^ Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) - 3 | case int(): - 4 | pass - | - info: Python 3.9 was assumed when parsing syntax because it was specified on the command line - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -/// Paths specified on the CLI are relative to the current working directory and not the project root. -/// -/// We test this by adding an extra search path from the CLI to the libs directory when -/// running the CLI from the child directory (using relative paths). -/// -/// Project layout: -/// ``` -/// - libs -/// |- utils.py -/// - child -/// | - test.py -/// - pyproject.toml -/// ``` -/// -/// And the command is run in the `child` directory. -#[test] -fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "pyproject.toml", - r#" - [tool.ty.environment] - python-version = "3.11" - "#, - ), - ( - "libs/utils.py", - r#" - def add(a: int, b: int) -> int: - return a + b - "#, - ), - ( - "child/test.py", - r#" - from utils import add - - stat = add(10, 15) - "#, - ), - ])?; - - // Make sure that the CLI fails when the `libs` directory is not in the search path. - assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r" - success: false - exit_code: 1 - ----- stdout ----- - error[unresolved-import]: Cannot resolve imported module `utils` - --> test.py:2:6 - | - 2 | from utils import add - | ^^^^^ - 3 | - 4 | stat = add(10, 15) - | - info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment - info: rule `unresolved-import` is enabled by default - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")).arg("--extra-search-path").arg("../libs"), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -/// Paths specified in a configuration file are relative to the project root. -/// -/// We test this by adding `libs` (as a relative path) to the extra search path in the configuration and run -/// the CLI from a subdirectory. -/// -/// Project layout: -/// ``` -/// - libs -/// |- utils.py -/// - child -/// | - test.py -/// - pyproject.toml -/// ``` -#[test] -fn paths_in_configuration_files_are_relative_to_the_project_root() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "pyproject.toml", - r#" - [tool.ty.environment] - python-version = "3.11" - extra-paths = ["libs"] - "#, - ), - ( - "libs/utils.py", - r#" - def add(a: int, b: int) -> int: - return a + b - "#, - ), - ( - "child/test.py", - r#" - from utils import add - - stat = add(10, 15) - "#, - ), - ])?; - - assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -/// The rule severity can be changed in the configuration file -#[test] -fn configuration_rule_severity() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - y = 4 / 0 - - for a in range(0, int(y)): - x = a - - prin(x) # unresolved-reference - "#, - )?; - - // Assert that there's an `unresolved-reference` diagnostic (error). - assert_cmd_snapshot!(case.command(), @r###" - success: false - exit_code: 1 - ----- stdout ----- - error[unresolved-reference]: Name `prin` used when not defined - --> test.py:7:1 - | - 5 | x = a - 6 | - 7 | prin(x) # unresolved-reference - | ^^^^ - | - info: rule `unresolved-reference` is enabled by default - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "###); - - case.write_file( - "pyproject.toml", - r#" - [tool.ty.rules] - division-by-zero = "warn" # promote to warn - unresolved-reference = "ignore" - "#, - )?; - - assert_cmd_snapshot!(case.command(), @r" - success: true - exit_code: 0 - ----- stdout ----- - warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero - --> test.py:2:5 - | - 2 | y = 4 / 0 - | ^^^^^ - 3 | - 4 | for a in range(0, int(y)): - | - info: rule `division-by-zero` was selected in the configuration file - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -/// The rule severity can be changed using `--ignore`, `--warn`, and `--error` -#[test] -fn cli_rule_severity() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - import does_not_exit - - y = 4 / 0 - - for a in range(0, int(y)): - x = a - - prin(x) # unresolved-reference - "#, - )?; - - // Assert that there's an `unresolved-reference` diagnostic (error) - // and an unresolved-import (error) diagnostic by default. - assert_cmd_snapshot!(case.command(), @r###" - success: false - exit_code: 1 - ----- stdout ----- - error[unresolved-import]: Cannot resolve imported module `does_not_exit` - --> test.py:2:8 - | - 2 | import does_not_exit - | ^^^^^^^^^^^^^ - 3 | - 4 | y = 4 / 0 - | - info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment - info: rule `unresolved-import` is enabled by default - - error[unresolved-reference]: Name `prin` used when not defined - --> test.py:9:1 - | - 7 | x = a - 8 | - 9 | prin(x) # unresolved-reference - | ^^^^ - | - info: rule `unresolved-reference` is enabled by default - - Found 2 diagnostics - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "###); - - assert_cmd_snapshot!( - case - .command() - .arg("--ignore") - .arg("unresolved-reference") - .arg("--warn") - .arg("division-by-zero") - .arg("--warn") - .arg("unresolved-import"), - @r" - success: true - exit_code: 0 - ----- stdout ----- - warning[unresolved-import]: Cannot resolve imported module `does_not_exit` - --> test.py:2:8 - | - 2 | import does_not_exit - | ^^^^^^^^^^^^^ - 3 | - 4 | y = 4 / 0 - | - info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment - info: rule `unresolved-import` was selected on the command line - - warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero - --> test.py:4:5 - | - 2 | import does_not_exit - 3 | - 4 | y = 4 / 0 - | ^^^^^ - 5 | - 6 | for a in range(0, int(y)): - | - info: rule `division-by-zero` was selected on the command line - - Found 2 diagnostics - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - " - ); - - Ok(()) -} - -/// The rule severity can be changed using `--ignore`, `--warn`, and `--error` and -/// values specified last override previous severities. -#[test] -fn cli_rule_severity_precedence() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - y = 4 / 0 - - for a in range(0, int(y)): - x = a - - prin(x) # unresolved-reference - "#, - )?; - - // Assert that there's a `unresolved-reference` diagnostic (error) by default. - assert_cmd_snapshot!(case.command(), @r###" - success: false - exit_code: 1 - ----- stdout ----- - error[unresolved-reference]: Name `prin` used when not defined - --> test.py:7:1 - | - 5 | x = a - 6 | - 7 | prin(x) # unresolved-reference - | ^^^^ - | - info: rule `unresolved-reference` is enabled by default - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "###); - - assert_cmd_snapshot!( - case - .command() - .arg("--warn") - .arg("unresolved-reference") - .arg("--warn") - .arg("division-by-zero") - .arg("--ignore") - .arg("unresolved-reference"), - @r" - success: true - exit_code: 0 - ----- stdout ----- - warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero - --> test.py:2:5 - | - 2 | y = 4 / 0 - | ^^^^^ - 3 | - 4 | for a in range(0, int(y)): - | - info: rule `division-by-zero` was selected on the command line - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - " - ); - - Ok(()) -} - -/// ty warns about unknown rules specified in a configuration file -#[test] -fn configuration_unknown_rules() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "pyproject.toml", - r#" - [tool.ty.rules] - division-by-zer = "warn" # incorrect rule name - "#, - ), - ("test.py", "print(10)"), - ])?; - - assert_cmd_snapshot!(case.command(), @r#" - success: true - exit_code: 0 - ----- stdout ----- - warning[unknown-rule] - --> pyproject.toml:3:1 - | - 2 | [tool.ty.rules] - 3 | division-by-zer = "warn" # incorrect rule name - | ^^^^^^^^^^^^^^^ Unknown lint rule `division-by-zer` - | - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "#); - - Ok(()) -} - -/// ty warns about unknown rules specified in a CLI argument -#[test] -fn cli_unknown_rules() -> anyhow::Result<()> { - let case = TestCase::with_file("test.py", "print(10)")?; - - assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r" - success: true - exit_code: 0 - ----- stdout ----- - warning[unknown-rule]: Unknown lint rule `division-by-zer` - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn python_cli_argument_virtual_environment() -> anyhow::Result<()> { - let path_to_executable = if cfg!(windows) { - "my-venv/Scripts/python.exe" - } else { - "my-venv/bin/python" - }; - - let other_venv_path = "my-venv/foo/some_other_file.txt"; - - let case = TestCase::with_files([ - ("test.py", ""), - ( - if cfg!(windows) { - "my-venv/Lib/site-packages/foo.py" - } else { - "my-venv/lib/python3.13/site-packages/foo.py" - }, - "", - ), - (path_to_executable, ""), - (other_venv_path, ""), - ])?; - - // Passing a path to the installation works - assert_cmd_snapshot!(case.command().arg("--python").arg("my-venv"), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - // And so does passing a path to the executable inside the installation - assert_cmd_snapshot!(case.command().arg("--python").arg(path_to_executable), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - // But random other paths inside the installation are rejected - assert_cmd_snapshot!(case.command().arg("--python").arg(other_venv_path), @r" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - ty failed - Cause: Invalid search path settings - Cause: Failed to discover the site-packages directory: Invalid `--python` argument `/my-venv/foo/some_other_file.txt`: does not point to a Python executable or a directory on disk - "); - - // And so are paths that do not exist on disk - assert_cmd_snapshot!(case.command().arg("--python").arg("not-a-directory-or-executable"), @r" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - ty failed - Cause: Invalid search path settings - Cause: Failed to discover the site-packages directory: Invalid `--python` argument `/not-a-directory-or-executable`: does not point to a Python executable or a directory on disk - "); - - Ok(()) -} - -#[test] -fn python_cli_argument_system_installation() -> anyhow::Result<()> { - let path_to_executable = if cfg!(windows) { - "Python3.11/python.exe" - } else { - "Python3.11/bin/python" - }; - - let case = TestCase::with_files([ - ("test.py", ""), - ( - if cfg!(windows) { - "Python3.11/Lib/site-packages/foo.py" - } else { - "Python3.11/lib/python3.11/site-packages/foo.py" - }, - "", - ), - (path_to_executable, ""), - ])?; - - // Passing a path to the installation works - assert_cmd_snapshot!(case.command().arg("--python").arg("Python3.11"), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - // And so does passing a path to the executable inside the installation - assert_cmd_snapshot!(case.command().arg("--python").arg(path_to_executable), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn config_file_broken_python_setting() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "pyproject.toml", - r#" - [project] - name = "test" - version = "0.1.0" - description = "Some description" - readme = "README.md" - requires-python = ">=3.13" - dependencies = [] - - [tool.ty.environment] - python = "not-a-directory-or-executable" - "#, - ), - ("test.py", ""), - ])?; - - assert_cmd_snapshot!(case.command(), @r#" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - ty failed - Cause: Invalid search path settings - Cause: Failed to discover the site-packages directory: Invalid `environment.python` setting - - --> Invalid setting in configuration file `/pyproject.toml` - | - 9 | - 10 | [tool.ty.environment] - 11 | python = "not-a-directory-or-executable" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not point to a Python executable or a directory on disk - | - "#); - - Ok(()) -} - -#[test] -fn config_file_python_setting_directory_with_no_site_packages() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "pyproject.toml", - r#" - [tool.ty.environment] - python = "directory-but-no-site-packages" - "#, - ), - ("directory-but-no-site-packages/lib/foo.py", ""), - ("test.py", ""), - ])?; - - assert_cmd_snapshot!(case.command(), @r#" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - ty failed - Cause: Invalid search path settings - Cause: Failed to discover the site-packages directory: Invalid `environment.python` setting - - --> Invalid setting in configuration file `/pyproject.toml` - | - 1 | - 2 | [tool.ty.environment] - 3 | python = "directory-but-no-site-packages" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not find a `site-packages` directory for this Python installation/executable - | - "#); - - Ok(()) -} - -// This error message is never emitted on Windows, because Windows installations have simpler layouts -#[cfg(not(windows))] -#[test] -fn unix_system_installation_with_no_lib_directory() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "pyproject.toml", - r#" - [tool.ty.environment] - python = "directory-but-no-site-packages" - "#, - ), - ("directory-but-no-site-packages/foo.py", ""), - ("test.py", ""), - ])?; - - assert_cmd_snapshot!(case.command(), @r#" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - ty failed - Cause: Invalid search path settings - Cause: Failed to discover the site-packages directory: Failed to iterate over the contents of the `lib` directory of the Python installation - - --> Invalid setting in configuration file `/pyproject.toml` - | - 1 | - 2 | [tool.ty.environment] - 3 | python = "directory-but-no-site-packages" - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - "#); - - Ok(()) -} - -#[test] -fn exit_code_only_warnings() -> anyhow::Result<()> { - let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?; - - assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r" - success: true - exit_code: 0 - ----- stdout ----- - warning[unresolved-reference]: Name `x` used when not defined - --> test.py:1:7 - | - 1 | print(x) # [unresolved-reference] - | ^ - | - info: rule `unresolved-reference` was selected on the command line - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn exit_code_only_info() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - from typing_extensions import reveal_type - reveal_type(1) - "#, - )?; - - assert_cmd_snapshot!(case.command(), @r" - success: true - exit_code: 0 - ----- stdout ----- - info[revealed-type]: Revealed type - --> test.py:3:13 - | - 2 | from typing_extensions import reveal_type - 3 | reveal_type(1) - | ^ `Literal[1]` - | - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - from typing_extensions import reveal_type - reveal_type(1) - "#, - )?; - - assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r" - success: true - exit_code: 0 - ----- stdout ----- - info[revealed-type]: Revealed type - --> test.py:3:13 - | - 2 | from typing_extensions import reveal_type - 3 | reveal_type(1) - | ^ `Literal[1]` - | - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> { - let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?; - - assert_cmd_snapshot!(case.command().arg("--error-on-warning").arg("--warn").arg("unresolved-reference"), @r" - success: false - exit_code: 1 - ----- stdout ----- - warning[unresolved-reference]: Name `x` used when not defined - --> test.py:1:7 - | - 1 | print(x) # [unresolved-reference] - | ^ - | - info: rule `unresolved-reference` was selected on the command line - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ("test.py", r"print(x) # [unresolved-reference]"), - ( - "ty.toml", - r#" - [terminal] - error-on-warning = true - "#, - ), - ])?; - - assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r" - success: false - exit_code: 1 - ----- stdout ----- - warning[unresolved-reference]: Name `x` used when not defined - --> test.py:1:7 - | - 1 | print(x) # [unresolved-reference] - | ^ - | - info: rule `unresolved-reference` was selected on the command line - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - print(x) # [unresolved-reference] - print(4[1]) # [non-subscriptable] - "#, - )?; - - assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r" - success: false - exit_code: 1 - ----- stdout ----- - warning[unresolved-reference]: Name `x` used when not defined - --> test.py:2:7 - | - 2 | print(x) # [unresolved-reference] - | ^ - 3 | print(4[1]) # [non-subscriptable] - | - info: rule `unresolved-reference` was selected on the command line - - error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method - --> test.py:3:7 - | - 2 | print(x) # [unresolved-reference] - 3 | print(4[1]) # [non-subscriptable] - | ^ - | - info: rule `non-subscriptable` is enabled by default - - Found 2 diagnostics - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r###" - print(x) # [unresolved-reference] - print(4[1]) # [non-subscriptable] - "###, - )?; - - assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--error-on-warning"), @r" - success: false - exit_code: 1 - ----- stdout ----- - warning[unresolved-reference]: Name `x` used when not defined - --> test.py:2:7 - | - 2 | print(x) # [unresolved-reference] - | ^ - 3 | print(4[1]) # [non-subscriptable] - | - info: rule `unresolved-reference` was selected on the command line - - error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method - --> test.py:3:7 - | - 2 | print(x) # [unresolved-reference] - 3 | print(4[1]) # [non-subscriptable] - | ^ - | - info: rule `non-subscriptable` is enabled by default - - Found 2 diagnostics - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn exit_code_exit_zero_is_true() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - print(x) # [unresolved-reference] - print(4[1]) # [non-subscriptable] - "#, - )?; - - assert_cmd_snapshot!(case.command().arg("--exit-zero").arg("--warn").arg("unresolved-reference"), @r" - success: true - exit_code: 0 - ----- stdout ----- - warning[unresolved-reference]: Name `x` used when not defined - --> test.py:2:7 - | - 2 | print(x) # [unresolved-reference] - | ^ - 3 | print(4[1]) # [non-subscriptable] - | - info: rule `unresolved-reference` was selected on the command line - - error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method - --> test.py:3:7 - | - 2 | print(x) # [unresolved-reference] - 3 | print(4[1]) # [non-subscriptable] - | ^ - | - info: rule `non-subscriptable` is enabled by default - - Found 2 diagnostics - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn user_configuration() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "project/ty.toml", - r#" - [rules] - division-by-zero = "warn" - "#, - ), - ( - "project/main.py", - r#" - y = 4 / 0 - - for a in range(0, int(y)): - x = a - - prin(x) - "#, - ), - ])?; - - let config_directory = case.root().join("home/.config"); - let config_env_var = if cfg!(windows) { - "APPDATA" - } else { - "XDG_CONFIG_HOME" - }; - - assert_cmd_snapshot!( - case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()), - @r" - success: false - exit_code: 1 - ----- stdout ----- - warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero - --> main.py:2:5 - | - 2 | y = 4 / 0 - | ^^^^^ - 3 | - 4 | for a in range(0, int(y)): - | - info: rule `division-by-zero` was selected in the configuration file - - error[unresolved-reference]: Name `prin` used when not defined - --> main.py:7:1 - | - 5 | x = a - 6 | - 7 | prin(x) - | ^^^^ - | - info: rule `unresolved-reference` is enabled by default - - Found 2 diagnostics - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - " - ); - - // The user-level configuration sets the severity for `unresolved-reference` to warn. - // Changing the level for `division-by-zero` has no effect, because the project-level configuration - // has higher precedence. - case.write_file( - config_directory.join("ty/ty.toml"), - r#" - [rules] - division-by-zero = "error" - unresolved-reference = "warn" - "#, - )?; - - assert_cmd_snapshot!( - case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()), - @r" - success: true - exit_code: 0 - ----- stdout ----- - warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero - --> main.py:2:5 - | - 2 | y = 4 / 0 - | ^^^^^ - 3 | - 4 | for a in range(0, int(y)): - | - info: rule `division-by-zero` was selected in the configuration file - - warning[unresolved-reference]: Name `prin` used when not defined - --> main.py:7:1 - | - 5 | x = a - 6 | - 7 | prin(x) - | ^^^^ - | - info: rule `unresolved-reference` was selected in the configuration file - - Found 2 diagnostics - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - " - ); - - Ok(()) -} - -#[test] -fn check_specific_paths() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "project/main.py", - r#" - y = 4 / 0 # error: division-by-zero - "#, - ), - ( - "project/tests/test_main.py", - r#" - import does_not_exist # error: unresolved-import - "#, - ), - ( - "project/other.py", - r#" - from main2 import z # error: unresolved-import - - print(z) - "#, - ), - ])?; - - assert_cmd_snapshot!( - case.command(), - @r###" - success: false - exit_code: 1 - ----- stdout ----- - error[unresolved-import]: Cannot resolve imported module `main2` - --> project/other.py:2:6 - | - 2 | from main2 import z # error: unresolved-import - | ^^^^^ - 3 | - 4 | print(z) - | - info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment - info: rule `unresolved-import` is enabled by default - - error[unresolved-import]: Cannot resolve imported module `does_not_exist` - --> project/tests/test_main.py:2:8 - | - 2 | import does_not_exist # error: unresolved-import - | ^^^^^^^^^^^^^^ - | - info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment - info: rule `unresolved-import` is enabled by default - - Found 2 diagnostics - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "### - ); - - // Now check only the `tests` and `other.py` files. - // We should no longer see any diagnostics related to `main.py`. - assert_cmd_snapshot!( - case.command().arg("project/tests").arg("project/other.py"), - @r" - success: false - exit_code: 1 - ----- stdout ----- - error[unresolved-import]: Cannot resolve imported module `main2` - --> project/other.py:2:6 - | - 2 | from main2 import z # error: unresolved-import - | ^^^^^ - 3 | - 4 | print(z) - | - info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment - info: rule `unresolved-import` is enabled by default - - error[unresolved-import]: Cannot resolve imported module `does_not_exist` - --> project/tests/test_main.py:2:8 - | - 2 | import does_not_exist # error: unresolved-import - | ^^^^^^^^^^^^^^ - | - info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment - info: rule `unresolved-import` is enabled by default - - Found 2 diagnostics - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - " - ); - - Ok(()) -} - -#[test] -fn check_non_existing_path() -> anyhow::Result<()> { - let case = TestCase::with_files([])?; - - let mut settings = insta::Settings::clone_current(); - settings.add_filter( - ®ex::escape("The system cannot find the path specified. (os error 3)"), - "No such file or directory (os error 2)", - ); - let _s = settings.bind_to_scope(); - - assert_cmd_snapshot!( - case.command().arg("project/main.py").arg("project/tests"), - @r" - success: false - exit_code: 1 - ----- stdout ----- - error[io]: `/project/main.py`: No such file or directory (os error 2) - - error[io]: `/project/tests`: No such file or directory (os error 2) - - Found 2 diagnostics - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - WARN No python files found under the given path(s) - " - ); - - Ok(()) -} - -#[test] -fn concise_diagnostics() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - print(x) # [unresolved-reference] - print(4[1]) # [non-subscriptable] - "#, - )?; - - assert_cmd_snapshot!(case.command().arg("--output-format=concise").arg("--warn").arg("unresolved-reference"), @r" - success: false - exit_code: 1 - ----- stdout ----- - warning[unresolved-reference] test.py:2:7: Name `x` used when not defined - error[non-subscriptable] test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method - Found 2 diagnostics - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -/// This tests the diagnostic format for revealed type. -/// -/// This test was introduced because changes were made to -/// how the revealed type diagnostic was constructed and -/// formatted in "verbose" mode. But it required extra -/// logic to ensure the concise version didn't regress on -/// information content. So this test was introduced to -/// capture that. -#[test] -fn concise_revealed_type() -> anyhow::Result<()> { - let case = TestCase::with_file( - "test.py", - r#" - from typing_extensions import reveal_type - - x = "hello" - reveal_type(x) - "#, - )?; - - assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r#" - success: true - exit_code: 0 - ----- stdout ----- - info[revealed-type] test.py:5:13: Revealed type: `Literal["hello"]` - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "#); - - Ok(()) -} - -#[test] -fn can_handle_large_binop_expressions() -> anyhow::Result<()> { - let mut content = String::new(); - writeln!( - &mut content, - " - from typing_extensions import reveal_type - total = 1{plus_one_repeated} - reveal_type(total) - ", - plus_one_repeated = " + 1".repeat(2000 - 1) - )?; - - let case = TestCase::with_file("test.py", &ruff_python_trivia::textwrap::dedent(&content))?; - - assert_cmd_snapshot!(case.command(), @r" - success: true - exit_code: 0 - ----- stdout ----- - info[revealed-type]: Revealed type - --> test.py:4:13 - | - 2 | from typing_extensions import reveal_type - 3 | total = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1... - 4 | reveal_type(total) - | ^^^^^ `Literal[2000]` - | - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn defaults_to_a_new_python_version() -> anyhow::Result<()> { - let case = TestCase::with_files([ - ( - "ty.toml", - &*format!( - r#" - [environment] - python-version = "{}" - python-platform = "linux" - "#, - PythonVersion::default() - ), - ), - ( - "main.py", - r#" - import os - - os.grantpt(1) # only available on unix, Python 3.13 or newer - "#, - ), - ])?; - - assert_cmd_snapshot!(case.command(), @r" - success: false - exit_code: 1 - ----- stdout ----- - error[unresolved-attribute]: Type `` has no attribute `grantpt` - --> main.py:4:1 - | - 2 | import os - 3 | - 4 | os.grantpt(1) # only available on unix, Python 3.13 or newer - | ^^^^^^^^^^ - | - info: rule `unresolved-attribute` is enabled by default - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - // Use default (which should be latest supported) - let case = TestCase::with_files([ - ( - "ty.toml", - r#" - [environment] - python-platform = "linux" - "#, - ), - ( - "main.py", - r#" - import os - - os.grantpt(1) # only available on unix, Python 3.13 or newer - "#, - ), - ])?; - - assert_cmd_snapshot!(case.command(), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn cli_config_args_toml_string_basic() -> anyhow::Result<()> { - let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?; - - // Long flag - assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=true"), @r" - success: false - exit_code: 1 - ----- stdout ----- - warning[unresolved-reference]: Name `x` used when not defined - --> test.py:1:7 - | - 1 | print(x) # [unresolved-reference] - | ^ - | - info: rule `unresolved-reference` was selected on the command line - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - // Short flag - assert_cmd_snapshot!(case.command().arg("-c").arg("terminal.error-on-warning=true"), @r" - success: false - exit_code: 1 - ----- stdout ----- - error[unresolved-reference]: Name `x` used when not defined - --> test.py:1:7 - | - 1 | print(x) # [unresolved-reference] - | ^ - | - info: rule `unresolved-reference` is enabled by default - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn cli_config_args_overrides_ty_toml() -> anyhow::Result<()> { - let case = TestCase::with_files(vec![ - ( - "ty.toml", - r#" - [terminal] - error-on-warning = true - "#, - ), - ("test.py", r"print(x) # [unresolved-reference]"), - ])?; - - // Exit code of 1 due to the setting in `ty.toml` - assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r" - success: false - exit_code: 1 - ----- stdout ----- - warning[unresolved-reference]: Name `x` used when not defined - --> test.py:1:7 - | - 1 | print(x) # [unresolved-reference] - | ^ - | - info: rule `unresolved-reference` was selected on the command line - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - // Exit code of 0 because the `ty.toml` setting is overwritten by `--config` - assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=false"), @r" - success: true - exit_code: 0 - ----- stdout ----- - warning[unresolved-reference]: Name `x` used when not defined - --> test.py:1:7 - | - 1 | print(x) # [unresolved-reference] - | ^ - | - info: rule `unresolved-reference` was selected on the command line - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn cli_config_args_later_overrides_earlier() -> anyhow::Result<()> { - let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?; - assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=true").arg("--config").arg("terminal.error-on-warning=false"), @r" - success: true - exit_code: 0 - ----- stdout ----- - warning[unresolved-reference]: Name `x` used when not defined - --> test.py:1:7 - | - 1 | print(x) # [unresolved-reference] - | ^ - | - info: rule `unresolved-reference` was selected on the command line - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn cli_config_args_invalid_option() -> anyhow::Result<()> { - let case = TestCase::with_file("test.py", r"print(1)")?; - assert_cmd_snapshot!(case.command().arg("--config").arg("bad-option=true"), @r###" - success: false - exit_code: 2 - ----- stdout ----- - - ----- stderr ----- - error: TOML parse error at line 1, column 1 - | - 1 | bad-option=true - | ^^^^^^^^^^ - unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal` - - - Usage: ty - - For more information, try '--help'. - "###); - - Ok(()) -} - -/// The `site-packages` directory is used by ty for external import. -/// Ty does the following checks to discover the `site-packages` directory in the order: -/// 1) If `VIRTUAL_ENV` environment variable is set -/// 2) If `CONDA_PREFIX` environment variable is set -/// 3) If a `.venv` directory exists at the project root -/// -/// This test is aiming at validating the logic around `CONDA_PREFIX`. -/// -/// A conda-like environment file structure is used -/// We test by first not setting the `CONDA_PREFIX` and expect a fail. -/// Then we test by setting `CONDA_PREFIX` to `conda-env` and expect a pass. -/// -/// ├── project -/// │ └── test.py -/// └── conda-env -/// └── lib -/// └── python3.13 -/// └── site-packages -/// └── package1 -/// └── __init__.py -/// -/// test.py imports package1 -/// And the command is run in the `project` directory. -#[test] -fn check_conda_prefix_var_to_resolve_path() -> anyhow::Result<()> { - let conda_package1_path = if cfg!(windows) { - "conda-env/Lib/site-packages/package1/__init__.py" - } else { - "conda-env/lib/python3.13/site-packages/package1/__init__.py" - }; - - let case = TestCase::with_files([ - ( - "project/test.py", - r#" - import package1 - "#, - ), - ( - conda_package1_path, - r#" - "#, - ), - ])?; - - assert_cmd_snapshot!(case.command().current_dir(case.root().join("project")), @r" - success: false - exit_code: 1 - ----- stdout ----- - error[unresolved-import]: Cannot resolve imported module `package1` - --> test.py:2:8 - | - 2 | import package1 - | ^^^^^^^^ - | - info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment - info: rule `unresolved-import` is enabled by default - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - // do command : CONDA_PREFIX=/conda_env - assert_cmd_snapshot!(case.command().current_dir(case.root().join("project")).env("CONDA_PREFIX", case.root().join("conda-env")), @r" - success: true - exit_code: 0 - ----- stdout ----- - All checks passed! - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -#[test] -fn config_file_override() -> anyhow::Result<()> { - // Set `error-on-warning` to true in the configuration file - // Explicitly set `--warn unresolved-reference` to ensure the rule warns instead of errors - let case = TestCase::with_files(vec![ - ("test.py", r"print(x) # [unresolved-reference]"), - ( - "ty-override.toml", - r#" - [terminal] - error-on-warning = true - "#, - ), - ])?; - - // Ensure flag works via CLI arg - assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config-file").arg("ty-override.toml"), @r" - success: false - exit_code: 1 - ----- stdout ----- - warning[unresolved-reference]: Name `x` used when not defined - --> test.py:1:7 - | - 1 | print(x) # [unresolved-reference] - | ^ - | - info: rule `unresolved-reference` was selected on the command line - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - // Ensure the flag works via an environment variable - assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").env("TY_CONFIG_FILE", "ty-override.toml"), @r" - success: false - exit_code: 1 - ----- stdout ----- - warning[unresolved-reference]: Name `x` used when not defined - --> test.py:1:7 - | - 1 | print(x) # [unresolved-reference] - | ^ - | - info: rule `unresolved-reference` was selected on the command line - - Found 1 diagnostic - - ----- stderr ----- - WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. - "); - - Ok(()) -} - -struct TestCase { - _temp_dir: TempDir, - _settings_scope: SettingsBindDropGuard, - project_dir: PathBuf, -} - -impl TestCase { - fn new() -> anyhow::Result { - let temp_dir = TempDir::new()?; - - // Canonicalize the tempdir path because macos uses symlinks for tempdirs - // and that doesn't play well with our snapshot filtering. - // Simplify with dunce because otherwise we get UNC paths on Windows. - let project_dir = dunce::simplified( - &temp_dir - .path() - .canonicalize() - .context("Failed to canonicalize project path")?, - ) - .to_path_buf(); - - let mut settings = insta::Settings::clone_current(); - settings.add_filter(&tempdir_filter(&project_dir), "/"); - settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1"); - - let settings_scope = settings.bind_to_scope(); - - Ok(Self { - project_dir, - _temp_dir: temp_dir, - _settings_scope: settings_scope, - }) - } - - fn with_files<'a>(files: impl IntoIterator) -> anyhow::Result { - let case = Self::new()?; - case.write_files(files)?; - Ok(case) - } - - fn with_file(path: impl AsRef, content: &str) -> anyhow::Result { - let case = Self::new()?; - case.write_file(path, content)?; - Ok(case) - } - - fn write_files<'a>( - &self, - files: impl IntoIterator, - ) -> anyhow::Result<()> { - for (path, content) in files { - self.write_file(path, content)?; - } - - Ok(()) - } - - fn write_file(&self, path: impl AsRef, content: &str) -> anyhow::Result<()> { - let path = path.as_ref(); - let path = self.project_dir.join(path); - - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent) - .with_context(|| format!("Failed to create directory `{}`", parent.display()))?; - } - std::fs::write(&path, &*ruff_python_trivia::textwrap::dedent(content)) - .with_context(|| format!("Failed to write file `{path}`", path = path.display()))?; - - Ok(()) - } - - fn root(&self) -> &Path { - &self.project_dir - } - - fn command(&self) -> Command { - let mut command = Command::new(get_cargo_bin("ty")); - command.current_dir(&self.project_dir).arg("check"); - command - } -} - -fn tempdir_filter(path: &Path) -> String { - format!(r"{}\\?/?", regex::escape(path.to_str().unwrap())) -} diff --git a/crates/ty/tests/cli/config_option.rs b/crates/ty/tests/cli/config_option.rs new file mode 100644 index 0000000000..4ea24fa4f3 --- /dev/null +++ b/crates/ty/tests/cli/config_option.rs @@ -0,0 +1,206 @@ +use insta_cmd::assert_cmd_snapshot; + +use crate::CliTest; + +#[test] +fn cli_config_args_toml_string_basic() -> anyhow::Result<()> { + let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?; + + // Long flag + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=true"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Short flag + assert_cmd_snapshot!(case.command().arg("-c").arg("terminal.error-on-warning=true"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn cli_config_args_overrides_ty_toml() -> anyhow::Result<()> { + let case = CliTest::with_files(vec![ + ( + "ty.toml", + r#" + [terminal] + error-on-warning = true + "#, + ), + ("test.py", r"print(x) # [unresolved-reference]"), + ])?; + + // Exit code of 1 due to the setting in `ty.toml` + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Exit code of 0 because the `ty.toml` setting is overwritten by `--config` + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=false"), @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn cli_config_args_later_overrides_earlier() -> anyhow::Result<()> { + let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?; + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config").arg("terminal.error-on-warning=true").arg("--config").arg("terminal.error-on-warning=false"), @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn cli_config_args_invalid_option() -> anyhow::Result<()> { + let case = CliTest::with_file("test.py", r"print(1)")?; + assert_cmd_snapshot!(case.command().arg("--config").arg("bad-option=true"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: TOML parse error at line 1, column 1 + | + 1 | bad-option=true + | ^^^^^^^^^^ + unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal` + + + Usage: ty + + For more information, try '--help'. + "###); + + Ok(()) +} + +#[test] +fn config_file_override() -> anyhow::Result<()> { + // Set `error-on-warning` to true in the configuration file + // Explicitly set `--warn unresolved-reference` to ensure the rule warns instead of errors + let case = CliTest::with_files(vec![ + ("test.py", r"print(x) # [unresolved-reference]"), + ( + "ty-override.toml", + r#" + [terminal] + error-on-warning = true + "#, + ), + ])?; + + // Ensure flag works via CLI arg + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--config-file").arg("ty-override.toml"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Ensure the flag works via an environment variable + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").env("TY_CONFIG_FILE", "ty-override.toml"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} diff --git a/crates/ty/tests/cli/exit_code.rs b/crates/ty/tests/cli/exit_code.rs new file mode 100644 index 0000000000..7c7a93e488 --- /dev/null +++ b/crates/ty/tests/cli/exit_code.rs @@ -0,0 +1,272 @@ +use insta_cmd::assert_cmd_snapshot; + +use crate::CliTest; + +#[test] +fn only_warnings() -> anyhow::Result<()> { + let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?; + + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn only_info() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + from typing_extensions import reveal_type + reveal_type(1) + "#, + )?; + + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + info[revealed-type]: Revealed type + --> test.py:3:13 + | + 2 | from typing_extensions import reveal_type + 3 | reveal_type(1) + | ^ `Literal[1]` + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn only_info_and_error_on_warning_is_true() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + from typing_extensions import reveal_type + reveal_type(1) + "#, + )?; + + assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r" + success: true + exit_code: 0 + ----- stdout ----- + info[revealed-type]: Revealed type + --> test.py:3:13 + | + 2 | from typing_extensions import reveal_type + 3 | reveal_type(1) + | ^ `Literal[1]` + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> { + let case = CliTest::with_file("test.py", r"print(x) # [unresolved-reference]")?; + + assert_cmd_snapshot!(case.command().arg("--error-on-warning").arg("--warn").arg("unresolved-reference"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn no_errors_but_error_on_warning_is_enabled_in_configuration() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ("test.py", r"print(x) # [unresolved-reference]"), + ( + "ty.toml", + r#" + [terminal] + error-on-warning = true + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:1:7 + | + 1 | print(x) # [unresolved-reference] + | ^ + | + info: rule `unresolved-reference` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn both_warnings_and_errors() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + print(x) # [unresolved-reference] + print(4[1]) # [non-subscriptable] + "#, + )?; + + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:2:7 + | + 2 | print(x) # [unresolved-reference] + | ^ + 3 | print(4[1]) # [non-subscriptable] + | + info: rule `unresolved-reference` was selected on the command line + + error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method + --> test.py:3:7 + | + 2 | print(x) # [unresolved-reference] + 3 | print(4[1]) # [non-subscriptable] + | ^ + | + info: rule `non-subscriptable` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r###" + print(x) # [unresolved-reference] + print(4[1]) # [non-subscriptable] + "###, + )?; + + assert_cmd_snapshot!(case.command().arg("--warn").arg("unresolved-reference").arg("--error-on-warning"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:2:7 + | + 2 | print(x) # [unresolved-reference] + | ^ + 3 | print(4[1]) # [non-subscriptable] + | + info: rule `unresolved-reference` was selected on the command line + + error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method + --> test.py:3:7 + | + 2 | print(x) # [unresolved-reference] + 3 | print(4[1]) # [non-subscriptable] + | ^ + | + info: rule `non-subscriptable` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn exit_zero_is_true() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + print(x) # [unresolved-reference] + print(4[1]) # [non-subscriptable] + "#, + )?; + + assert_cmd_snapshot!(case.command().arg("--exit-zero").arg("--warn").arg("unresolved-reference"), @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[unresolved-reference]: Name `x` used when not defined + --> test.py:2:7 + | + 2 | print(x) # [unresolved-reference] + | ^ + 3 | print(4[1]) # [non-subscriptable] + | + info: rule `unresolved-reference` was selected on the command line + + error[non-subscriptable]: Cannot subscript object of type `Literal[4]` with no `__getitem__` method + --> test.py:3:7 + | + 2 | print(x) # [unresolved-reference] + 3 | print(4[1]) # [non-subscriptable] + | ^ + | + info: rule `non-subscriptable` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} diff --git a/crates/ty/tests/cli/main.rs b/crates/ty/tests/cli/main.rs new file mode 100644 index 0000000000..023e14cefc --- /dev/null +++ b/crates/ty/tests/cli/main.rs @@ -0,0 +1,690 @@ +mod config_option; +mod exit_code; +mod python_environment; +mod rule_selection; + +use anyhow::Context as _; +use insta::internals::SettingsBindDropGuard; +use insta_cmd::{assert_cmd_snapshot, get_cargo_bin}; +use std::{ + fmt::Write, + path::{Path, PathBuf}, + process::Command, +}; +use tempfile::TempDir; + +#[test] +fn test_run_in_sub_directory() -> anyhow::Result<()> { + let case = CliTest::with_files([("test.py", "~"), ("subdir/nothing", "")])?; + assert_cmd_snapshot!(case.command().current_dir(case.root().join("subdir")).arg(".."), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[invalid-syntax] + --> /test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + Ok(()) +} + +#[test] +fn test_include_hidden_files_by_default() -> anyhow::Result<()> { + let case = CliTest::with_files([(".test.py", "~")])?; + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[invalid-syntax] + --> .test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + Ok(()) +} + +#[test] +fn test_respect_ignore_files() -> anyhow::Result<()> { + // First test that the default option works correctly (the file is skipped) + let case = CliTest::with_files([(".ignore", "test.py"), ("test.py", "~")])?; + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + WARN No python files found under the given path(s) + "); + + // Test that we can set to false via CLI + assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[invalid-syntax] + --> test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Test that we can set to false via config file + case.write_file("ty.toml", "src.respect-ignore-files = false")?; + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[invalid-syntax] + --> test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Ensure CLI takes precedence + case.write_file("ty.toml", "src.respect-ignore-files = true")?; + assert_cmd_snapshot!(case.command().arg("--no-respect-ignore-files"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[invalid-syntax] + --> test.py:1:2 + | + 1 | ~ + | ^ Expected an expression + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + Ok(()) +} + +/// Paths specified on the CLI are relative to the current working directory and not the project root. +/// +/// We test this by adding an extra search path from the CLI to the libs directory when +/// running the CLI from the child directory (using relative paths). +/// +/// Project layout: +/// ``` +/// - libs +/// |- utils.py +/// - child +/// | - test.py +/// - pyproject.toml +/// ``` +/// +/// And the command is run in the `child` directory. +#[test] +fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python-version = "3.11" + "#, + ), + ( + "libs/utils.py", + r#" + def add(a: int, b: int) -> int: + return a + b + "#, + ), + ( + "child/test.py", + r#" + from utils import add + + stat = add(10, 15) + "#, + ), + ])?; + + // Make sure that the CLI fails when the `libs` directory is not in the search path. + assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-import]: Cannot resolve imported module `utils` + --> test.py:2:6 + | + 2 | from utils import add + | ^^^^^ + 3 | + 4 | stat = add(10, 15) + | + info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment + info: rule `unresolved-import` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")).arg("--extra-search-path").arg("../libs"), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// Paths specified in a configuration file are relative to the project root. +/// +/// We test this by adding `libs` (as a relative path) to the extra search path in the configuration and run +/// the CLI from a subdirectory. +/// +/// Project layout: +/// ``` +/// - libs +/// |- utils.py +/// - child +/// | - test.py +/// - pyproject.toml +/// ``` +#[test] +fn paths_in_configuration_files_are_relative_to_the_project_root() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python-version = "3.11" + extra-paths = ["libs"] + "#, + ), + ( + "libs/utils.py", + r#" + def add(a: int, b: int) -> int: + return a + b + "#, + ), + ( + "child/test.py", + r#" + from utils import add + + stat = add(10, 15) + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn user_configuration() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "project/ty.toml", + r#" + [rules] + division-by-zero = "warn" + "#, + ), + ( + "project/main.py", + r#" + y = 4 / 0 + + for a in range(0, int(y)): + x = a + + prin(x) + "#, + ), + ])?; + + let config_directory = case.root().join("home/.config"); + let config_env_var = if cfg!(windows) { + "APPDATA" + } else { + "XDG_CONFIG_HOME" + }; + + assert_cmd_snapshot!( + case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()), + @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> main.py:2:5 + | + 2 | y = 4 / 0 + | ^^^^^ + 3 | + 4 | for a in range(0, int(y)): + | + info: rule `division-by-zero` was selected in the configuration file + + error[unresolved-reference]: Name `prin` used when not defined + --> main.py:7:1 + | + 5 | x = a + 6 | + 7 | prin(x) + | ^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + " + ); + + // The user-level configuration sets the severity for `unresolved-reference` to warn. + // Changing the level for `division-by-zero` has no effect, because the project-level configuration + // has higher precedence. + case.write_file( + config_directory.join("ty/ty.toml"), + r#" + [rules] + division-by-zero = "error" + unresolved-reference = "warn" + "#, + )?; + + assert_cmd_snapshot!( + case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()), + @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> main.py:2:5 + | + 2 | y = 4 / 0 + | ^^^^^ + 3 | + 4 | for a in range(0, int(y)): + | + info: rule `division-by-zero` was selected in the configuration file + + warning[unresolved-reference]: Name `prin` used when not defined + --> main.py:7:1 + | + 5 | x = a + 6 | + 7 | prin(x) + | ^^^^ + | + info: rule `unresolved-reference` was selected in the configuration file + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + " + ); + + Ok(()) +} + +#[test] +fn check_specific_paths() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "project/main.py", + r#" + y = 4 / 0 # error: division-by-zero + "#, + ), + ( + "project/tests/test_main.py", + r#" + import does_not_exist # error: unresolved-import + "#, + ), + ( + "project/other.py", + r#" + from main2 import z # error: unresolved-import + + print(z) + "#, + ), + ])?; + + assert_cmd_snapshot!( + case.command(), + @r###" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-import]: Cannot resolve imported module `main2` + --> project/other.py:2:6 + | + 2 | from main2 import z # error: unresolved-import + | ^^^^^ + 3 | + 4 | print(z) + | + info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment + info: rule `unresolved-import` is enabled by default + + error[unresolved-import]: Cannot resolve imported module `does_not_exist` + --> project/tests/test_main.py:2:8 + | + 2 | import does_not_exist # error: unresolved-import + | ^^^^^^^^^^^^^^ + | + info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment + info: rule `unresolved-import` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "### + ); + + // Now check only the `tests` and `other.py` files. + // We should no longer see any diagnostics related to `main.py`. + assert_cmd_snapshot!( + case.command().arg("project/tests").arg("project/other.py"), + @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-import]: Cannot resolve imported module `main2` + --> project/other.py:2:6 + | + 2 | from main2 import z # error: unresolved-import + | ^^^^^ + 3 | + 4 | print(z) + | + info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment + info: rule `unresolved-import` is enabled by default + + error[unresolved-import]: Cannot resolve imported module `does_not_exist` + --> project/tests/test_main.py:2:8 + | + 2 | import does_not_exist # error: unresolved-import + | ^^^^^^^^^^^^^^ + | + info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment + info: rule `unresolved-import` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + " + ); + + Ok(()) +} + +#[test] +fn check_non_existing_path() -> anyhow::Result<()> { + let case = CliTest::with_files([])?; + + let mut settings = insta::Settings::clone_current(); + settings.add_filter( + ®ex::escape("The system cannot find the path specified. (os error 3)"), + "No such file or directory (os error 2)", + ); + let _s = settings.bind_to_scope(); + + assert_cmd_snapshot!( + case.command().arg("project/main.py").arg("project/tests"), + @r" + success: false + exit_code: 1 + ----- stdout ----- + error[io]: `/project/main.py`: No such file or directory (os error 2) + + error[io]: `/project/tests`: No such file or directory (os error 2) + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + WARN No python files found under the given path(s) + " + ); + + Ok(()) +} + +#[test] +fn concise_diagnostics() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + print(x) # [unresolved-reference] + print(4[1]) # [non-subscriptable] + "#, + )?; + + assert_cmd_snapshot!(case.command().arg("--output-format=concise").arg("--warn").arg("unresolved-reference"), @r" + success: false + exit_code: 1 + ----- stdout ----- + warning[unresolved-reference] test.py:2:7: Name `x` used when not defined + error[non-subscriptable] test.py:3:7: Cannot subscript object of type `Literal[4]` with no `__getitem__` method + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// This tests the diagnostic format for revealed type. +/// +/// This test was introduced because changes were made to +/// how the revealed type diagnostic was constructed and +/// formatted in "verbose" mode. But it required extra +/// logic to ensure the concise version didn't regress on +/// information content. So this test was introduced to +/// capture that. +#[test] +fn concise_revealed_type() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + from typing_extensions import reveal_type + + x = "hello" + reveal_type(x) + "#, + )?; + + assert_cmd_snapshot!(case.command().arg("--output-format=concise"), @r#" + success: true + exit_code: 0 + ----- stdout ----- + info[revealed-type] test.py:5:13: Revealed type: `Literal["hello"]` + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + Ok(()) +} + +#[test] +fn can_handle_large_binop_expressions() -> anyhow::Result<()> { + let mut content = String::new(); + writeln!( + &mut content, + " + from typing_extensions import reveal_type + total = 1{plus_one_repeated} + reveal_type(total) + ", + plus_one_repeated = " + 1".repeat(2000 - 1) + )?; + + let case = CliTest::with_file("test.py", &ruff_python_trivia::textwrap::dedent(&content))?; + + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + info[revealed-type]: Revealed type + --> test.py:4:13 + | + 2 | from typing_extensions import reveal_type + 3 | total = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1... + 4 | reveal_type(total) + | ^^^^^ `Literal[2000]` + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +pub(crate) struct CliTest { + _temp_dir: TempDir, + _settings_scope: SettingsBindDropGuard, + project_dir: PathBuf, +} + +impl CliTest { + pub(crate) fn new() -> anyhow::Result { + let temp_dir = TempDir::new()?; + + // Canonicalize the tempdir path because macos uses symlinks for tempdirs + // and that doesn't play well with our snapshot filtering. + // Simplify with dunce because otherwise we get UNC paths on Windows. + let project_dir = dunce::simplified( + &temp_dir + .path() + .canonicalize() + .context("Failed to canonicalize project path")?, + ) + .to_path_buf(); + + let mut settings = insta::Settings::clone_current(); + settings.add_filter(&tempdir_filter(&project_dir), "/"); + settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1"); + + let settings_scope = settings.bind_to_scope(); + + Ok(Self { + project_dir, + _temp_dir: temp_dir, + _settings_scope: settings_scope, + }) + } + + pub(crate) fn with_files<'a>( + files: impl IntoIterator, + ) -> anyhow::Result { + let case = Self::new()?; + case.write_files(files)?; + Ok(case) + } + + pub(crate) fn with_file(path: impl AsRef, content: &str) -> anyhow::Result { + let case = Self::new()?; + case.write_file(path, content)?; + Ok(case) + } + + pub(crate) fn write_files<'a>( + &self, + files: impl IntoIterator, + ) -> anyhow::Result<()> { + for (path, content) in files { + self.write_file(path, content)?; + } + + Ok(()) + } + + pub(crate) fn write_file(&self, path: impl AsRef, content: &str) -> anyhow::Result<()> { + let path = path.as_ref(); + let path = self.project_dir.join(path); + + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("Failed to create directory `{}`", parent.display()))?; + } + std::fs::write(&path, &*ruff_python_trivia::textwrap::dedent(content)) + .with_context(|| format!("Failed to write file `{path}`", path = path.display()))?; + + Ok(()) + } + + pub(crate) fn root(&self) -> &Path { + &self.project_dir + } + + pub(crate) fn command(&self) -> Command { + let mut command = Command::new(get_cargo_bin("ty")); + command.current_dir(&self.project_dir).arg("check"); + + // Unset environment variables that can affect test behavior + command.env_remove("VIRTUAL_ENV"); + command.env_remove("CONDA_PREFIX"); + + command + } +} + +fn tempdir_filter(path: &Path) -> String { + format!(r"{}\\?/?", regex::escape(path.to_str().unwrap())) +} diff --git a/crates/ty/tests/cli/python_environment.rs b/crates/ty/tests/cli/python_environment.rs new file mode 100644 index 0000000000..ab9192242d --- /dev/null +++ b/crates/ty/tests/cli/python_environment.rs @@ -0,0 +1,774 @@ +use insta_cmd::assert_cmd_snapshot; +use ruff_python_ast::PythonVersion; + +use crate::CliTest; + +/// Specifying an option on the CLI should take precedence over the same setting in the +/// project's configuration. Here, this is tested for the Python version. +#[test] +fn config_override_python_version() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python-version = "3.11" + "#, + ), + ( + "test.py", + r#" + import sys + + # Access `sys.last_exc` that was only added in Python 3.12 + print(sys.last_exc) + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-attribute]: Type `` has no attribute `last_exc` + --> test.py:5:7 + | + 4 | # Access `sys.last_exc` that was only added in Python 3.12 + 5 | print(sys.last_exc) + | ^^^^^^^^^^^^ + | + info: rule `unresolved-attribute` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + assert_cmd_snapshot!(case.command().arg("--python-version").arg("3.12"), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// Same as above, but for the Python platform. +#[test] +fn config_override_python_platform() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python-platform = "linux" + "#, + ), + ( + "test.py", + r#" + import sys + from typing_extensions import reveal_type + + reveal_type(sys.platform) + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: true + exit_code: 0 + ----- stdout ----- + info[revealed-type]: Revealed type + --> test.py:5:13 + | + 3 | from typing_extensions import reveal_type + 4 | + 5 | reveal_type(sys.platform) + | ^^^^^^^^^^^^ `Literal["linux"]` + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + assert_cmd_snapshot!(case.command().arg("--python-platform").arg("all"), @r" + success: true + exit_code: 0 + ----- stdout ----- + info[revealed-type]: Revealed type + --> test.py:5:13 + | + 3 | from typing_extensions import reveal_type + 4 | + 5 | reveal_type(sys.platform) + | ^^^^^^^^^^^^ `LiteralString` + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn config_file_annotation_showing_where_python_version_set_typing_error() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python-version = "3.8" + "#, + ), + ( + "test.py", + r#" + aiter + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `aiter` used when not defined + --> test.py:2:1 + | + 2 | aiter + | ^^^^^ + | + info: `aiter` was added as a builtin in Python 3.10 + info: Python 3.8 was assumed when resolving types + --> pyproject.toml:3:18 + | + 2 | [tool.ty.environment] + 3 | python-version = "3.8" + | ^^^^^ Python 3.8 assumed due to this configuration setting + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `aiter` used when not defined + --> test.py:2:1 + | + 2 | aiter + | ^^^^^ + | + info: `aiter` was added as a builtin in Python 3.10 + info: Python 3.9 was assumed when resolving types because it was specified on the command line + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python = "venv" + "#, + ), + ( + "venv/pyvenv.cfg", + r#" + version = 3.8 + home = foo/bar/bin + "#, + ), + if cfg!(target_os = "windows") { + ("foo/bar/bin/python.exe", "") + } else { + ("foo/bar/bin/python", "") + }, + if cfg!(target_os = "windows") { + ("venv/Lib/site-packages/foo.py", "") + } else { + ("venv/lib/python3.8/site-packages/foo.py", "") + }, + ("test.py", "aiter"), + ])?; + + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `aiter` used when not defined + --> test.py:1:1 + | + 1 | aiter + | ^^^^^ + | + info: `aiter` was added as a builtin in Python 3.10 + info: Python 3.8 was assumed when resolving types because of your virtual environment + --> venv/pyvenv.cfg:2:11 + | + 2 | version = 3.8 + | ^^^ Python version inferred from virtual environment metadata file + 3 | home = foo/bar/bin + | + info: No Python version was specified on the command line or in a configuration file + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python = "venv" + "#, + ), + ( + "venv/pyvenv.cfg", + r#"home = foo/bar/bin + + + version = 3.8"#, + ), + if cfg!(target_os = "windows") { + ("foo/bar/bin/python.exe", "") + } else { + ("foo/bar/bin/python", "") + }, + if cfg!(target_os = "windows") { + ("venv/Lib/site-packages/foo.py", "") + } else { + ("venv/lib/python3.8/site-packages/foo.py", "") + }, + ("test.py", "aiter"), + ])?; + + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `aiter` used when not defined + --> test.py:1:1 + | + 1 | aiter + | ^^^^^ + | + info: `aiter` was added as a builtin in Python 3.10 + info: Python 3.8 was assumed when resolving types because of your virtual environment + --> venv/pyvenv.cfg:4:23 + | + 4 | version = 3.8 + | ^^^ Python version inferred from virtual environment metadata file + | + info: No Python version was specified on the command line or in a configuration file + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn config_file_annotation_showing_where_python_version_set_syntax_error() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [project] + requires-python = ">=3.8" + "#, + ), + ( + "test.py", + r#" + match object(): + case int(): + pass + case _: + pass + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 1 + ----- stdout ----- + error[invalid-syntax] + --> test.py:2:1 + | + 2 | match object(): + | ^^^^^ Cannot use `match` statement on Python 3.8 (syntax was added in Python 3.10) + 3 | case int(): + 4 | pass + | + info: Python 3.8 was assumed when parsing syntax + --> pyproject.toml:3:19 + | + 2 | [project] + 3 | requires-python = ">=3.8" + | ^^^^^^^ Python 3.8 assumed due to this configuration setting + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[invalid-syntax] + --> test.py:2:1 + | + 2 | match object(): + | ^^^^^ Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10) + 3 | case int(): + 4 | pass + | + info: Python 3.9 was assumed when parsing syntax because it was specified on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn python_cli_argument_virtual_environment() -> anyhow::Result<()> { + let path_to_executable = if cfg!(windows) { + "my-venv/Scripts/python.exe" + } else { + "my-venv/bin/python" + }; + + let other_venv_path = "my-venv/foo/some_other_file.txt"; + + let case = CliTest::with_files([ + ("test.py", ""), + ( + if cfg!(windows) { + "my-venv/Lib/site-packages/foo.py" + } else { + "my-venv/lib/python3.13/site-packages/foo.py" + }, + "", + ), + (path_to_executable, ""), + (other_venv_path, ""), + ])?; + + // Passing a path to the installation works + assert_cmd_snapshot!(case.command().arg("--python").arg("my-venv"), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // And so does passing a path to the executable inside the installation + assert_cmd_snapshot!(case.command().arg("--python").arg(path_to_executable), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // But random other paths inside the installation are rejected + assert_cmd_snapshot!(case.command().arg("--python").arg(other_venv_path), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + ty failed + Cause: Invalid search path settings + Cause: Failed to discover the site-packages directory: Invalid `--python` argument `/my-venv/foo/some_other_file.txt`: does not point to a Python executable or a directory on disk + "); + + // And so are paths that do not exist on disk + assert_cmd_snapshot!(case.command().arg("--python").arg("not-a-directory-or-executable"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + ty failed + Cause: Invalid search path settings + Cause: Failed to discover the site-packages directory: Invalid `--python` argument `/not-a-directory-or-executable`: does not point to a Python executable or a directory on disk + "); + + Ok(()) +} + +#[test] +fn python_cli_argument_system_installation() -> anyhow::Result<()> { + let path_to_executable = if cfg!(windows) { + "Python3.11/python.exe" + } else { + "Python3.11/bin/python" + }; + + let case = CliTest::with_files([ + ("test.py", ""), + ( + if cfg!(windows) { + "Python3.11/Lib/site-packages/foo.py" + } else { + "Python3.11/lib/python3.11/site-packages/foo.py" + }, + "", + ), + (path_to_executable, ""), + ])?; + + // Passing a path to the installation works + assert_cmd_snapshot!(case.command().arg("--python").arg("Python3.11"), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // And so does passing a path to the executable inside the installation + assert_cmd_snapshot!(case.command().arg("--python").arg(path_to_executable), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +#[test] +fn config_file_broken_python_setting() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [project] + name = "test" + version = "0.1.0" + description = "Some description" + readme = "README.md" + requires-python = ">=3.13" + dependencies = [] + + [tool.ty.environment] + python = "not-a-directory-or-executable" + "#, + ), + ("test.py", ""), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + ty failed + Cause: Invalid search path settings + Cause: Failed to discover the site-packages directory: Invalid `environment.python` setting + + --> Invalid setting in configuration file `/pyproject.toml` + | + 9 | + 10 | [tool.ty.environment] + 11 | python = "not-a-directory-or-executable" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not point to a Python executable or a directory on disk + | + "#); + + Ok(()) +} + +#[test] +fn config_file_python_setting_directory_with_no_site_packages() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python = "directory-but-no-site-packages" + "#, + ), + ("directory-but-no-site-packages/lib/foo.py", ""), + ("test.py", ""), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + ty failed + Cause: Invalid search path settings + Cause: Failed to discover the site-packages directory: Invalid `environment.python` setting + + --> Invalid setting in configuration file `/pyproject.toml` + | + 1 | + 2 | [tool.ty.environment] + 3 | python = "directory-but-no-site-packages" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Could not find a `site-packages` directory for this Python installation/executable + | + "#); + + Ok(()) +} + +// This error message is never emitted on Windows, because Windows installations have simpler layouts +#[cfg(not(windows))] +#[test] +fn unix_system_installation_with_no_lib_directory() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.environment] + python = "directory-but-no-site-packages" + "#, + ), + ("directory-but-no-site-packages/foo.py", ""), + ("test.py", ""), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + ty failed + Cause: Invalid search path settings + Cause: Failed to discover the site-packages directory: Failed to iterate over the contents of the `lib` directory of the Python installation + + --> Invalid setting in configuration file `/pyproject.toml` + | + 1 | + 2 | [tool.ty.environment] + 3 | python = "directory-but-no-site-packages" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + "#); + + Ok(()) +} + +#[test] +fn defaults_to_a_new_python_version() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "ty.toml", + &*format!( + r#" + [environment] + python-version = "{}" + python-platform = "linux" + "#, + PythonVersion::default() + ), + ), + ( + "main.py", + r#" + import os + + os.grantpt(1) # only available on unix, Python 3.13 or newer + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-attribute]: Type `` has no attribute `grantpt` + --> main.py:4:1 + | + 2 | import os + 3 | + 4 | os.grantpt(1) # only available on unix, Python 3.13 or newer + | ^^^^^^^^^^ + | + info: rule `unresolved-attribute` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // Use default (which should be latest supported) + let case = CliTest::with_files([ + ( + "ty.toml", + r#" + [environment] + python-platform = "linux" + "#, + ), + ( + "main.py", + r#" + import os + + os.grantpt(1) # only available on unix, Python 3.13 or newer + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// The `site-packages` directory is used by ty for external import. +/// Ty does the following checks to discover the `site-packages` directory in the order: +/// 1) If `VIRTUAL_ENV` environment variable is set +/// 2) If `CONDA_PREFIX` environment variable is set +/// 3) If a `.venv` directory exists at the project root +/// +/// This test is aiming at validating the logic around `CONDA_PREFIX`. +/// +/// A conda-like environment file structure is used +/// We test by first not setting the `CONDA_PREFIX` and expect a fail. +/// Then we test by setting `CONDA_PREFIX` to `conda-env` and expect a pass. +/// +/// ├── project +/// │ └── test.py +/// └── conda-env +/// └── lib +/// └── python3.13 +/// └── site-packages +/// └── package1 +/// └── __init__.py +/// +/// test.py imports package1 +/// And the command is run in the `child` directory. +#[test] +fn check_conda_prefix_var_to_resolve_path() -> anyhow::Result<()> { + let conda_package1_path = if cfg!(windows) { + "conda-env/Lib/site-packages/package1/__init__.py" + } else { + "conda-env/lib/python3.13/site-packages/package1/__init__.py" + }; + + let case = CliTest::with_files([ + ( + "project/test.py", + r#" + import package1 + "#, + ), + ( + conda_package1_path, + r#" + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command().current_dir(case.root().join("project")), @r" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-import]: Cannot resolve imported module `package1` + --> test.py:2:8 + | + 2 | import package1 + | ^^^^^^^^ + | + info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment + info: rule `unresolved-import` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + // do command : CONDA_PREFIX=/conda_env + assert_cmd_snapshot!(case.command().current_dir(case.root().join("project")).env("CONDA_PREFIX", case.root().join("conda-env")), @r" + success: true + exit_code: 0 + ----- stdout ----- + All checks passed! + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} diff --git a/crates/ty/tests/cli/rule_selection.rs b/crates/ty/tests/cli/rule_selection.rs new file mode 100644 index 0000000000..9269790e3a --- /dev/null +++ b/crates/ty/tests/cli/rule_selection.rs @@ -0,0 +1,292 @@ +use insta_cmd::assert_cmd_snapshot; + +use crate::CliTest; + +/// The rule severity can be changed in the configuration file +#[test] +fn configuration_rule_severity() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + y = 4 / 0 + + for a in range(0, int(y)): + x = a + + prin(x) # unresolved-reference + "#, + )?; + + // Assert that there's an `unresolved-reference` diagnostic (error). + assert_cmd_snapshot!(case.command(), @r###" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `prin` used when not defined + --> test.py:7:1 + | + 5 | x = a + 6 | + 7 | prin(x) # unresolved-reference + | ^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "###); + + case.write_file( + "pyproject.toml", + r#" + [tool.ty.rules] + division-by-zero = "warn" # promote to warn + unresolved-reference = "ignore" + "#, + )?; + + assert_cmd_snapshot!(case.command(), @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> test.py:2:5 + | + 2 | y = 4 / 0 + | ^^^^^ + 3 | + 4 | for a in range(0, int(y)): + | + info: rule `division-by-zero` was selected in the configuration file + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +} + +/// The rule severity can be changed using `--ignore`, `--warn`, and `--error` +#[test] +fn cli_rule_severity() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + import does_not_exit + + y = 4 / 0 + + for a in range(0, int(y)): + x = a + + prin(x) # unresolved-reference + "#, + )?; + + // Assert that there's an `unresolved-reference` diagnostic (error) + // and an unresolved-import (error) diagnostic by default. + assert_cmd_snapshot!(case.command(), @r###" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-import]: Cannot resolve imported module `does_not_exit` + --> test.py:2:8 + | + 2 | import does_not_exit + | ^^^^^^^^^^^^^ + 3 | + 4 | y = 4 / 0 + | + info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment + info: rule `unresolved-import` is enabled by default + + error[unresolved-reference]: Name `prin` used when not defined + --> test.py:9:1 + | + 7 | x = a + 8 | + 9 | prin(x) # unresolved-reference + | ^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "###); + + assert_cmd_snapshot!( + case + .command() + .arg("--ignore") + .arg("unresolved-reference") + .arg("--warn") + .arg("division-by-zero") + .arg("--warn") + .arg("unresolved-import"), + @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[unresolved-import]: Cannot resolve imported module `does_not_exit` + --> test.py:2:8 + | + 2 | import does_not_exit + | ^^^^^^^^^^^^^ + 3 | + 4 | y = 4 / 0 + | + info: make sure your Python environment is properly configured: https://github.com/astral-sh/ty/blob/main/docs/README.md#python-environment + info: rule `unresolved-import` was selected on the command line + + warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> test.py:4:5 + | + 2 | import does_not_exit + 3 | + 4 | y = 4 / 0 + | ^^^^^ + 5 | + 6 | for a in range(0, int(y)): + | + info: rule `division-by-zero` was selected on the command line + + Found 2 diagnostics + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + " + ); + + Ok(()) +} + +/// The rule severity can be changed using `--ignore`, `--warn`, and `--error` and +/// values specified last override previous severities. +#[test] +fn cli_rule_severity_precedence() -> anyhow::Result<()> { + let case = CliTest::with_file( + "test.py", + r#" + y = 4 / 0 + + for a in range(0, int(y)): + x = a + + prin(x) # unresolved-reference + "#, + )?; + + // Assert that there's a `unresolved-reference` diagnostic (error) by default. + assert_cmd_snapshot!(case.command(), @r###" + success: false + exit_code: 1 + ----- stdout ----- + error[unresolved-reference]: Name `prin` used when not defined + --> test.py:7:1 + | + 5 | x = a + 6 | + 7 | prin(x) # unresolved-reference + | ^^^^ + | + info: rule `unresolved-reference` is enabled by default + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "###); + + assert_cmd_snapshot!( + case + .command() + .arg("--warn") + .arg("unresolved-reference") + .arg("--warn") + .arg("division-by-zero") + .arg("--ignore") + .arg("unresolved-reference"), + @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[division-by-zero]: Cannot divide object of type `Literal[4]` by zero + --> test.py:2:5 + | + 2 | y = 4 / 0 + | ^^^^^ + 3 | + 4 | for a in range(0, int(y)): + | + info: rule `division-by-zero` was selected on the command line + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + " + ); + + Ok(()) +} + +/// ty warns about unknown rules specified in a configuration file +#[test] +fn configuration_unknown_rules() -> anyhow::Result<()> { + let case = CliTest::with_files([ + ( + "pyproject.toml", + r#" + [tool.ty.rules] + division-by-zer = "warn" # incorrect rule name + "#, + ), + ("test.py", "print(10)"), + ])?; + + assert_cmd_snapshot!(case.command(), @r#" + success: true + exit_code: 0 + ----- stdout ----- + warning[unknown-rule] + --> pyproject.toml:3:1 + | + 2 | [tool.ty.rules] + 3 | division-by-zer = "warn" # incorrect rule name + | ^^^^^^^^^^^^^^^ Unknown lint rule `division-by-zer` + | + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "#); + + Ok(()) +} + +/// ty warns about unknown rules specified in a CLI argument +#[test] +fn cli_unknown_rules() -> anyhow::Result<()> { + let case = CliTest::with_file("test.py", "print(10)")?; + + assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r" + success: true + exit_code: 0 + ----- stdout ----- + warning[unknown-rule]: Unknown lint rule `division-by-zer` + + Found 1 diagnostic + + ----- stderr ----- + WARN ty is pre-release software and not ready for production use. Expect to encounter bugs, missing features, and fatal errors. + "); + + Ok(()) +}