mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
126667257a
Add 6 multi-language coverage format parsers: - JaCoCo XML (Java/Kotlin) with line/branch/method/instruction counters - dotcover XML (C#/.NET) with statement-level coverage - Tarpaulin JSON (Rust) with trace-based line coverage - Go cover text format with block-level statement tracking - Kover XML (Kotlin/JVM) via JaCoCo-compatible parsing - xcresult JSON (Swift/iOS) with function-level detail Add language-aware agent routing: - New `languages` field on AgentCapabilityProfile - 0.15 score boost for agents matching request language - Configurable language weight in RoutingWeights - Case-insensitive, backward-compatible Update MCP tool schemas: - Add `language` and `coverageFormat` params to coverage analyze/gaps tools - Auto-detect format from filename and content signatures - Extended findAndParseCoverage with multi-language file paths 24 new tests, all passing. 30 existing routing tests unaffected. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
333 lines
11 KiB
TypeScript
333 lines
11 KiB
TypeScript
/**
|
|
* Multi-Language Coverage Parser Tests
|
|
*
|
|
* Tests for Phase 5 coverage format parsers:
|
|
* - JaCoCo XML (Java/Kotlin)
|
|
* - dotcover XML (C#/.NET)
|
|
* - Tarpaulin JSON (Rust)
|
|
* - Go cover text (Go)
|
|
* - Kover XML (Kotlin/JVM)
|
|
* - xcresult JSON (Swift/iOS)
|
|
*
|
|
* @see Issue #324 — Multi-language Phase 5
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
import {
|
|
parseJaCoCoContent,
|
|
parseDotcoverContent,
|
|
parseTarpaulinContent,
|
|
parseGoCoverContent,
|
|
parseXcresultContent,
|
|
parseLCOVContent,
|
|
type CoverageReport,
|
|
type CoverageFormat,
|
|
} from '../../../src/domains/coverage-analysis/services/coverage-parser.js';
|
|
|
|
// ============================================================================
|
|
// JaCoCo XML
|
|
// ============================================================================
|
|
|
|
describe('JaCoCo XML Parser', () => {
|
|
const JACOCO_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<report name="MyProject">
|
|
<package name="com/example/auth">
|
|
<sourcefile name="AuthService.java">
|
|
<line nr="10" mi="0" ci="3" mb="0" cb="2"/>
|
|
<line nr="11" mi="2" ci="0" mb="1" cb="0"/>
|
|
<line nr="12" mi="0" ci="1" mb="0" cb="0"/>
|
|
<counter type="INSTRUCTION" missed="5" covered="20"/>
|
|
<counter type="BRANCH" missed="1" covered="3"/>
|
|
<counter type="LINE" missed="1" covered="2"/>
|
|
<counter type="METHOD" missed="0" covered="3"/>
|
|
</sourcefile>
|
|
</package>
|
|
</report>`;
|
|
|
|
it('should parse JaCoCo XML and extract line coverage', () => {
|
|
const report = parseJaCoCoContent(JACOCO_XML, '/project');
|
|
|
|
expect(report.format).toBe('jacoco');
|
|
expect(report.language).toBe('java');
|
|
expect(report.files.size).toBe(1);
|
|
|
|
const file = Array.from(report.files.values())[0];
|
|
expect(file.lines.total).toBe(3); // 1 missed + 2 covered
|
|
expect(file.lines.covered).toBe(2);
|
|
expect(file.branches.total).toBe(4); // 1 missed + 3 covered
|
|
expect(file.branches.covered).toBe(3);
|
|
expect(file.functions.total).toBe(3);
|
|
expect(file.functions.covered).toBe(3);
|
|
});
|
|
|
|
it('should extract uncovered lines from line elements', () => {
|
|
const report = parseJaCoCoContent(JACOCO_XML, '/project');
|
|
const file = Array.from(report.files.values())[0];
|
|
|
|
// Line 11 has ci=0 (covered instructions = 0)
|
|
expect(file.lines.uncoveredLines).toContain(11);
|
|
});
|
|
|
|
it('should compute summary correctly', () => {
|
|
const report = parseJaCoCoContent(JACOCO_XML, '/project');
|
|
|
|
expect(report.summary.totalFiles).toBe(1);
|
|
expect(report.summary.lines.covered).toBe(2);
|
|
expect(report.summary.lines.total).toBe(3);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// dotcover XML
|
|
// ============================================================================
|
|
|
|
describe('dotcover XML Parser', () => {
|
|
const DOTCOVER_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Root CoveredStatements="15" TotalStatements="20" CoveragePercent="75">
|
|
<Assembly Name="MyApp">
|
|
<Namespace Name="MyApp.Services">
|
|
<Type Name="UserService">
|
|
<File Index="1" Name="/src/UserService.cs"/>
|
|
<Statement FileIndex="1" Line="10" Column="5" EndLine="10" EndColumn="30" Covered="True"/>
|
|
<Statement FileIndex="1" Line="11" Column="5" EndLine="11" EndColumn="25" Covered="True"/>
|
|
<Statement FileIndex="1" Line="15" Column="5" EndLine="15" EndColumn="20" Covered="False"/>
|
|
</Type>
|
|
</Namespace>
|
|
</Assembly>
|
|
</Root>`;
|
|
|
|
it('should parse dotcover XML and extract statement coverage', () => {
|
|
const report = parseDotcoverContent(DOTCOVER_XML, '/project');
|
|
|
|
expect(report.format).toBe('dotcover');
|
|
expect(report.language).toBe('csharp');
|
|
expect(report.files.size).toBe(1);
|
|
|
|
const file = Array.from(report.files.values())[0];
|
|
expect(file.statements.total).toBe(3);
|
|
expect(file.statements.covered).toBe(2);
|
|
});
|
|
|
|
it('should identify uncovered lines', () => {
|
|
const report = parseDotcoverContent(DOTCOVER_XML, '/project');
|
|
const file = Array.from(report.files.values())[0];
|
|
|
|
expect(file.lines.uncoveredLines).toContain(15);
|
|
});
|
|
|
|
it('should fall back to root attributes when no Statement elements', () => {
|
|
const simpleXml = `<Root CoveredStatements="80" TotalStatements="100"/>`;
|
|
const report = parseDotcoverContent(simpleXml, '/project');
|
|
|
|
expect(report.files.size).toBe(1);
|
|
const file = Array.from(report.files.values())[0];
|
|
expect(file.statements.total).toBe(100);
|
|
expect(file.statements.covered).toBe(80);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Tarpaulin JSON (Rust)
|
|
// ============================================================================
|
|
|
|
describe('Tarpaulin JSON Parser', () => {
|
|
const TARPAULIN_JSON = JSON.stringify({
|
|
files: [
|
|
{
|
|
path: '/project/src/lib.rs',
|
|
content: '',
|
|
traces: [
|
|
{ line: 5, stats: { Line: 3 } },
|
|
{ line: 6, stats: { Line: 0 } },
|
|
{ line: 10, stats: { Line: 1 } },
|
|
{ line: 15, stats: { Line: 0 } },
|
|
],
|
|
},
|
|
{
|
|
path: '/project/src/main.rs',
|
|
content: '',
|
|
traces: [
|
|
{ line: 3, stats: { Line: 5 } },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
it('should parse tarpaulin JSON and extract line coverage', () => {
|
|
const report = parseTarpaulinContent(TARPAULIN_JSON, '/project');
|
|
|
|
expect(report.format).toBe('tarpaulin');
|
|
expect(report.language).toBe('rust');
|
|
expect(report.files.size).toBe(2);
|
|
|
|
const libFile = report.files.get('/project/src/lib.rs')!;
|
|
expect(libFile.lines.total).toBe(4);
|
|
expect(libFile.lines.covered).toBe(2);
|
|
expect(libFile.lines.uncoveredLines).toEqual([6, 15]);
|
|
});
|
|
|
|
it('should handle empty traces array', () => {
|
|
const emptyJson = JSON.stringify({ files: [{ path: '/project/src/empty.rs', traces: [] }] });
|
|
const report = parseTarpaulinContent(emptyJson, '/project');
|
|
|
|
const file = report.files.get('/project/src/empty.rs')!;
|
|
expect(file.lines.total).toBe(0);
|
|
expect(file.coveragePercentage).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Go cover
|
|
// ============================================================================
|
|
|
|
describe('Go cover Parser', () => {
|
|
const GO_COVER = `mode: set
|
|
github.com/myapp/pkg/handler.go:10.30,15.2 3 1
|
|
github.com/myapp/pkg/handler.go:17.40,22.2 4 0
|
|
github.com/myapp/pkg/util.go:5.20,8.2 2 1
|
|
github.com/myapp/pkg/util.go:10.25,14.2 3 1`;
|
|
|
|
it('should parse Go coverage profile format', () => {
|
|
const report = parseGoCoverContent(GO_COVER, '/project');
|
|
|
|
expect(report.format).toBe('gocover');
|
|
expect(report.language).toBe('go');
|
|
expect(report.files.size).toBe(2);
|
|
});
|
|
|
|
it('should extract statement counts from coverage blocks', () => {
|
|
const report = parseGoCoverContent(GO_COVER, '/project');
|
|
|
|
const handler = report.files.get('github.com/myapp/pkg/handler.go')!;
|
|
expect(handler.statements.total).toBe(7); // 3 + 4
|
|
expect(handler.statements.covered).toBe(3); // only first block covered
|
|
|
|
const util = report.files.get('github.com/myapp/pkg/util.go')!;
|
|
expect(util.statements.total).toBe(5); // 2 + 3
|
|
expect(util.statements.covered).toBe(5); // both covered
|
|
});
|
|
|
|
it('should mark uncovered lines from blocks with count=0', () => {
|
|
const report = parseGoCoverContent(GO_COVER, '/project');
|
|
|
|
const handler = report.files.get('github.com/myapp/pkg/handler.go')!;
|
|
// Lines 17-22 should be uncovered (count=0)
|
|
expect(handler.lines.uncoveredLines).toContain(17);
|
|
expect(handler.lines.uncoveredLines).toContain(22);
|
|
});
|
|
|
|
it('should skip mode line', () => {
|
|
const report = parseGoCoverContent('mode: atomic\n', '/project');
|
|
expect(report.files.size).toBe(0);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Kover XML (Kotlin)
|
|
// ============================================================================
|
|
|
|
describe('Kover XML Parser', () => {
|
|
it('should parse Kover XML as JaCoCo-compatible with kotlin language', () => {
|
|
const koverXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<report name="Kover Report">
|
|
<package name="com/example">
|
|
<sourcefile name="Service.kt">
|
|
<counter type="LINE" missed="2" covered="8"/>
|
|
<counter type="BRANCH" missed="0" covered="4"/>
|
|
<counter type="METHOD" missed="1" covered="5"/>
|
|
<counter type="INSTRUCTION" missed="10" covered="50"/>
|
|
</sourcefile>
|
|
</package>
|
|
</report>`;
|
|
|
|
// Kover uses JaCoCo-compatible XML, just with different format/language tags
|
|
const report = parseJaCoCoContent(koverXml, '/project');
|
|
// When called via parseKover, format would be 'kover' and language 'kotlin'
|
|
// Here we test the underlying parsing works
|
|
expect(report.files.size).toBe(1);
|
|
|
|
const file = Array.from(report.files.values())[0];
|
|
expect(file.lines.total).toBe(10);
|
|
expect(file.lines.covered).toBe(8);
|
|
expect(file.functions.total).toBe(6);
|
|
expect(file.functions.covered).toBe(5);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// xcresult JSON (Swift)
|
|
// ============================================================================
|
|
|
|
describe('xcresult JSON Parser', () => {
|
|
const XCRESULT_JSON = JSON.stringify({
|
|
targets: [
|
|
{
|
|
name: 'MyAppTests',
|
|
files: [
|
|
{
|
|
path: '/project/Sources/ViewController.swift',
|
|
lineCoverage: 0.85,
|
|
coveredLines: 17,
|
|
executableLines: 20,
|
|
functions: [
|
|
{ name: 'viewDidLoad()', lineNumber: 10, coveredLines: 5 },
|
|
{ name: 'handleTap(_:)', lineNumber: 25, coveredLines: 0 },
|
|
],
|
|
},
|
|
{
|
|
path: '/project/Sources/Model.swift',
|
|
lineCoverage: 1.0,
|
|
coveredLines: 10,
|
|
executableLines: 10,
|
|
functions: [],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
it('should parse xcresult JSON and extract coverage', () => {
|
|
const report = parseXcresultContent(XCRESULT_JSON, '/project');
|
|
|
|
expect(report.format).toBe('xcresult');
|
|
expect(report.language).toBe('swift');
|
|
expect(report.files.size).toBe(2);
|
|
});
|
|
|
|
it('should extract function coverage from xcresult', () => {
|
|
const report = parseXcresultContent(XCRESULT_JSON, '/project');
|
|
|
|
const vc = report.files.get('/project/Sources/ViewController.swift')!;
|
|
expect(vc.lines.total).toBe(20);
|
|
expect(vc.lines.covered).toBe(17);
|
|
expect(vc.coveragePercentage).toBe(85);
|
|
expect(vc.functions.total).toBe(2);
|
|
expect(vc.functions.covered).toBe(1);
|
|
expect(vc.functions.uncoveredFunctions).toContain('handleTap(_:)');
|
|
});
|
|
|
|
it('should handle 100% coverage files', () => {
|
|
const report = parseXcresultContent(XCRESULT_JSON, '/project');
|
|
|
|
const model = report.files.get('/project/Sources/Model.swift')!;
|
|
expect(model.coveragePercentage).toBe(100);
|
|
expect(model.lines.covered).toBe(10);
|
|
});
|
|
});
|
|
|
|
// ============================================================================
|
|
// Format Detection
|
|
// ============================================================================
|
|
|
|
describe('Coverage Format Type', () => {
|
|
it('should support all new format types', () => {
|
|
const formats: CoverageFormat[] = [
|
|
'lcov', 'cobertura', 'json',
|
|
'jacoco', 'dotcover', 'tarpaulin',
|
|
'gocover', 'kover', 'xcresult',
|
|
];
|
|
expect(formats).toHaveLength(9);
|
|
});
|
|
});
|