Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement rye test command built on pytest #847

Merged
merged 26 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a6da7ef
First draft for test command.
hyyking Feb 25, 2024
3a286e4
install pytest into dev-dependencies if it is missing
hyyking Feb 25, 2024
44aa3be
add rye-devtools test folder
hyyking Feb 25, 2024
8540729
update test command to include rye test
hyyking Feb 25, 2024
4d8c33d
ignore workspace root if the package is a workspace
hyyking Feb 25, 2024
9b8570f
add ignore and no-capture arguments to test args
hyyking Feb 25, 2024
69b8c64
document test::execute
hyyking Feb 25, 2024
5a40bda
Merge branch 'main' of https://github.com/mitsuhiko/rye
hyyking Mar 3, 2024
4313a28
fix: ignore target directory by default
hyyking Mar 3, 2024
845c4ca
docs: add rye test documentation
hyyking Mar 3, 2024
73cd886
Merge branch 'main' of https://github.com/mitsuhiko/rye
hyyking Mar 5, 2024
98c10d2
Merge branch 'main' into hyyking/main
mitsuhiko Mar 6, 2024
cc02806
Alternative approach to pytest
mitsuhiko Mar 7, 2024
15f6e1d
Update documentation
mitsuhiko Mar 7, 2024
5d28494
Do not run `rye test` in Makefile
mitsuhiko Mar 7, 2024
6bf7405
Added basic test for test command
mitsuhiko Mar 7, 2024
b111475
Add more tests
mitsuhiko Mar 7, 2024
736c15a
Update docs on tests
mitsuhiko Mar 7, 2024
e4d9f20
Ensure consistent terminal width for pytest
mitsuhiko Mar 7, 2024
79c3fd5
Fix tests
mitsuhiko Mar 7, 2024
18c62b2
Fix columns
mitsuhiko Mar 7, 2024
0c9e258
Restore visibility of sync mod
mitsuhiko Mar 7, 2024
8167cb4
Fix tests between platforms
mitsuhiko Mar 7, 2024
628f340
Fix a bug in a test
mitsuhiko Mar 7, 2024
4730688
Added changelog entry
mitsuhiko Mar 7, 2024
8b12f90
Sort projects consistently in location
mitsuhiko Mar 7, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ _Unreleased_

- `rye add` now retains version and URL for the requirements when `uv` is used. #846

- Added a `rye test` command which invokes `pytest`. #847

<!-- released start -->

## 0.27.0
Expand Down
58 changes: 58 additions & 0 deletions docs/guide/commands/test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# `test`

+++ X.X.X

Run the test suites of the project. At the moment this always runs `pytest`.
Note that `pytest` must be installed into the virtual env unlike `ruff`
which is used behind the scenes automatically for linting and formatting.
Thus in order to use this, you need to declare `pytest` as dev dependency.

```
$ rye add --dev pytest
```

It's recommended to place tests in a folder called `tests` adjacent to the
`src` folder of your project.

