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 <noreply@anthropic.com>
This commit is contained in:
@@ -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", []))}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<IGameService>();
|
||||
_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<IPaymentService>();
|
||||
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 = """
|
||||
|
||||
Reference in New Issue
Block a user