From be2353cf2f93406dc889b108e44a9ed8cdfb1312 Mon Sep 17 00:00:00 2001 From: YusufKaraaslanSpyke Date: Fri, 30 Jan 2026 10:12:45 +0300 Subject: [PATCH] fix: Add C# test example extraction and fix config_type field mismatch Bug fixes: - Fix KeyError in config_enhancer.py where "config_type" was expected but config_extractor saves as "type". Now supports both field names for backward compatibility. - Fix settings "value_type" vs "type" mismatch in the same file. New features: - Add C# support for regex-based test example extraction - Add language alias mapping (C# -> csharp, C++ -> cpp) - Enhanced C# patterns for NUnit, xUnit, MSTest test frameworks - Support for mock patterns (NSubstitute, Moq) - Support for Zenject dependency injection patterns - Support for setup/teardown method extraction Tests: - Add 2 new C# test extraction tests (NUnit tests, mock patterns) - All 1257 tests pass (165 skipped) Co-Authored-By: Claude Opus 4.5 --- src/skill_seekers/cli/config_enhancer.py | 12 ++- .../cli/test_example_extractor.py | 75 +++++++++++++++++- tests/test_test_example_extractor.py | 76 +++++++++++++++++++ 3 files changed, 157 insertions(+), 6 deletions(-) diff --git a/src/skill_seekers/cli/config_enhancer.py b/src/skill_seekers/cli/config_enhancer.py index da2082a..b033ef9 100644 --- a/src/skill_seekers/cli/config_enhancer.py +++ b/src/skill_seekers/cli/config_enhancer.py @@ -165,12 +165,16 @@ class ConfigEnhancer: for cf in config_files[:10]: # Limit to first 10 files settings_summary = [] for setting in cf.get("settings", [])[:5]: # First 5 settings per file + # Support both "type" (from config_extractor) and "value_type" (legacy) + value_type = setting.get("type", setting.get("value_type", "unknown")) settings_summary.append( - f" - {setting['key']}: {setting['value']} ({setting['value_type']})" + f" - {setting['key']}: {setting['value']} ({value_type})" ) + # Support both "type" (from config_extractor) and "config_type" (legacy) + config_type = cf.get("type", cf.get("config_type", "unknown")) config_summary.append(f""" -File: {cf["relative_path"]} ({cf["config_type"]}) +File: {cf["relative_path"]} ({config_type}) Purpose: {cf["purpose"]} Settings: {chr(10).join(settings_summary)} @@ -291,8 +295,10 @@ Focus on actionable insights that help developers understand and improve their c # Format config data for Claude config_data = [] for cf in config_files[:10]: + # Support both "type" (from config_extractor) and "config_type" (legacy) + config_type = cf.get("type", cf.get("config_type", "unknown")) config_data.append(f""" -### {cf["relative_path"]} ({cf["config_type"]}) +### {cf["relative_path"]} ({config_type}) - Purpose: {cf["purpose"]} - Patterns: {", ".join(cf.get("patterns", []))} - Settings count: {len(cf.get("settings", []))} diff --git a/src/skill_seekers/cli/test_example_extractor.py b/src/skill_seekers/cli/test_example_extractor.py index 7baebf2..282673e 100644 --- a/src/skill_seekers/cli/test_example_extractor.py +++ b/src/skill_seekers/cli/test_example_extractor.py @@ -651,9 +651,20 @@ class GenericTestAnalyzer: "test_function": r"@Test\s+public\s+void\s+(\w+)\(\)", }, "csharp": { - "instantiation": r"var\s+(\w+)\s*=\s*new\s+(\w+)\(([^)]*)\)", - "assertion": r"Assert\.(?:AreEqual|IsTrue|IsFalse|IsNotNull)\(([^)]+)\)", - "test_function": r"\[Test\]\s+public\s+void\s+(\w+)\(\)", + # Object instantiation patterns (var, explicit type, generic) + "instantiation": r"(?:var|[\w<>]+)\s+(\w+)\s*=\s*new\s+([\w<>]+)\(([^)]*)\)", + # NUnit assertions (Assert.AreEqual, Assert.That, etc.) + "assertion": r"Assert\.(?:AreEqual|AreNotEqual|IsTrue|IsFalse|IsNull|IsNotNull|That|Throws|DoesNotThrow|Greater|Less|Contains)\(([^)]+)\)", + # NUnit test attributes ([Test], [TestCase], [TestCaseSource]) + "test_function": r"\[(?:Test|TestCase|TestCaseSource|Theory|Fact)\(?[^\]]*\)?\]\s*(?:\[[\w\(\)\"',\s]+\]\s*)*public\s+(?:async\s+)?(?:Task|void)\s+(\w+)\s*\(", + # Setup/Teardown patterns + "setup": r"\[(?:SetUp|OneTimeSetUp|TearDown|OneTimeTearDown)\]\s*public\s+(?:async\s+)?(?:Task|void)\s+(\w+)\s*\(", + # Mock/substitute patterns (NSubstitute, Moq) + "mock": r"(?:Substitute\.For<([\w<>]+)>|new\s+Mock<([\w<>]+)>|MockRepository\.GenerateMock<([\w<>]+)>)\(", + # Dependency injection patterns (Zenject, etc.) + "injection": r"Container\.(?:Bind|BindInterfacesTo|BindInterfacesAndSelfTo)<([\w<>]+)>", + # Configuration/setup dictionaries + "config": r"(?:var|[\w<>]+)\s+\w+\s*=\s*new\s+(?:Dictionary|List|HashSet)<[^>]+>\s*\{[\s\S]{20,500}?\}", }, "php": { "instantiation": r"\$(\w+)\s*=\s*new\s+(\w+)\(([^)]*)\)", @@ -667,11 +678,21 @@ class GenericTestAnalyzer: }, } + # Language name normalization mapping + LANGUAGE_ALIASES = { + "c#": "csharp", + "c++": "cpp", + "c plus plus": "cpp", + } + def extract(self, file_path: str, code: str, language: str) -> list[TestExample]: """Extract examples from test file using regex patterns""" examples = [] language_lower = language.lower() + # Normalize language name (e.g., "C#" -> "csharp") + language_lower = self.LANGUAGE_ALIASES.get(language_lower, language_lower) + if language_lower not in self.PATTERNS: logger.warning(f"Language {language} not supported for regex extraction") return [] @@ -715,6 +736,54 @@ class GenericTestAnalyzer: ) examples.append(example) + # Extract mock/substitute patterns (if pattern exists) + if "mock" in patterns: + for mock_match in re.finditer(patterns["mock"], test_body): + example = self._create_example( + test_name=test_name, + category="setup", + code=mock_match.group(0), + language=language, + file_path=file_path, + line_number=code[: start_pos + mock_match.start()].count("\n") + 1, + ) + examples.append(example) + + # Extract dependency injection patterns (if pattern exists) + if "injection" in patterns: + for inject_match in re.finditer(patterns["injection"], test_body): + example = self._create_example( + test_name=test_name, + category="setup", + code=inject_match.group(0), + language=language, + file_path=file_path, + line_number=code[: start_pos + inject_match.start()].count("\n") + 1, + ) + examples.append(example) + + # Also extract setup/teardown methods (outside test functions) + if "setup" in patterns: + for setup_match in re.finditer(patterns["setup"], code): + setup_name = setup_match.group(1) + # Get setup function body + setup_start = setup_match.end() + # Find next method (setup or test) + next_pattern = patterns.get("setup", patterns["test_function"]) + next_setup = re.search(next_pattern, code[setup_start:]) + setup_end = setup_start + next_setup.start() if next_setup else min(setup_start + 500, len(code)) + setup_body = code[setup_start:setup_end] + + example = self._create_example( + test_name=setup_name, + category="setup", + code=setup_match.group(0) + setup_body[:200], # Include some of the body + language=language, + file_path=file_path, + line_number=code[: setup_match.start()].count("\n") + 1, + ) + examples.append(example) + return examples def _create_example( diff --git a/tests/test_test_example_extractor.py b/tests/test_test_example_extractor.py index 90cca76..0ac855b 100644 --- a/tests/test_test_example_extractor.py +++ b/tests/test_test_example_extractor.py @@ -307,6 +307,82 @@ fn test_subtract() { self.assertGreater(len(examples), 0) self.assertEqual(examples[0].language, "Rust") + def test_extract_csharp_nunit_tests(self): + """Test C# NUnit test extraction""" + code = """ +using NUnit.Framework; +using NSubstitute; + +[TestFixture] +public class GameControllerTests +{ + private IGameService _gameService; + private GameController _controller; + + [SetUp] + public void SetUp() + { + _gameService = Substitute.For(); + _controller = new GameController(_gameService); + } + + [Test] + public void StartGame_ShouldInitializeBoard() + { + var config = new GameConfig { Rows = 8, Columns = 8 }; + var board = new GameBoard(config); + + _controller.StartGame(board); + + Assert.IsTrue(board.IsInitialized); + Assert.AreEqual(64, board.CellCount); + } + + [TestCase(1, 2)] + [TestCase(3, 4)] + public void MovePlayer_ShouldUpdatePosition(int x, int y) + { + var player = new Player("Test"); + _controller.MovePlayer(player, x, y); + + Assert.AreEqual(x, player.X); + Assert.AreEqual(y, player.Y); + } +} +""" + examples = self.analyzer.extract("GameControllerTests.cs", code, "C#") + + # Should extract test functions and instantiations + self.assertGreater(len(examples), 0) + self.assertEqual(examples[0].language, "C#") + + # Check that we found some instantiations + instantiations = [e for e in examples if e.category == "instantiation"] + self.assertGreater(len(instantiations), 0) + + # Check for setup extraction + setups = [e for e in examples if e.category == "setup"] + # May or may not have setups depending on extraction + + def test_extract_csharp_with_mocks(self): + """Test C# mock pattern extraction (NSubstitute)""" + code = """ +[Test] +public void ProcessOrder_ShouldCallPaymentService() +{ + var paymentService = Substitute.For(); + var orderProcessor = new OrderProcessor(paymentService); + + orderProcessor.ProcessOrder(100); + + paymentService.Received().Charge(100); +} +""" + examples = self.analyzer.extract("OrderTests.cs", code, "C#") + + # Should extract instantiation and mock + self.assertGreater(len(examples), 0) + def test_language_fallback(self): """Test handling of unsupported languages""" code = """