For more information about how to use pytest, have a look at the
[Pytest Documentation](https://docs.pytest.org/en/8.0.x/).

## Example

Run the test suite:

```
$ rye test
platform win32 -- Python 3.11.1, pytest-8.0.2, pluggy-1.4.0
rootdir: /Users/john/Development/stuff
plugins: anyio-4.3.0
collected 1 item

stuff/tests/test_batch.py . [100%]
```

## Arguments

* `[EXTRA_ARGS]...` Extra arguments to the test runner.

These arguments are forwarded directly to the underlying test runner (currently
always `pytest`). Note that extra arguments must be separated from other arguments
with the `--` marker.

## Options

* `-a, --all`: Lint all packages in the workspace

* `-p, --package <PACKAGE>`: Run the test suite of a specific package

* `--pyproject <PYPROJECT_TOML>`: Use this `pyproject.toml` file

* `-v, --verbose`: Enables verbose diagnostics

* `-q, --quiet`: Turns off all output

* `-i, --ignore`: Ignore the specified directory

* `-s`, `--no-capture`: Disable stdout/stderr capture for the test runner

* `-h, --help`: Print help (see a summary with '-h')
4 changes: 3 additions & 1 deletion rye-devtools/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ build-backend = "hatchling.build"

[tool.rye]
managed = true
dev-dependencies = []
dev-dependencies = [
"pytest==8.0.2",
]

[tool.hatch.metadata]
allow-direct-references = true
Expand Down
5 changes: 5 additions & 0 deletions rye-devtools/tests/test_batch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from rye_devtools.find_downloads import batched


def test_batched():
assert list(batched("ABCDEFG", 3)) == [tuple("ABC"), tuple("DEF"), tuple("G")]
3 changes: 3 additions & 0 deletions rye/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ mod rye;
mod shim;
mod show;
mod sync;
mod test;
mod toolchain;
mod tools;
mod uninstall;
Expand Down Expand Up @@ -67,6 +68,7 @@ enum Command {
Run(run::Args),
Show(show::Args),
Sync(sync::Args),
Test(test::Args),
Toolchain(toolchain::Args),
Tools(tools::Args),
#[command(name = "self")]
Expand Down Expand Up @@ -125,6 +127,7 @@ pub fn execute() -> Result<(), Error> {
Command::Run(cmd) => run::execute(cmd),
Command::Show(cmd) => show::execute(cmd),
Command::Sync(cmd) => sync::execute(cmd),
Command::Test(cmd) => test::execute(cmd),
Command::Toolchain(cmd) => toolchain::execute(cmd),
Command::Tools(cmd) => tools::execute(cmd),
Command::Rye(cmd) => rye::execute(cmd),
Expand Down
151 changes: 151 additions & 0 deletions rye/src/cli/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use std::env::consts::EXE_EXTENSION;
use std::ffi::OsString;
use std::path::PathBuf;
use std::process::Command;

use anyhow::{bail, Error};
use clap::Parser;
use console::style;
use same_file::is_same_file;

use crate::config::Config;
use crate::consts::VENV_BIN;
use crate::pyproject::{locate_projects, normalize_package_name, DependencyKind, PyProject};
use crate::sync::autosync;
use crate::utils::{CommandOutput, QuietExit};

/// Run the tests on the project.
///
/// Today this will always run `pytest` for all projects.
#[derive(Parser, Debug)]
pub struct Args {
/// Perform the operation on all packages
#[arg(short, long)]
all: bool,
/// Perform the operation on a specific package
#[arg(short, long)]
package: Vec<String>,
/// Use this pyproject.toml file
#[arg(long, value_name = "PYPROJECT_TOML")]
pyproject: Option<PathBuf>,
// Disable test output capture to stdout
#[arg(long = "no-capture", short = 's')]
no_capture: bool,
/// Enables verbose diagnostics.
#[arg(short, long)]
verbose: bool,
/// Turns off all output.
#[arg(short, long, conflicts_with = "verbose")]
quiet: bool,
/// Extra arguments to pytest
#[arg(last = true)]
extra_args: Vec<OsString>,
}

pub fn execute(cmd: Args) -> Result<(), Error> {
let output = CommandOutput::from_quiet_and_verbose(cmd.quiet, cmd.verbose);
let project = PyProject::load_or_discover(cmd.pyproject.as_deref())?;

let mut failed_with = None;

// when working with workspaces we always want to know what other projects exist.
// for that we locate all those projects and their paths. This is later used to
// prevent accidentally recursing into the wrong projects.
let project_roots = if let Some(workspace) = project.workspace() {
workspace
.iter_projects()
.filter_map(|x| x.ok())
.map(|x| x.root_path().to_path_buf())
.collect()
} else {
vec![project.root_path().to_path_buf()]
};

let pytest = project
.venv_path()
.join(VENV_BIN)
.join("pytest")
.with_extension(EXE_EXTENSION);

let projects = locate_projects(project, cmd.all, &cmd.package[..])?;

if !pytest.is_file() {
let has_pytest = has_pytest_dependency(&projects)?;
if has_pytest {
if Config::current().autosync() {
autosync(&projects[0], output)?;
} else {
bail!("pytest not installed but in dependencies. Run `rye sync`.")
}
} else {
bail!("pytest not installed. Run `rye add --dev pytest`");
}
}

for (idx, project) in projects.iter().enumerate() {
if output != CommandOutput::Quiet {
if idx > 0 {
println!();
}
println!(
"Running tests for {} ({})",
style(project.name().unwrap_or("<unknown>")).cyan(),
style(project.root_path().display()).dim()
);
}

let mut pytest_cmd = Command::new(&pytest);
if cmd.no_capture || output == CommandOutput::Quiet {
pytest_cmd.arg("--capture=no");
}
match output {
CommandOutput::Normal => {}
CommandOutput::Verbose => {
pytest_cmd.arg("-q");
}
CommandOutput::Quiet => {
pytest_cmd.arg("-v");
}
}
pytest_cmd.args(&cmd.extra_args);
pytest_cmd
.arg("--rootdir")
.arg(project.root_path().as_os_str())
.current_dir(project.root_path());

// always ignore projects that are nested but not selected.
for path in &project_roots {
if !is_same_file(path, project.root_path()).unwrap_or(false) {
pytest_cmd.arg("--ignore").arg(path.as_os_str());
}
}

let status = pytest_cmd.status()?;
if !status.success() {
failed_with = Some(status.code().unwrap_or(1));
}
}

if let Some(code) = failed_with {
Err(Error::new(QuietExit(code)))
} else {
Ok(())
}
}

/// Does any of those projects have a pytest dependency?
fn has_pytest_dependency(projects: &[PyProject]) -> Result<bool, Error> {
for project in projects {
for dep in project
.iter_dependencies(DependencyKind::Dev)
.chain(project.iter_dependencies(DependencyKind::Normal))
{
if let Ok(req) = dep.expand(|name| std::env::var(name).ok()) {
if normalize_package_name(&req.name) == "pytest" {
return Ok(true);
}
}
}
}
Ok(false)
}
3 changes: 3 additions & 0 deletions rye/src/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1412,5 +1412,8 @@ pub fn locate_projects(
}
}
}

projects.sort_by(|a, b| a.name().cmp(&b.name()));

Ok(projects)
}
Loading
Loading