Skip to content

Commit

Permalink
python: Add runnable unittest tasks (zed-industries#12451)
Browse files Browse the repository at this point in the history
Add runnable tasks for Python, starting with `unittest` from the
standard library. Both `TestCase`s (classes meant to be a unit of
testing) and individual test functions in a `TestCase` will have
runnable icons. For completeness, I also included a task that will run
`unittest` on the current file.

The implementation follows the `unittest` CLI. The unittest module can
be used from the command line to run tests from modules, classes or even
individual test methods:

```
python -m unittest test_module.TestClass
python -m unittest test_module.TestClass.test_method
```

```python
import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)

if __name__ == '__main__':
    unittest.main()
```

From the snippet provided by `unittest` docs, a user may want to run
test_split independently of the other test functions in the test case.
Hence, I decided to make each test function runnable despite `TestCase`s
being the unit of testing.

## Example of running a `TestCase`
<img width="600" alt="image"
src="https://github.com/zed-industries/zed/assets/16619392/7be38b71-9d51-4b44-9840-f819502d600a">

## Example of running a test function in a `TestCase`
<img width="600" alt="image"
src="https://github.com/zed-industries/zed/assets/16619392/f0b6274c-4fa7-424e-a0f5-1dc723842046">

`unittest` will also run the `setUp` and `tearDown` fixtures.

Eventually, I want to add the more commonly used `pytest` runnables
(perhaps as an extension instead).

Release Notes:

- Added runnable tasks for Python `unittest`.
([zed-industries#12080](zed-industries#12080)).
  • Loading branch information
rayduck authored Jun 1, 2024
1 parent f0d9795 commit 95e360b
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 22 deletions.
8 changes: 3 additions & 5 deletions crates/languages/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ use gpui::{AppContext, UpdateGlobal};
use json::json_task_context;
pub use language::*;
use node_runtime::NodeRuntime;
use python::PythonContextProvider;
use rust_embed::RustEmbed;
use settings::SettingsStore;
use smol::stream::StreamExt;
use std::{str, sync::Arc};
use typescript::typescript_task_context;
use util::{asset_str, ResultExt};

use crate::{
bash::bash_task_context, go::GoContextProvider, python::python_task_context,
rust::RustContextProvider,
};
use crate::{bash::bash_task_context, go::GoContextProvider, rust::RustContextProvider};

mod bash;
mod c;
Expand Down Expand Up @@ -130,7 +128,7 @@ pub fn init(
vec![Arc::new(python::PythonLspAdapter::new(
node_runtime.clone(),
))],
python_task_context()
PythonContextProvider
);
language!(
"rust",
Expand Down
105 changes: 88 additions & 17 deletions crates/languages/src/python.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use anyhow::Result;
use async_trait::async_trait;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use language::{ContextProvider, LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
use std::{
any::Any,
borrow::Cow,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
Expand Down Expand Up @@ -182,21 +182,92 @@ async fn get_cached_server_binary(
}
}

pub(super) fn python_task_context() -> ContextProviderWithTasks {
ContextProviderWithTasks::new(TaskTemplates(vec![
TaskTemplate {
label: "execute selection".to_owned(),
command: "python3".to_owned(),
args: vec!["-c".to_owned(), VariableName::SelectedText.template_value()],
..TaskTemplate::default()
},
TaskTemplate {
label: format!("run '{}'", VariableName::File.template_value()),
command: "python3".to_owned(),
args: vec![VariableName::File.template_value()],
..TaskTemplate::default()
},
]))
pub(crate) struct PythonContextProvider;

const PYTHON_UNITTEST_TARGET_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("PYTHON_UNITTEST_TARGET"));

impl ContextProvider for PythonContextProvider {
fn build_context(
&self,
variables: &task::TaskVariables,
_location: &project::Location,
_cx: &mut gpui::AppContext,
) -> Result<task::TaskVariables> {
let python_module_name = python_module_name_from_relative_path(
variables.get(&VariableName::RelativeFile).unwrap_or(""),
);
let unittest_class_name =
variables.get(&VariableName::Custom(Cow::Borrowed("_unittest_class_name")));
let unittest_method_name = variables.get(&VariableName::Custom(Cow::Borrowed(
"_unittest_method_name",
)));

let unittest_target_str = match (unittest_class_name, unittest_method_name) {
(Some(class_name), Some(method_name)) => {
format!("{}.{}.{}", python_module_name, class_name, method_name)
}
(Some(class_name), None) => format!("{}.{}", python_module_name, class_name),
(None, None) => python_module_name,
(None, Some(_)) => return Ok(task::TaskVariables::default()), // should never happen, a TestCase class is the unit of testing
};

let unittest_target = (
PYTHON_UNITTEST_TARGET_TASK_VARIABLE.clone(),
unittest_target_str,
);

Ok(task::TaskVariables::from_iter([unittest_target]))
}

fn associated_tasks(&self) -> Option<TaskTemplates> {
Some(TaskTemplates(vec![
TaskTemplate {
label: "execute selection".to_owned(),
command: "python3".to_owned(),
args: vec!["-c".to_owned(), VariableName::SelectedText.template_value()],
..TaskTemplate::default()
},
TaskTemplate {
label: format!("run '{}'", VariableName::File.template_value()),
command: "python3".to_owned(),
args: vec![VariableName::File.template_value()],
..TaskTemplate::default()
},
TaskTemplate {
label: format!("unittest '{}'", VariableName::File.template_value()),
command: "python3".to_owned(),
args: vec![
"-m".to_owned(),
"unittest".to_owned(),
VariableName::File.template_value(),
],
..TaskTemplate::default()
},
TaskTemplate {
label: "unittest $ZED_CUSTOM_PYTHON_UNITTEST_TARGET".to_owned(),
command: "python3".to_owned(),
args: vec![
"-m".to_owned(),
"unittest".to_owned(),
"$ZED_CUSTOM_PYTHON_UNITTEST_TARGET".to_owned(),
],
tags: vec![
"python-unittest-class".to_owned(),
"python-unittest-method".to_owned(),
],
..TaskTemplate::default()
},
]))
}
}

fn python_module_name_from_relative_path(relative_path: &str) -> String {
let path_with_dots = relative_path.replace('/', ".");
path_with_dots
.strip_suffix(".py")
.unwrap_or(&path_with_dots)
.to_string()
}

#[cfg(test)]
Expand Down
31 changes: 31 additions & 0 deletions crates/languages/src/python/runnables.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
; subclasses of unittest.TestCase or TestCase
(
(class_definition
name: (identifier) @run @_unittest_class_name
superclasses: (argument_list
[(identifier) @_superclass
(attribute (identifier) @_superclass)]
)
(#eq? @_superclass "TestCase")
) @python-unittest-class
(#set! tag python-unittest-class)
)

; test methods whose names start with `test` in a TestCase
(
(class_definition
name: (identifier) @_unittest_class_name
superclasses: (argument_list
[(identifier) @_superclass
(attribute (identifier) @_superclass)]
)
(#eq? @_superclass "TestCase")
body: (block
(function_definition
name: (identifier) @run @_unittest_method_name
(#match? @_unittest_method_name "^test.*")
) @python-unittest-method
(#set! tag python-unittest-method)
)
)
)

0 comments on commit 95e360b

Please sign in to comment.