diff --git a/Cargo.lock b/Cargo.lock index ba5ccea..c3fe422 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "anyhow" version = "1.0.82" @@ -32,6 +41,16 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "cc" +version = "1.2.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.1" @@ -64,6 +83,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + [[package]] name = "flate2" version = "1.1.2" @@ -417,6 +442,35 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + [[package]] name = "ryu" version = "1.0.17" @@ -458,12 +512,19 @@ version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ + "indexmap", "itoa", "memchr", "ryu", "serde", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "slab" version = "0.4.10" @@ -491,6 +552,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" + [[package]] name = "syn" version = "2.0.104" @@ -529,6 +596,36 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" +[[package]] +name = "tree-sitter" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" +dependencies = [ + "cc", + "regex", + "regex-syntax", + "serde_json", + "streaming-iterator", + "tree-sitter-language", +] + +[[package]] +name = "tree-sitter-language" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4013970217383f67b18aef68f6fb2e8d409bc5755227092d32efb0422ba24b8" + +[[package]] +name = "tree-sitter-swift" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef216011c3e3df4fa864736f347cb8d509b1066cf0c8549fb1fd81ac9832e59" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -746,6 +843,9 @@ version = "0.4.4" dependencies = [ "serde", "serde_json", + "streaming-iterator", + "tree-sitter", + "tree-sitter-swift", "zed_extension_api", ] diff --git a/Cargo.toml b/Cargo.toml index a9ae686..25289bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,8 @@ crate-type = ["cdylib"] serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" zed_extension_api = "0.6.0" + +[dev-dependencies] +tree-sitter = "0.25" +tree-sitter-swift = "0.7" +streaming-iterator = "0.1" diff --git a/languages/swift/runnables.scm b/languages/swift/runnables.scm index fb9dec4..e73d1fe 100644 --- a/languages/swift/runnables.scm +++ b/languages/swift/runnables.scm @@ -4,6 +4,21 @@ ;; ;; While the tasks defined in this extension don't care which library is used, ;; other tasks built by users might. +;; +;; INDIRECT XCTEST SUBCLASSES: +;; Tree-sitter queries cannot follow inheritance chains semantically, so indirect +;; subclasses (MyTests <- MyTestsBase <- XCTestCase) are not automatically detected. +;; +;; WORKAROUND: Add a // @XCTestClass comment before indirect subclass declarations: +;; +;; class MyTestsBase: XCTestCase { } +;; +;; // @XCTestClass +;; class MyTests: MyTestsBase { +;; func testSomething() { } +;; } +;; +;; This allows the query to recognize and run tests in indirect subclasses. ;; @Suite struct/class ( @@ -88,6 +103,30 @@ (#set! tag swift-xctest-func) ) +;; XCTestCase indirect subclass with @XCTestClass comment annotation +;; This pattern allows users to mark indirect subclasses (MyTests <- MyTestsBase <- XCTestCase) +;; by adding a "// @XCTestClass" comment before the class declaration. +( + (comment) @_marker (#match? @_marker ".*@XCTestClass.*") + (class_declaration + name: (type_identifier) @SWIFT_TEST_CLASS @run + ) @_swift-xctest-class + (#set! tag swift-xctest-class) +) + +;; Test function within comment-annotated XCTest class +( + (comment) @_marker (#match? @_marker ".*@XCTestClass.*") + (class_declaration + name: (type_identifier) @SWIFT_TEST_CLASS + body: (class_body + (function_declaration + name: (simple_identifier) @_name @SWIFT_TEST_FUNC @run (#match? @run "^test") + ) + ) + ) @_swift-xctest-func + (#set! tag swift-xctest-func) +) ;; QuickSpec subclass ( @@ -113,4 +152,4 @@ ) ) @_swift-test-async-spec (#set! tag swift-test-async-spec) -) \ No newline at end of file +) diff --git a/src/runnables_test.rs b/src/runnables_test.rs new file mode 100644 index 0000000..84f142a --- /dev/null +++ b/src/runnables_test.rs @@ -0,0 +1,697 @@ +//! # Runnables Tests +//! +//! Unit tests for the Swift runnables tree-sitter query file (`languages/swift/runnables.scm`). +//! +//! ## Overview +//! +//! The runnables query is used by Zed to identify test functions and test classes in Swift code. +//! The tests validate that the query correctly captures: +//! +//! 1. **Swift Testing Framework** (modern Swift 6+ testing) +//! - `@Suite` annotations on structs and classes +//! - `@Test` annotations on top-level functions +//! - `@Test` annotations on member functions within test suites +//! +//! 2. **XCTest Framework** (traditional Swift testing) +//! - Classes that inherit from `XCTestCase` +//! - Classes marked with `@XCTestCase` comment annotation (for indirect subclasses) +//! - Test methods within XCTest classes (must start with `test` prefix) +//! +//! 3. **Quick/Nimble Framework** (BDD-style testing) +//! - `QuickSpec` subclasses +//! - `AsyncSpec` subclasses +//! +//! ## Running Tests +//! +//! ```bash +//! # Run all tests +//! cargo test --lib +//! +//! # Run only the runnables tests +//! cargo test --lib runnables_test +//! ``` +//! +//! ## Test Structure +//! +//! Each test: +//! 1. Defines a Swift code snippet containing test code +//! 2. Parses the code using tree-sitter-swift +//! 3. Runs the runnables query against the parsed tree +//! 4. Validates that the correct captures are returned with the appropriate tags +//! +//! ## Tags +//! +//! The query assigns tags to different types of test definitions: +//! +//! - `swift-testing-suite` - A struct/class with `@Suite` annotation +//! - `swift-testing-bare-func` - A top-level function with `@Test` annotation +//! - `swift-testing-member-func` - A member function with `@Test` annotation within a suite +//! - `swift-xctest-class` - A class that inherits from `XCTestCase` or is marked with `@XCTestCase` comment +//! - `swift-xctest-func` - A test method within an XCTest class +//! - `swift-test-quick-spec` - A QuickSpec subclass +//! - `swift-test-async-spec` - An AsyncSpec subclass +//! +//! ## Adding New Tests +//! +//! When adding support for new test frameworks or patterns: +//! +//! 1. Add a new test function in this module +//! 2. Create a Swift code snippet that demonstrates the pattern +//! 3. Use `get_captures()` to run the query +//! 4. Assert that the expected tags and names are captured +//! +//! Example: +//! +//! ```rust +//! #[test] +//! fn test_new_framework() { +//! let source = r#" +//! // Your Swift test code here +//! "#; +//! +//! let captures = get_captures(source, get_query()); +//! +//! assert!( +//! captures +//! .iter() +//! .any(|(tag, class, func)| tag == "expected-tag" && class == "ExpectedClass"), +//! "Expected to find the test pattern" +//! ); +//! } +//! ``` + +#[cfg(test)] +mod tests { + use std::sync::OnceLock; + use streaming_iterator::StreamingIterator; + use tree_sitter::{Language, Parser, Query, QueryCursor}; + + const RUNNABLES_QUERY: &str = include_str!("../languages/swift/runnables.scm"); + + // PERFORMANCE NOTE: + // With tree-sitter 0.23 and tree-sitter-swift 0.7, query compilation is significantly + // faster than the previous versions (0.20/0.6). We use OnceLock to cache both the + // language and compiled query across all tests for optimal performance. + + fn get_language() -> &'static Language { + static LANGUAGE: OnceLock = OnceLock::new(); + LANGUAGE.get_or_init(|| tree_sitter_swift::LANGUAGE.into()) + } + + fn get_query() -> &'static Query { + static QUERY: OnceLock = OnceLock::new(); + QUERY.get_or_init(|| Query::new(&get_language(), RUNNABLES_QUERY).unwrap()) + } + + fn setup_parser() -> Parser { + let mut parser = Parser::new(); + parser.set_language(&get_language()).unwrap(); + parser + } + + fn get_captures(source: &str, query: &Query) -> Vec<(String, String, String)> { + let mut parser = setup_parser(); + let tree = parser.parse(source, None).unwrap(); + let mut cursor = QueryCursor::new(); + + let mut results = Vec::new(); + let mut matches = cursor.matches(query, tree.root_node(), source.as_bytes()); + + while let Some(match_) = matches.next() { + let mut tag = String::new(); + let mut class_name = String::new(); + let mut func_name = String::new(); + + for capture in match_.captures { + let capture_name = &query.capture_names()[capture.index as usize]; + let text = capture.node.utf8_text(source.as_bytes()).unwrap(); + + match capture_name.as_ref() { + "SWIFT_TEST_CLASS" => class_name = text.to_string(), + "SWIFT_TEST_FUNC" => func_name = text.to_string(), + _ => {} + } + } + + // Get the tag from pattern properties + if let Some(props) = match_.pattern_index.checked_sub(0) { + for prop in query.property_settings(props as usize) { + if prop.key.as_ref() == "tag" { + if let Some(value) = &prop.value { + tag = value.to_string(); + } + } + } + } + + results.push((tag, class_name, func_name)); + } + + results + } + + #[test] + fn test_swift_testing_suite() { + let source = r#" +@Suite +struct MyTestSuite { + @Test func testSomething() {} +} +"#; + + let captures = get_captures(source, get_query()); + + // Should capture the @Suite struct + assert!( + captures + .iter() + .any(|(tag, class, _)| tag == "swift-testing-suite" && class == "MyTestSuite"), + "Expected to find swift-testing-suite tag for MyTestSuite, got: {:?}", + captures + ); + } + + #[test] + fn test_swift_testing_suite_class() { + let source = r#" +@Suite +class MyTestClass { + @Test func testSomething() {} +} +"#; + + let captures = get_captures(source, get_query()); + + assert!( + captures + .iter() + .any(|(tag, class, _)| tag == "swift-testing-suite" && class == "MyTestClass"), + "Expected to find swift-testing-suite tag for MyTestClass" + ); + } + + #[test] + fn test_swift_testing_bare_func() { + let source = r#" +@Test func testTopLevelFunction() { + // test code +} +"#; + + let captures = get_captures(source, get_query()); + + assert!( + captures + .iter() + .any(|(tag, _, func)| tag == "swift-testing-bare-func" + && func == "testTopLevelFunction"), + "Expected to find swift-testing-bare-func tag for testTopLevelFunction" + ); + } + + #[test] + fn test_swift_testing_member_func() { + let source = r#" +struct TestSuite { + @Test func testMemberFunction() { + // test code + } +} +"#; + + let captures = get_captures(source, get_query()); + + assert!( + captures + .iter() + .any(|(tag, class, func)| tag == "swift-testing-member-func" + && class == "TestSuite" + && func == "testMemberFunction"), + "Expected to find swift-testing-member-func tag" + ); + } + + #[test] + fn test_xctest_class() { + let source = r#" +import XCTest + +class MyTests: XCTestCase { + func testExample() { + XCTAssertTrue(true) + } +} +"#; + + let captures = get_captures(source, get_query()); + + assert!( + captures + .iter() + .any(|(tag, class, _)| tag == "swift-xctest-class" && class == "MyTests"), + "Expected to find swift-xctest-class tag for MyTests" + ); + } + + #[test] + fn test_xctest_func() { + let source = r#" +class MyTests: XCTestCase { + func testExample() { + XCTAssertTrue(true) + } + + func testAnotherThing() { + XCTAssertEqual(1, 1) + } +} +"#; + + let captures = get_captures(source, get_query()); + + assert!( + captures + .iter() + .any(|(tag, class, func)| tag == "swift-xctest-func" + && class == "MyTests" + && func == "testExample"), + "Expected to find swift-xctest-func tag for testExample" + ); + + assert!( + captures + .iter() + .any(|(tag, class, func)| tag == "swift-xctest-func" + && class == "MyTests" + && func == "testAnotherThing"), + "Expected to find swift-xctest-func tag for testAnotherThing" + ); + } + + #[test] + fn test_xctest_func_requires_test_prefix() { + let source = r#" +class MyTests: XCTestCase { + func setUp() { + // setup code + } + + func helperMethod() { + // not a test + } +} +"#; + + let captures = get_captures(source, get_query()); + + // setUp and helperMethod should NOT be captured as they don't start with "test" + assert!( + !captures + .iter() + .any(|(tag, _, func)| tag == "swift-xctest-func" && func == "setUp"), + "setUp should not be captured as a test function" + ); + + assert!( + !captures + .iter() + .any(|(tag, _, func)| tag == "swift-xctest-func" && func == "helperMethod"), + "helperMethod should not be captured as a test function" + ); + } + + #[test] + fn test_quick_spec() { + let source = r#" +import Quick +import Nimble + +class MyQuickSpec: QuickSpec { + override func spec() { + describe("something") { + it("does something") { + expect(true).to(beTrue()) + } + } + } +} +"#; + + let captures = get_captures(source, get_query()); + + assert!( + captures + .iter() + .any(|(tag, _, _)| tag == "swift-test-quick-spec"), + "Expected to find swift-test-quick-spec tag" + ); + } + + #[test] + fn test_async_spec() { + let source = r#" +import Quick +import Nimble + +class MyAsyncSpec: AsyncSpec { + override func spec() { + describe("async operations") { + it("does async work") { + await someAsyncFunction() + } + } + } +} +"#; + + let captures = get_captures(source, get_query()); + + assert!( + captures + .iter() + .any(|(tag, _, _)| tag == "swift-test-async-spec"), + "Expected to find swift-test-async-spec tag" + ); + } + + #[test] + fn test_multiple_test_types_in_same_file() { + let source = r#" +@Test func topLevelTest() {} + +@Suite +struct SwiftTestingSuite { + @Test func swiftTestingTest() {} +} + +class XCTestSuite: XCTestCase { + func testXCTest() {} +} +"#; + + let captures = get_captures(source, get_query()); + + // Should find all three types of tests + assert!( + captures + .iter() + .any(|(tag, _, _)| tag == "swift-testing-bare-func"), + "Should find bare test function" + ); + + assert!( + captures + .iter() + .any(|(tag, _, _)| tag == "swift-testing-suite"), + "Should find Swift Testing suite" + ); + + assert!( + captures + .iter() + .any(|(tag, _, _)| tag == "swift-xctest-class"), + "Should find XCTest class" + ); + } + + #[test] + fn test_nested_classes_not_confused() { + let source = r#" +class OuterTests: XCTestCase { + func testOuter() {} + + class Inner { + func testInner() {} + } +} +"#; + + let captures = get_captures(source, get_query()); + + // Only testOuter should be captured as it's in the XCTestCase subclass + assert!( + captures + .iter() + .any(|(tag, class, func)| tag == "swift-xctest-func" + && class == "OuterTests" + && func == "testOuter"), + "Should find testOuter in XCTestCase" + ); + + // testInner should NOT be captured as it's in a nested class that doesn't inherit from XCTestCase + let inner_test_count = captures + .iter() + .filter(|(tag, _, func)| tag == "swift-xctest-func" && func == "testInner") + .count(); + + assert_eq!(inner_test_count, 0, "testInner should not be captured"); + } + + #[test] + fn test_xctest_indirect_subclass() { + // This test documents a known limitation: tree-sitter queries work on syntax, not semantics, + // so they cannot follow inheritance chains. Indirect subclasses (MyTests <- MyTestsBase <- XCTestCase) + // are NOT detected WITHOUT annotation. + // + // However, users can use the // @XCTestClass comment annotation as a workaround. + + let source = r#" +import XCTest + +class MyTestsBase: XCTestCase { + // Base class with common functionality +} + +class MyTests: MyTestsBase { + func testSomething() { + XCTAssertTrue(true) + } + + func testAnotherThing() { + XCTAssertEqual(1, 1) + } +} +"#; + + let captures = get_captures(source, get_query()); + + // Should capture MyTestsBase class (direct subclass of XCTestCase) + assert!( + captures + .iter() + .any(|(tag, class, _)| tag == "swift-xctest-class" && class == "MyTestsBase"), + "Expected to find swift-xctest-class tag for MyTestsBase (direct subclass of XCTestCase)" + ); + + // MyTests will NOT be captured as an XCTest class because tree-sitter can't follow + // the inheritance chain. It only sees that MyTests inherits from MyTestsBase, not that + // MyTestsBase inherits from XCTestCase. + assert!( + !captures + .iter() + .any(|(tag, class, _)| tag == "swift-xctest-class" && class == "MyTests"), + "MyTests should NOT be captured because tree-sitter cannot follow indirect inheritance" + ); + + // Test functions in MyTests will NOT be captured because the class itself wasn't + // recognized as an XCTestCase subclass + assert!( + !captures + .iter() + .any(|(tag, class, func)| tag == "swift-xctest-func" + && class == "MyTests" + && func == "testSomething"), + "testSomething should NOT be captured - tree-sitter can't follow indirect inheritance" + ); + + assert!( + !captures + .iter() + .any(|(tag, class, func)| tag == "swift-xctest-func" + && class == "MyTests" + && func == "testAnotherThing"), + "testAnotherThing should NOT be captured - tree-sitter can't follow indirect inheritance" + ); + } + + #[test] + fn test_xctest_indirect_subclass_with_annotation() { + // This test verifies that the // @XCTestClass comment annotation workaround + // allows indirect XCTest subclasses to be detected by the runnables.scm query. + + let source = r#" +import XCTest + +class MyTestsBase: XCTestCase { + // Base class with common functionality +} + +// @XCTestClass +class MyTests: MyTestsBase { + func testSomething() { + XCTAssertTrue(true) + } + + func testAnotherThing() { + XCTAssertEqual(1, 1) + } +} +"#; + + let captures = get_captures(source, get_query()); + + // Should capture MyTestsBase class (direct subclass of XCTestCase) + assert!( + captures + .iter() + .any(|(tag, class, _)| tag == "swift-xctest-class" && class == "MyTestsBase"), + "Expected to find swift-xctest-class tag for MyTestsBase (direct subclass of XCTestCase)" + ); + + // MyTests SHOULD now be captured because of the @XCTestClass annotation + assert!( + captures + .iter() + .any(|(tag, class, _)| tag == "swift-xctest-class" && class == "MyTests"), + "Expected to find swift-xctest-class tag for MyTests with @XCTestClass annotation" + ); + + // Test functions in MyTests SHOULD be captured with the annotation + assert!( + captures + .iter() + .any(|(tag, class, func)| tag == "swift-xctest-func" + && class == "MyTests" + && func == "testSomething"), + "Expected to find testSomething in annotated indirect subclass" + ); + + assert!( + captures + .iter() + .any(|(tag, class, func)| tag == "swift-xctest-func" + && class == "MyTests" + && func == "testAnotherThing"), + "Expected to find testAnotherThing in annotated indirect subclass" + ); + } + + #[test] + fn test_comment_annotation_for_indirect_subclass() { + // This test demonstrates the complete workaround for indirect XCTest subclasses. + // It shows a realistic scenario with a base test class and an annotated subclass. + + let source = r#" +import XCTest + +// Base test class with shared setup/teardown +class BaseTestCase: XCTestCase { + var sharedResource: String! + + override func setUp() { + super.setUp() + sharedResource = "test" + } +} + +// @XCTestClass +class MyFeatureTests: BaseTestCase { + func testFeatureA() { + XCTAssertNotNil(sharedResource) + } + + func testFeatureB() { + XCTAssertEqual(sharedResource, "test") + } + + func helperMethod() { + // Not a test + } +} + +// Another annotated indirect subclass +// @XCTestClass +class MyOtherTests: BaseTestCase { + func testAnotherFeature() { + XCTAssertTrue(true) + } +} +"#; + + let captures = get_captures(source, get_query()); + + // Should capture the base class (direct XCTestCase subclass) + assert!( + captures + .iter() + .any(|(tag, class, _)| tag == "swift-xctest-class" && class == "BaseTestCase"), + "Expected to find BaseTestCase" + ); + + // Should capture MyFeatureTests class (annotated) + assert!( + captures + .iter() + .any(|(tag, class, _)| tag == "swift-xctest-class" && class == "MyFeatureTests"), + "Expected to find MyFeatureTests with @XCTestClass annotation" + ); + + // Should capture test functions in MyFeatureTests + assert!( + captures + .iter() + .any(|(tag, class, func)| tag == "swift-xctest-func" + && class == "MyFeatureTests" + && func == "testFeatureA"), + "Expected to find testFeatureA" + ); + + assert!( + captures + .iter() + .any(|(tag, class, func)| tag == "swift-xctest-func" + && class == "MyFeatureTests" + && func == "testFeatureB"), + "Expected to find testFeatureB" + ); + + // helperMethod should NOT be captured (doesn't start with "test") + assert!( + !captures + .iter() + .any(|(tag, _, func)| tag == "swift-xctest-func" && func == "helperMethod"), + "helperMethod should not be captured" + ); + + // Should capture MyOtherTests class (annotated) + assert!( + captures + .iter() + .any(|(tag, class, _)| tag == "swift-xctest-class" && class == "MyOtherTests"), + "Expected to find MyOtherTests with @XCTestClass annotation" + ); + + // Should capture test in MyOtherTests + assert!( + captures + .iter() + .any(|(tag, class, func)| tag == "swift-xctest-func" + && class == "MyOtherTests" + && func == "testAnotherFeature"), + "Expected to find testAnotherFeature" + ); + } + + #[test] + fn test_query_is_valid() { + // This test ensures the query itself is syntactically valid + // The query is compiled on first access via get_query() + let query = get_query(); + + // If we got here, the query compiled successfully + assert!( + query.capture_names().len() > 0, + "Query should have capture names" + ); + } +} diff --git a/src/swift.rs b/src/swift.rs index 12d5c50..8ca8fbc 100644 --- a/src/swift.rs +++ b/src/swift.rs @@ -213,3 +213,6 @@ impl zed::Extension for SwiftExtension { } zed::register_extension!(SwiftExtension); + +#[cfg(test)] +mod runnables_test;