From 4bb8c5c42f3735f137b51497e9d8e33e358ddf70 Mon Sep 17 00:00:00 2001 From: Den1552 Date: Fri, 30 Jan 2026 08:57:28 +0100 Subject: [PATCH 01/20] Initial --- package.json | 14 ++++ src/extension.ts | 181 +++++++++++++++++++++++++++++++++++++++++++++++ src/utilities.ts | 5 ++ 3 files changed, 200 insertions(+) diff --git a/package.json b/package.json index aaecc9e8..3eb460ef 100644 --- a/package.json +++ b/package.json @@ -229,6 +229,11 @@ "category": "VectorCAST Test Explorer", "title": "Open Source File under Test" }, + { + "command": "vectorcastTestExplorer.openReqsCoverageReview", + "category": "VectorCAST Test Explorer", + "title": "Open Requirements Coverage Review" + }, { "command": "vectorcastTestExplorer.insertBasisPathTests", "category": "VectorCAST Test Explorer", @@ -771,6 +776,10 @@ { "command": "vectorcastTestExplorer.openSourceFileFromTestpaneCommand", "when": "never" + }, + { + "command": "vectorcastTestExplorer.openReqsCoverageReview", + "when": "never" }, { "command": "vectorcastTestExplorer.insertBasisPathTests", @@ -1080,6 +1089,11 @@ "group": "vcast.enviroManagement", "when": "testId =~ /^vcast:.*$/ && !(testId =~ /^.*<>.*$/) && !(testId =~ /^.*<>.*$/) && !(testId =~ /.*coded_tests_driver.*/) && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && testId not in vectorcastTestExplorer.vcastEnviroList" }, + { + "command": "vectorcastTestExplorer.openReqsCoverageReview", + "group": "vcast.enviroManagement", + "when": "testId =~ /^vcast:.*$/ && !(testId =~ /^.*<>.*$/) && !(testId =~ /^.*<>.*$/) && !(testId =~ /.*coded_tests_driver.*/) && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && testId not in vectorcastTestExplorer.vcastEnviroList" + }, { "command": "vectorcastTestExplorer.insertBasisPathTests", "group": "vcast.testGeneration", diff --git a/src/extension.ts b/src/extension.ts index 7d859e6c..4848dae9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -79,6 +79,7 @@ import { forceLowerCaseDriveLetter, decodeVar, getFullEnvReport, + requirementsTestData, } from "./utilities"; import { @@ -1307,6 +1308,186 @@ function configureExtension(context: vscode.ExtensionContext) { } ); context.subscriptions.push(openSourceFileFromTestpaneCommand); + // Type for a single requirement + interface RequirementData { + title: string; + description: string; + lineNumber: number; + importantLineStart: number; + importantLineEnd: number; + coverageStatus: "covered" | "partially-covered" | "uncovered"; + } + + // Store active highlight decoration + let activeHighlightDecoration: vscode.TextEditorDecorationType | null = null; + + // helper: wrap text to fixed width + function wrapText(text: string, width: number): string[] { + const words = text.split(" "); + const lines: string[] = []; + let current = ""; + + for (const w of words) { + if ((current + w).length > width) { + lines.push(current.trimEnd()); + current = w + " "; + } else { + current += w + " "; + } + } + if (current.trim()) { + lines.push(current.trimEnd()); + } + return lines; + } + + // Command: vectorcastTestExplorer.openReqsCoverageReview + let openReqsCoverageReview = vscode.commands.registerCommand( + "vectorcastTestExplorer.openReqsCoverageReview", + async (args: any) => { + if (!args) return; + + const testNode: testNodeType = getTestNode(args.id); + if (!testNode) return; + + const { enviroPath, unitName } = testNode; + + const envData = await getEnvironmentData(enviroPath); + if (!envData?.unitData) return; + + const reqData: RequirementData = { + title: "REQ-069: Session Timeout", + description: + "The system shall automatically terminate user sessions after 30 minutes of inactivity to ensure security.", + lineNumber: 74, + importantLineStart: 75, + importantLineEnd: 78, + coverageStatus: "covered", + }; + + const matchingUnit = envData.unitData.find((unit: { path: string }) => { + if (!unit.path) return false; + const unitBaseName = path.basename(unit.path, path.extname(unit.path)); + return unitBaseName === unitName; + }); + + if (!matchingUnit?.path) return; + + const sourceFileUri = vscode.Uri.file(matchingUnit.path); + const document = await vscode.workspace.openTextDocument(sourceFileUri); + + const peekPosition = new vscode.Position( + Math.max(0, reqData.lineNumber - 2), + 0 + ); + + const editor = await vscode.window.showTextDocument(document, { + preview: false, + preserveFocus: false, + selection: new vscode.Range(peekPosition, peekPosition), + }); + + if (activeHighlightDecoration) { + activeHighlightDecoration.dispose(); + } + + activeHighlightDecoration = vscode.window.createTextEditorDecorationType({ + backgroundColor: "rgba(0,255,0,0.15)", + before: { + contentText: "", + border: "4px solid", + borderColor: "#00ff00", + margin: "0 10px 0 0", + }, + }); + + const blockRange = new vscode.Range( + new vscode.Position(reqData.importantLineStart - 1, 0), + new vscode.Position( + reqData.importantLineEnd - 1, + document.lineAt(reqData.importantLineEnd - 1).text.length + ) + ); + + editor.setDecorations(activeHighlightDecoration, [blockRange]); + + const virtualDocUri = vscode.Uri.parse( + "requirement-info:Requirement Info" + ); + + const BOX_WIDTH = 66; + const wrappedDescription = wrapText(reqData.description, BOX_WIDTH - 6); + + const provider = new (class + implements vscode.TextDocumentContentProvider + { + provideTextDocumentContent(): string { + return [ + "", + "╔" + "═".repeat(BOX_WIDTH) + "╗", + `║ ${reqData.title.padEnd(BOX_WIDTH - 2)}║`, + "╠" + "═".repeat(BOX_WIDTH) + "╣", + "║ DESCRIPTION".padEnd(BOX_WIDTH + 1) + "║", + "║ " + "─".repeat(BOX_WIDTH - 2) + "║", + ...wrappedDescription.map( + (l) => `║ ${l.padEnd(BOX_WIDTH - 4)} ║` + ), + "║".padEnd(BOX_WIDTH + 1) + "║", + `║ Critical Lines : ${reqData.importantLineStart} - ${reqData.importantLineEnd}`.padEnd( + BOX_WIDTH + 1 + ) + "║", + "╚" + "═".repeat(BOX_WIDTH) + "╝", + "", + ].join("\n"); + } + })(); + + const providerDisposable = + vscode.workspace.registerTextDocumentContentProvider( + "requirement-info", + provider + ); + + await vscode.workspace.openTextDocument(virtualDocUri); + + await vscode.commands.executeCommand( + "editor.action.peekLocations", + sourceFileUri, + peekPosition, + [new vscode.Location(virtualDocUri, new vscode.Position(0, 0))], + "peek" + ); + + setTimeout(() => { + for (const e of vscode.window.visibleTextEditors) { + if (e.document.uri.scheme === "requirement-info") { + e.options = { + ...e.options, + lineNumbers: vscode.TextEditorLineNumbersStyle.Off, + }; + } + } + }, 0); + + setTimeout(() => { + providerDisposable.dispose(); + }, 1000); + } + ); + + // Command to close requirement box and highlights + let closeRequirementBoxes = vscode.commands.registerCommand( + "vectorcastTestExplorer.closeRequirementBoxes", + () => { + if (activeHighlightDecoration) { + activeHighlightDecoration.dispose(); + activeHighlightDecoration = null; + } + } + ); + + context.subscriptions.push(openReqsCoverageReview); + context.subscriptions.push(closeRequirementBoxes); let showRequirementsCommand = vscode.commands.registerCommand( "vectorcastTestExplorer.showRequirements", diff --git a/src/utilities.ts b/src/utilities.ts index 7087a279..154150c1 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -41,6 +41,11 @@ export interface jsonDataType { jsonDataAsString: string; } +export interface requirementsTestData { + title: string; + description: string; +} + /** * Retrieves the environment path associated with a given file path. * From b5211d3e6fade0991a8f962d9eceb52689c02d56 Mon Sep 17 00:00:00 2001 From: Den1552 Date: Fri, 30 Jan 2026 15:47:17 +0100 Subject: [PATCH 02/20] Enabled opening tst script --- src/extension.ts | 159 +++------------- src/requirements/requirementsUtils.ts | 257 ++++++++++++++++++++++++++ src/vcastUtilities.ts | 2 +- 3 files changed, 284 insertions(+), 134 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 4848dae9..3ae86976 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -90,6 +90,7 @@ import { rebuildEnvironment, openProjectInVcast, deleteLevel, + dumpTestScriptFile, } from "./vcastAdapter"; import { @@ -126,10 +127,15 @@ import { } from "./vcastInstallation"; import { + activeHighlightDecoration, + setActiveHighlightDecoration, findRelevantRequirementGateway, generateRequirementsHtml, + openSourceFileWithHighlight, + openTstScriptAtTest, parseRequirementsFromFile, performLLMProviderUsableCheck, + RequirementData, requirementsFileWatcher, updateRequirementsAvailability, } from "./requirements/requirementsUtils"; @@ -1308,40 +1314,11 @@ function configureExtension(context: vscode.ExtensionContext) { } ); context.subscriptions.push(openSourceFileFromTestpaneCommand); - // Type for a single requirement - interface RequirementData { - title: string; - description: string; - lineNumber: number; - importantLineStart: number; - importantLineEnd: number; - coverageStatus: "covered" | "partially-covered" | "uncovered"; - } - - // Store active highlight decoration - let activeHighlightDecoration: vscode.TextEditorDecorationType | null = null; - - // helper: wrap text to fixed width - function wrapText(text: string, width: number): string[] { - const words = text.split(" "); - const lines: string[] = []; - let current = ""; - - for (const w of words) { - if ((current + w).length > width) { - lines.push(current.trimEnd()); - current = w + " "; - } else { - current += w + " "; - } - } - if (current.trim()) { - lines.push(current.trimEnd()); - } - return lines; - } - // Command: vectorcastTestExplorer.openReqsCoverageReview + /** + * Command: Opens the requirements coverage review interface + * Shows source file with highlighting and TST script side-by-side + */ let openReqsCoverageReview = vscode.commands.registerCommand( "vectorcastTestExplorer.openReqsCoverageReview", async (args: any) => { @@ -1350,11 +1327,12 @@ function configureExtension(context: vscode.ExtensionContext) { const testNode: testNodeType = getTestNode(args.id); if (!testNode) return; + // Get environment and unit data const { enviroPath, unitName } = testNode; - const envData = await getEnvironmentData(enviroPath); if (!envData?.unitData) return; + // TODO: Replace with actual requirement data from environment const reqData: RequirementData = { title: "REQ-069: Session Timeout", description: @@ -1365,6 +1343,7 @@ function configureExtension(context: vscode.ExtensionContext) { coverageStatus: "covered", }; + // Find the matching unit's source file const matchingUnit = envData.unitData.find((unit: { path: string }) => { if (!unit.path) return false; const unitBaseName = path.basename(unit.path, path.extname(unit.path)); @@ -1373,119 +1352,33 @@ function configureExtension(context: vscode.ExtensionContext) { if (!matchingUnit?.path) return; - const sourceFileUri = vscode.Uri.file(matchingUnit.path); - const document = await vscode.workspace.openTextDocument(sourceFileUri); - - const peekPosition = new vscode.Position( - Math.max(0, reqData.lineNumber - 2), - 0 - ); - - const editor = await vscode.window.showTextDocument(document, { - preview: false, - preserveFocus: false, - selection: new vscode.Range(peekPosition, peekPosition), - }); - - if (activeHighlightDecoration) { - activeHighlightDecoration.dispose(); - } - - activeHighlightDecoration = vscode.window.createTextEditorDecorationType({ - backgroundColor: "rgba(0,255,0,0.15)", - before: { - contentText: "", - border: "4px solid", - borderColor: "#00ff00", - margin: "0 10px 0 0", - }, - }); - - const blockRange = new vscode.Range( - new vscode.Position(reqData.importantLineStart - 1, 0), - new vscode.Position( - reqData.importantLineEnd - 1, - document.lineAt(reqData.importantLineEnd - 1).text.length - ) - ); - - editor.setDecorations(activeHighlightDecoration, [blockRange]); - - const virtualDocUri = vscode.Uri.parse( - "requirement-info:Requirement Info" - ); - - const BOX_WIDTH = 66; - const wrappedDescription = wrapText(reqData.description, BOX_WIDTH - 6); - - const provider = new (class - implements vscode.TextDocumentContentProvider - { - provideTextDocumentContent(): string { - return [ - "", - "╔" + "═".repeat(BOX_WIDTH) + "╗", - `║ ${reqData.title.padEnd(BOX_WIDTH - 2)}║`, - "╠" + "═".repeat(BOX_WIDTH) + "╣", - "║ DESCRIPTION".padEnd(BOX_WIDTH + 1) + "║", - "║ " + "─".repeat(BOX_WIDTH - 2) + "║", - ...wrappedDescription.map( - (l) => `║ ${l.padEnd(BOX_WIDTH - 4)} ║` - ), - "║".padEnd(BOX_WIDTH + 1) + "║", - `║ Critical Lines : ${reqData.importantLineStart} - ${reqData.importantLineEnd}`.padEnd( - BOX_WIDTH + 1 - ) + "║", - "╚" + "═".repeat(BOX_WIDTH) + "╝", - "", - ].join("\n"); - } - })(); - - const providerDisposable = - vscode.workspace.registerTextDocumentContentProvider( - "requirement-info", - provider - ); - - await vscode.workspace.openTextDocument(virtualDocUri); - - await vscode.commands.executeCommand( - "editor.action.peekLocations", - sourceFileUri, - peekPosition, - [new vscode.Location(virtualDocUri, new vscode.Position(0, 0))], - "peek" - ); + // Close sidebar for more screen space + await vscode.commands.executeCommand("workbench.action.closeSidebar"); - setTimeout(() => { - for (const e of vscode.window.visibleTextEditors) { - if (e.document.uri.scheme === "requirement-info") { - e.options = { - ...e.options, - lineNumbers: vscode.TextEditorLineNumbersStyle.Off, - }; - } - } - }, 0); + // Open source file with requirement highlighting + // Pass context so it can register the peek window close listener + await openSourceFileWithHighlight(matchingUnit.path, reqData, context); - setTimeout(() => { - providerDisposable.dispose(); - }, 1000); + // Open TST script beside source file + const scriptPath = testNode.enviroPath + ".tst"; + await openTstScriptAtTest(testNode, scriptPath); } ); - // Command to close requirement box and highlights + /** + * Command: Closes requirement highlights and peek boxes + */ let closeRequirementBoxes = vscode.commands.registerCommand( "vectorcastTestExplorer.closeRequirementBoxes", () => { if (activeHighlightDecoration) { activeHighlightDecoration.dispose(); - activeHighlightDecoration = null; + setActiveHighlightDecoration(null); } } ); + // Register commands context.subscriptions.push(openReqsCoverageReview); context.subscriptions.push(closeRequirementBoxes); diff --git a/src/requirements/requirementsUtils.ts b/src/requirements/requirementsUtils.ts index baf2cc48..1e36e82e 100644 --- a/src/requirements/requirementsUtils.ts +++ b/src/requirements/requirementsUtils.ts @@ -10,6 +10,9 @@ import { logCliOperation, } from "./requirementsOperations"; import { makeEnviroNodeID } from "../testPane"; +import { dumpTestScriptFile } from "../vcastAdapter"; +import { convertTestScriptContents } from "../vcastUtilities"; +import { testNodeType } from "../testData"; const path = require("path"); const fs = require("fs"); @@ -594,3 +597,257 @@ export function expandEnvVars(inputPath: string): string { return value; }); } + +export interface RequirementData { + title: string; + description: string; + lineNumber: number; + importantLineStart: number; + importantLineEnd: number; + coverageStatus: "covered" | "partially-covered" | "uncovered"; +} + +// State Management +export let activeHighlightDecoration: vscode.TextEditorDecorationType | null = + null; + +/** + * Wraps text to a specified width, breaking at word boundaries + */ +export function wrapText(text: string, width: number): string[] { + const words = text.split(" "); + const lines: string[] = []; + let current = ""; + + for (const word of words) { + if ((current + word).length > width) { + lines.push(current.trimEnd()); + current = word + " "; + } else { + current += word + " "; + } + } + + if (current.trim()) { + lines.push(current.trimEnd()); + } + + return lines; +} + +/** + * Creates a formatted text box containing requirement information + */ +export function createRequirementInfoBox(reqData: RequirementData): string { + const BOX_WIDTH = 66; + const wrappedDescription = wrapText(reqData.description, BOX_WIDTH - 6); + + return [ + "", + "╔" + "═".repeat(BOX_WIDTH) + "╗", + `║ ${reqData.title.padEnd(BOX_WIDTH - 2)}║`, + "╠" + "═".repeat(BOX_WIDTH) + "╣", + "║ DESCRIPTION".padEnd(BOX_WIDTH + 1) + "║", + "║ " + "─".repeat(BOX_WIDTH - 2) + "║", + ...wrappedDescription.map((line) => `║ ${line.padEnd(BOX_WIDTH - 4)} ║`), + "║".padEnd(BOX_WIDTH + 1) + "║", + `║ Critical Lines : ${reqData.importantLineStart} - ${reqData.importantLineEnd}`.padEnd( + BOX_WIDTH + 1 + ) + "║", + "╚" + "═".repeat(BOX_WIDTH) + "╝", + "", + ].join("\n"); +} + +/** + * Finds the line number containing the test name in the TST script + */ +export function findTestNameLine(tstContent: string, testName: string): number { + const lines = tstContent.split("\n"); + const searchPattern = `TEST.NAME:${testName}`; + + for (let i = 0; i < lines.length; i++) { + if (lines[i].includes(searchPattern)) { + return i; + } + } + + return 0; // Default to top of file if not found +} + +/** + * Highlights the critical lines in the source file + */ +export function highlightCriticalLines( + editor: vscode.TextEditor, + document: vscode.TextDocument, + reqData: RequirementData +): void { + // Dispose previous highlight if exists + if (activeHighlightDecoration) { + activeHighlightDecoration.dispose(); + } + + // Create highlight decoration + activeHighlightDecoration = vscode.window.createTextEditorDecorationType({ + backgroundColor: "rgba(0,255,0,0.15)", + before: { + contentText: "", + border: "4px solid", + borderColor: "#00ff00", + margin: "0 10px 0 0", + }, + }); + + // Apply highlight to critical lines + const blockRange = new vscode.Range( + new vscode.Position(reqData.importantLineStart - 1, 0), + new vscode.Position( + reqData.importantLineEnd - 1, + document.lineAt(reqData.importantLineEnd - 1).text.length + ) + ); + + editor.setDecorations(activeHighlightDecoration, [blockRange]); +} + +/** + * Shows the requirement info box as a peek window + * Automatically clears highlights when the peek window is closed + */ +export async function showRequirementPeekBox( + sourceFileUri: vscode.Uri, + peekPosition: vscode.Position, + reqData: RequirementData, + context: vscode.ExtensionContext +): Promise { + const virtualDocUri = vscode.Uri.parse("requirement-info:Requirement Info"); + const infoBoxContent = createRequirementInfoBox(reqData); + + // Create content provider for the virtual document + const provider = new (class implements vscode.TextDocumentContentProvider { + provideTextDocumentContent(): string { + return infoBoxContent; + } + })(); + + const providerDisposable = + vscode.workspace.registerTextDocumentContentProvider( + "requirement-info", + provider + ); + + await vscode.workspace.openTextDocument(virtualDocUri); + + // Show peek window + await vscode.commands.executeCommand( + "editor.action.peekLocations", + sourceFileUri, + peekPosition, + [new vscode.Location(virtualDocUri, new vscode.Position(0, 0))], + "peek" + ); + + // Disable line numbers in peek window + setTimeout(() => { + for (const editor of vscode.window.visibleTextEditors) { + if (editor.document.uri.scheme === "requirement-info") { + editor.options = { + ...editor.options, + lineNumbers: vscode.TextEditorLineNumbersStyle.Off, + }; + } + } + }, 0); + + // Listen for when the peek window is closed and clear highlights + const disposable = vscode.window.onDidChangeVisibleTextEditors((editors) => { + const peekWindowOpen = editors.some( + (editor) => editor.document.uri.scheme === "requirement-info" + ); + + if (!peekWindowOpen && activeHighlightDecoration) { + activeHighlightDecoration.dispose(); + setActiveHighlightDecoration(null); + disposable.dispose(); // Clean up this listener + } + }); + + context.subscriptions.push(disposable); + + // Clean up provider after peek window is shown + setTimeout(() => { + providerDisposable.dispose(); + }, 1000); +} + +/** + * Opens the source file with requirement highlighting + */ +export async function openSourceFileWithHighlight( + sourceFilePath: string, + reqData: RequirementData, + context: vscode.ExtensionContext +): Promise { + const sourceFileUri = vscode.Uri.file(sourceFilePath); + const document = await vscode.workspace.openTextDocument(sourceFileUri); + + // Position cursor near the requirement line + const peekPosition = new vscode.Position( + Math.max(0, reqData.lineNumber - 2), + 0 + ); + + // Open document + const editor = await vscode.window.showTextDocument(document, { + preview: false, + preserveFocus: false, + selection: new vscode.Range(peekPosition, peekPosition), + }); + + highlightCriticalLines(editor, document, reqData); + await showRequirementPeekBox(sourceFileUri, peekPosition, reqData, context); +} + +/** + * Opens the TST script and jumps to the test definition + */ +export async function openTstScriptAtTest( + testNode: testNodeType, + scriptPath: string +): Promise { + const commandStatus = await dumpTestScriptFile(testNode, scriptPath); + + if (commandStatus.errorCode !== 0) { + return; + } + + convertTestScriptContents(scriptPath); + + const tstScriptUri = vscode.Uri.file(scriptPath); + const tstDocument = await vscode.workspace.openTextDocument(tstScriptUri); + + // Find the test name line in the script + let targetLine = 0; + if (testNode.testName) { + const tstContent = tstDocument.getText(); + targetLine = findTestNameLine(tstContent, testNode.testName); + } + + // Open document and jump to test definition + const position = new vscode.Position(targetLine, 0); + const selection = new vscode.Range(position, position); + + await vscode.window.showTextDocument(tstDocument, { + viewColumn: vscode.ViewColumn.Beside, + selection: selection, + preview: false, + preserveFocus: false, + }); +} + +export function setActiveHighlightDecoration( + decoration: vscode.TextEditorDecorationType | null +): void { + activeHighlightDecoration = decoration; +} diff --git a/src/vcastUtilities.ts b/src/vcastUtilities.ts index c4e8c7bc..50ff3e53 100644 --- a/src/vcastUtilities.ts +++ b/src/vcastUtilities.ts @@ -166,7 +166,7 @@ function insertIncludePath(filePath: string) { fs.writeFileSync(filePath, existingJSONasString); } -function convertTestScriptContents(scriptPath: string) { +export function convertTestScriptContents(scriptPath: string) { // Read the file let originalLines = fs.readFileSync(scriptPath).toString().split(os.EOL); let newLines: string[] = []; From ec4816c601c7d3398b874dcb87fed7eb2fdd7c3d Mon Sep 17 00:00:00 2001 From: Den1552 Date: Tue, 3 Feb 2026 13:21:31 +0100 Subject: [PATCH 03/20] Real Data Review --- src/editorDecorator.ts | 1 + src/extension.ts | 147 +++++++++++++++++++-- src/requirements/requirementsOperations.ts | 5 + src/requirements/requirementsUtils.ts | 5 +- src/testData.ts | 3 + src/testPane.ts | 1 + 6 files changed, 150 insertions(+), 12 deletions(-) diff --git a/src/editorDecorator.ts b/src/editorDecorator.ts index 9719e6a0..c5e9139b 100644 --- a/src/editorDecorator.ts +++ b/src/editorDecorator.ts @@ -146,6 +146,7 @@ export function buildTestNodeForFunction(args: any): testNodeType | undefined { enviroName: unitData.enviroName, unitName: unitData.unitName, functionName: functionName, + notes: "", testName: "", testFile: "", testStartLine: 0, diff --git a/src/extension.ts b/src/extension.ts index 3ae86976..8a20f85a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -138,6 +138,7 @@ import { RequirementData, requirementsFileWatcher, updateRequirementsAvailability, + spawnWithVcastEnv, } from "./requirements/requirementsUtils"; import { @@ -146,7 +147,11 @@ import { generateTestsFromRequirements, importRequirementsFromGateway, initializeReqs2X, + LLM2CHECK_EXECUTABLE_PATH, + logCliError, + logCliOperation, populateRequirementsGateway, + TEST2CHECK_EXECUTABLE_PATH, } from "./requirements/requirementsOperations"; import { @@ -1330,19 +1335,140 @@ function configureExtension(context: vscode.ExtensionContext) { // Get environment and unit data const { enviroPath, unitName } = testNode; const envData = await getEnvironmentData(enviroPath); + const envRGWPath = findRelevantRequirementGateway(enviroPath); + const testName = testNode.testName; + if (!envData?.unitData) return; - // TODO: Replace with actual requirement data from environment - const reqData: RequirementData = { - title: "REQ-069: Session Timeout", - description: - "The system shall automatically terminate user sessions after 30 minutes of inactivity to ensure security.", - lineNumber: 74, - importantLineStart: 75, - importantLineEnd: 78, - coverageStatus: "covered", - }; + // Execute llm2check to get requirement coverage data + let reqData: RequirementData | null = null; + + if (envRGWPath && testName) { + const commandArgs = [ + "-e", + enviroPath, + envRGWPath, + "-f", + testName, + "--json", + ]; + + const commandString = `${TEST2CHECK_EXECUTABLE_PATH} ${commandArgs.join(" ")}`; + logCliOperation(`Executing command: ${commandString}`); + + try { + const process = await spawnWithVcastEnv( + TEST2CHECK_EXECUTABLE_PATH, + commandArgs + ); + + const stdoutData: string[] = []; + const stderrData: string[] = []; + + process.stdout.on("data", (data: { toString: () => string }) => { + stdoutData.push(data.toString()); + }); + + process.stderr.on("data", (data: { toString: () => string }) => { + stderrData.push(data.toString()); + logCliError(`test2check: ${data.toString()}`); + }); + + await new Promise((resolve, reject) => { + process.on("close", (code: number) => { + if (code === 0) { + logCliOperation( + `test2check completed successfully with code ${code}` + ); + resolve(); + } else { + const errorMessage = `Error: test2check exited with code ${code}`; + logCliError(errorMessage); + reject(new Error(errorMessage)); + } + }); + }); + + // Parse JSON output + const output = stdoutData.join(""); + if (output.trim()) { + try { + const jsonData = JSON.parse(output); + + // llm2check returns an array of test results + if (Array.isArray(jsonData) && jsonData.length > 0) { + const testResult = jsonData[0]; // Get first test result + + // Extract expected coverage for the current unit + const expectedCoverage = testResult.expected_coverage || {}; + const actualCoverage = testResult.actual_coverage || []; + // Find coverage data for the current unit + const unitCoverage = actualCoverage.find( + (cov: any) => cov.unit === unitName + ); + + // Get expected coverage lines for this unit + let expectedLines: number[] = []; + for (const funcKey in expectedCoverage) { + const funcCoverage = expectedCoverage[funcKey]; + if (Array.isArray(funcCoverage)) { + const unitExpected = funcCoverage.find( + (cov: any) => cov.unit === unitName + ); + if (unitExpected?.lines) { + expectedLines = unitExpected.lines; + } + } + } + + // Determine line highlighting range + const minLine = + expectedLines.length > 0 ? Math.min(...expectedLines) : 0; + const maxLine = + expectedLines.length > 0 ? Math.max(...expectedLines) : 0; + + // Determine coverage status + let status = "covered"; + if (testResult.name?.includes("REVIEW-NEEDED")) { + status = "review-needed"; + } + + reqData = { + title: testResult.name || testName, + description: `${testNode.notes}`, + lineNumber: minLine, + importantLineStart: minLine, + importantLineEnd: maxLine, + coverageStatus: status, + // Store full data for reference + expectedLines: expectedLines, + actualLines: unitCoverage?.lines || [], + fullData: testResult, + }; + } + } catch (parseError) { + logCliError( + `Failed to parse llm2check JSON output: ${parseError}` + ); + } + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + vscode.window.showWarningMessage( + `Failed to get requirement data: ${errorMessage}` + ); + logCliError(`llm2check error: ${errorMessage}`); + } + } + + if (!reqData) { + vscode.window.showWarningMessage( + `Failed to retrieve requirements test data. Aborting Test Review.` + ); + return; + } // Find the matching unit's source file const matchingUnit = envData.unitData.find((unit: { path: string }) => { if (!unit.path) return false; @@ -1356,7 +1482,6 @@ function configureExtension(context: vscode.ExtensionContext) { await vscode.commands.executeCommand("workbench.action.closeSidebar"); // Open source file with requirement highlighting - // Pass context so it can register the peek window close listener await openSourceFileWithHighlight(matchingUnit.path, reqData, context); // Open TST script beside source file diff --git a/src/requirements/requirementsOperations.ts b/src/requirements/requirementsOperations.ts index ceab16d7..11da0a97 100644 --- a/src/requirements/requirementsOperations.ts +++ b/src/requirements/requirementsOperations.ts @@ -22,6 +22,7 @@ let CODE2REQS_EXECUTABLE_PATH: string; let REQS2TESTS_EXECUTABLE_PATH: string; let PANREQ_EXECUTABLE_PATH: string; +export let TEST2CHECK_EXECUTABLE_PATH: string; export let LLM2CHECK_EXECUTABLE_PATH: string; // Add a new output channel for CLI operations @@ -173,6 +174,10 @@ function setupReqs2XExecutablePaths(context: vscode.ExtensionContext): boolean { baseUri, exeFilename("llm2check") ).fsPath; + TEST2CHECK_EXECUTABLE_PATH = vscode.Uri.joinPath( + baseUri, + exeFilename("tests2check") + ).fsPath; return true; } diff --git a/src/requirements/requirementsUtils.ts b/src/requirements/requirementsUtils.ts index 1e36e82e..7ca7e8d2 100644 --- a/src/requirements/requirementsUtils.ts +++ b/src/requirements/requirementsUtils.ts @@ -604,7 +604,10 @@ export interface RequirementData { lineNumber: number; importantLineStart: number; importantLineEnd: number; - coverageStatus: "covered" | "partially-covered" | "uncovered"; + coverageStatus: any; + expectedLines: any; + actualLines: any; + fullData: any; } // State Management diff --git a/src/testData.ts b/src/testData.ts index d1d01b1d..d6da90f9 100644 --- a/src/testData.ts +++ b/src/testData.ts @@ -42,6 +42,7 @@ export interface testNodeType { // initially will be used for coded-tests testFile: string; testStartLine: number; + notes: string; } // this is a lookup table for the nodes in the test tree // the key is the nodeID, the data is an testNodeType @@ -58,6 +59,7 @@ export function createTestNodeInCache( functionName: string = "", testName: string = "", testFile: string = "", + notes: string = "", testStartLine: number = 1 ) { let testNode: testNodeType = { @@ -68,6 +70,7 @@ export function createTestNodeInCache( functionName: functionName, testName: testName, testFile: testFile, + notes: notes, testStartLine: testStartLine, }; // set will over-write if nodeID exists diff --git a/src/testPane.ts b/src/testPane.ts index 4d6183cf..3c7281fe 100644 --- a/src/testPane.ts +++ b/src/testPane.ts @@ -186,6 +186,7 @@ function addTestNodes( testNodeForCache.testName = testName; testNodeForCache.testFile = testData.testFile; testNodeForCache.testStartLine = testData.testStartLine; + testNodeForCache.notes = testData.notes; addTestNodeToCache(testNodeID, testNodeForCache); globalTestStatusArray[testNodeID] = testData; From eab0ad5278598019f470b95c2962bd529d386af7 Mon Sep 17 00:00:00 2001 From: Den1552 Date: Tue, 3 Feb 2026 15:06:20 +0100 Subject: [PATCH 04/20] Implemented Coverage --- src/coverage.ts | 165 +++++++++++++++++++++ src/extension.ts | 199 +++++++++++++------------- src/requirements/requirementsUtils.ts | 124 ++++++++++++---- 3 files changed, 363 insertions(+), 125 deletions(-) diff --git a/src/coverage.ts b/src/coverage.ts index e4618054..191d31e0 100644 --- a/src/coverage.ts +++ b/src/coverage.ts @@ -83,6 +83,8 @@ export function initializeCodeCoverageFeatures( //fontWeight: "bold", gutterIconPath: context.asAbsolutePath("./images/light/cover-icon.svg"), }; + + initializeReviewModeDecorations(context); } // global decoration arrays @@ -183,6 +185,13 @@ export async function updateCOVdecorations() { ) { const filePath = url.fileURLToPath(activeEditor.document.uri.toString()); + // Check if we're in review mode for this file + if (isReviewModeActive() && filePath === getReviewModeFilePath()) { + // In review mode, use review mode decorations instead + updateReviewModeDecorations(); + return; + } + // this returns the cached coverage data for this file const coverageData = getCoverageDataForFile(filePath); @@ -331,3 +340,159 @@ export async function toggleCoverageAction() { export async function updateDisplayedCoverage() { if (coverageOn) await updateCOVdecorations(); } + +// Review mode state +let reviewModeActive: boolean = false; +let reviewModeTestName: string | null = null; +let reviewModeExpectedLines: number[] = []; +let reviewModeActualLines: number[] = []; +let reviewModeFilePath: string | null = null; + +// Review mode decoration types +let reviewCoveredDecorationType: TextEditorDecorationType; +let reviewUncoveredDecorationType: TextEditorDecorationType; + +let reviewCoveredRenderOptions: DecorationRenderOptions; +let reviewUncoveredRenderOptions: DecorationRenderOptions; + +export function initializeReviewModeDecorations( + context: vscode.ExtensionContext +) { + reviewUncoveredRenderOptions = { + gutterIconPath: context.asAbsolutePath("./images/light/no-cover-icon.svg"), + }; + + reviewCoveredRenderOptions = { + gutterIconPath: context.asAbsolutePath("./images/light/cover-icon.svg"), + }; +} + +export function isReviewModeActive(): boolean { + return reviewModeActive; +} + +export function getReviewModeFilePath(): string | null { + return reviewModeFilePath; +} + +export function enterReviewMode( + testName: string, + expectedLines: number[], + actualLines: number[], + filePath: string +): void { + reviewModeActive = true; + reviewModeTestName = testName; + reviewModeExpectedLines = expectedLines; + reviewModeActualLines = actualLines; + reviewModeFilePath = filePath; + + resetGlobalDecorations(); + + // Immediately update decorations for the active editor + updateReviewModeDecorations(); +} + +export async function exitReviewMode(): Promise { + reviewModeActive = false; + reviewModeTestName = null; + reviewModeExpectedLines = []; + reviewModeActualLines = []; + reviewModeFilePath = null; + + // Clear review mode decorations + clearReviewModeDecorations(); + + // restore normal coverage + if (coverageOn) { + await updateCOVdecorations(); + } +} + +function clearReviewModeDecorations(): void { + if (reviewCoveredDecorationType) { + reviewCoveredDecorationType.dispose(); + } + if (reviewUncoveredDecorationType) { + reviewUncoveredDecorationType.dispose(); + } +} + +export function updateReviewModeDecorations(): void { + if (!reviewModeActive) { + return; + } + + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + return; + } + + // Only apply review decorations to the specific file + if (activeEditor.document.uri.fsPath !== reviewModeFilePath) { + return; + } + + // Clear previous decorations + clearReviewModeDecorations(); + + const filePath = activeEditor.document.uri.fsPath; + const coverageData = getCoverageDataForFile(filePath); + + if (!coverageData.hasCoverageData) { + return; + } + + // Base coverage data (ONLY coverable lines) + const covered = new Set(coverageData.covered); + const uncovered = new Set(coverageData.uncovered); + const partiallyCovered = new Set(coverageData.partiallyCovered); + + // Lines that should count as "covered" in review mode + const expectedOrActual = new Set([ + ...reviewModeExpectedLines, + ...reviewModeActualLines, + ]); + + // Any covered line NOT in expected/actual becomes uncovered + for (const line of covered) { + if (!expectedOrActual.has(line - 1)) { + covered.delete(line); + uncovered.add(line); + } + } + + const coveredDecorations: vscode.DecorationOptions[] = []; + const uncoveredDecorations: vscode.DecorationOptions[] = []; + + // Only iterate over lines that actually have coverage data + const allCoverableLines = new Set([ + ...covered, + ...uncovered, + ...partiallyCovered, + ]); + + for (const lineNumber of allCoverableLines) { + const lineIndex = lineNumber - 1; + if (covered.has(lineNumber)) { + coveredDecorations.push(getRangeOption(lineIndex)); + } else { + // uncovered + partial both render as uncovered in review mode + uncoveredDecorations.push(getRangeOption(lineIndex)); + } + } + + // Apply decorations + reviewCoveredDecorationType = vscode.window.createTextEditorDecorationType( + reviewCoveredRenderOptions + ); + reviewUncoveredDecorationType = vscode.window.createTextEditorDecorationType( + reviewUncoveredRenderOptions + ); + + activeEditor.setDecorations(reviewCoveredDecorationType, coveredDecorations); + activeEditor.setDecorations( + reviewUncoveredDecorationType, + uncoveredDecorations + ); +} diff --git a/src/extension.ts b/src/extension.ts index 8a20f85a..78a33480 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1344,123 +1344,119 @@ function configureExtension(context: vscode.ExtensionContext) { let reqData: RequirementData | null = null; if (envRGWPath && testName) { - const commandArgs = [ - "-e", - enviroPath, - envRGWPath, - "-f", - testName, - "--json", - ]; + reqData = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Retrieving requirement coverage data", + cancellable: false, + }, + async (progress) => { + progress.report({ message: "Running test2check…" }); + + if (!envRGWPath || !testName) { + return null; + } - const commandString = `${TEST2CHECK_EXECUTABLE_PATH} ${commandArgs.join(" ")}`; - logCliOperation(`Executing command: ${commandString}`); + const commandArgs = [ + "-e", + enviroPath, + envRGWPath, + "-f", + testName, + "--json", + ]; - try { - const process = await spawnWithVcastEnv( - TEST2CHECK_EXECUTABLE_PATH, - commandArgs - ); + try { + const process = await spawnWithVcastEnv( + TEST2CHECK_EXECUTABLE_PATH, + commandArgs + ); - const stdoutData: string[] = []; - const stderrData: string[] = []; + const stdoutData: string[] = []; + const stderrData: string[] = []; - process.stdout.on("data", (data: { toString: () => string }) => { - stdoutData.push(data.toString()); - }); + process.stdout.on("data", (data) => { + stdoutData.push(data.toString()); + }); - process.stderr.on("data", (data: { toString: () => string }) => { - stderrData.push(data.toString()); - logCliError(`test2check: ${data.toString()}`); - }); + process.stderr.on("data", (data) => { + stderrData.push(data.toString()); + logCliError(`test2check: ${data.toString()}`); + }); - await new Promise((resolve, reject) => { - process.on("close", (code: number) => { - if (code === 0) { - logCliOperation( - `test2check completed successfully with code ${code}` - ); - resolve(); - } else { - const errorMessage = `Error: test2check exited with code ${code}`; - logCliError(errorMessage); - reject(new Error(errorMessage)); - } - }); - }); + await new Promise((resolve, reject) => { + process.on("close", (code: number) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`test2check exited with code ${code}`)); + } + }); + }); - // Parse JSON output - const output = stdoutData.join(""); - if (output.trim()) { - try { - const jsonData = JSON.parse(output); + progress.report({ message: "Processing coverage results…" }); - // llm2check returns an array of test results - if (Array.isArray(jsonData) && jsonData.length > 0) { - const testResult = jsonData[0]; // Get first test result + const output = stdoutData.join(""); + if (!output.trim()) return null; - // Extract expected coverage for the current unit - const expectedCoverage = testResult.expected_coverage || {}; - const actualCoverage = testResult.actual_coverage || []; + const jsonData = JSON.parse(output); + if (!Array.isArray(jsonData) || jsonData.length === 0) + return null; - // Find coverage data for the current unit - const unitCoverage = actualCoverage.find( - (cov: any) => cov.unit === unitName - ); + const testResult = jsonData[0]; + const expectedCoverage = testResult.expected_coverage || {}; + const actualCoverage = testResult.actual_coverage || []; - // Get expected coverage lines for this unit - let expectedLines: number[] = []; - for (const funcKey in expectedCoverage) { - const funcCoverage = expectedCoverage[funcKey]; - if (Array.isArray(funcCoverage)) { - const unitExpected = funcCoverage.find( - (cov: any) => cov.unit === unitName - ); - if (unitExpected?.lines) { - expectedLines = unitExpected.lines; - } + const unitCoverage = actualCoverage.find( + (cov: any) => cov.unit === unitName + ); + + let expectedLines: number[] = []; + for (const funcKey in expectedCoverage) { + const funcCoverage = expectedCoverage[funcKey]; + if (Array.isArray(funcCoverage)) { + const unitExpected = funcCoverage.find( + (cov: any) => cov.unit === unitName + ); + if (unitExpected?.lines) { + expectedLines = unitExpected.lines; } } + } - // Determine line highlighting range - const minLine = - expectedLines.length > 0 ? Math.min(...expectedLines) : 0; - const maxLine = - expectedLines.length > 0 ? Math.max(...expectedLines) : 0; - - // Determine coverage status - let status = "covered"; - if (testResult.name?.includes("REVIEW-NEEDED")) { - status = "review-needed"; - } + const minLine = expectedLines.length + ? Math.min(...expectedLines) + : 0; + const maxLine = expectedLines.length + ? Math.max(...expectedLines) + : 0; - reqData = { - title: testResult.name || testName, - description: `${testNode.notes}`, - lineNumber: minLine, - importantLineStart: minLine, - importantLineEnd: maxLine, - coverageStatus: status, - // Store full data for reference - expectedLines: expectedLines, - actualLines: unitCoverage?.lines || [], - fullData: testResult, - }; + let status = "covered"; + if (testResult.name?.includes("REVIEW-NEEDED")) { + status = "review-needed"; } - } catch (parseError) { - logCliError( - `Failed to parse llm2check JSON output: ${parseError}` + + return { + title: testResult.name || testName, + description: `${testNode.notes}`, + lineNumber: minLine, + importantLineStart: minLine, + importantLineEnd: maxLine, + coverageStatus: status, + expectedLines, + actualLines: unitCoverage?.lines || [], + fullData: testResult, + }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + vscode.window.showWarningMessage( + `Failed to get requirement data: ${msg}` ); + logCliError(msg); + return null; } } - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - vscode.window.showWarningMessage( - `Failed to get requirement data: ${errorMessage}` - ); - logCliError(`llm2check error: ${errorMessage}`); - } + ); } if (!reqData) { @@ -1482,7 +1478,12 @@ function configureExtension(context: vscode.ExtensionContext) { await vscode.commands.executeCommand("workbench.action.closeSidebar"); // Open source file with requirement highlighting - await openSourceFileWithHighlight(matchingUnit.path, reqData, context); + await openSourceFileWithHighlight( + matchingUnit.path, + reqData, + context, + testName + ); // Open TST script beside source file const scriptPath = testNode.enviroPath + ".tst"; diff --git a/src/requirements/requirementsUtils.ts b/src/requirements/requirementsUtils.ts index 7ca7e8d2..6d4655d3 100644 --- a/src/requirements/requirementsUtils.ts +++ b/src/requirements/requirementsUtils.ts @@ -1,9 +1,18 @@ import * as vscode from "vscode"; -import { workspace } from "vscode"; +import { + DecorationRenderOptions, + TextEditorDecorationType, + workspace, +} from "vscode"; import { ChildProcessWithoutNullStreams, spawn } from "node:child_process"; import { parse as csvParse } from "csv-parse/sync"; import { vcastInstallationDirectory } from "../vcastInstallation"; -import { exeFilename, normalizePath, showSettings } from "../utilities"; +import { + exeFilename, + getRangeOption, + normalizePath, + showSettings, +} from "../utilities"; import { LLM2CHECK_EXECUTABLE_PATH, logCliError, @@ -13,6 +22,11 @@ import { makeEnviroNodeID } from "../testPane"; import { dumpTestScriptFile } from "../vcastAdapter"; import { convertTestScriptContents } from "../vcastUtilities"; import { testNodeType } from "../testData"; +import { + enterReviewMode, + exitReviewMode, + updateDisplayedCoverage, +} from "../coverage"; const path = require("path"); const fs = require("fs"); @@ -642,22 +656,54 @@ export function wrapText(text: string, width: number): string[] { * Creates a formatted text box containing requirement information */ export function createRequirementInfoBox(reqData: RequirementData): string { - const BOX_WIDTH = 66; - const wrappedDescription = wrapText(reqData.description, BOX_WIDTH - 6); + const BOX_WIDTH = 66; // inner width (between borders) + const TEXT_WIDTH = BOX_WIDTH - 4; // "║ " + text + " ║" + + const topBorder = "╔" + "═".repeat(BOX_WIDTH) + "╗"; + const midBorder = "╠" + "═".repeat(BOX_WIDTH) + "╣"; + const bottomBorder = "╚" + "═".repeat(BOX_WIDTH) + "╝"; + const emptyLine = "║" + " ".repeat(BOX_WIDTH) + "║"; + + const formatLine = (text = "") => "║ " + text.padEnd(TEXT_WIDTH) + " ║"; + + /** + * Wrap text that may contain newlines into boxed lines + */ + const formatMultilineText = (text: string): string[] => { + const paragraphs = text.split(/\r?\n/); + + const lines: string[] = []; + + for (const para of paragraphs) { + if (!para.trim()) { + // preserve blank lines as empty boxed lines + lines.push(formatLine()); + continue; + } + + const wrapped = wrapText(para, TEXT_WIDTH); + wrapped.forEach((w) => lines.push(formatLine(w))); + } + + return lines; + }; return [ "", - "╔" + "═".repeat(BOX_WIDTH) + "╗", - `║ ${reqData.title.padEnd(BOX_WIDTH - 2)}║`, - "╠" + "═".repeat(BOX_WIDTH) + "╣", - "║ DESCRIPTION".padEnd(BOX_WIDTH + 1) + "║", - "║ " + "─".repeat(BOX_WIDTH - 2) + "║", - ...wrappedDescription.map((line) => `║ ${line.padEnd(BOX_WIDTH - 4)} ║`), - "║".padEnd(BOX_WIDTH + 1) + "║", - `║ Critical Lines : ${reqData.importantLineStart} - ${reqData.importantLineEnd}`.padEnd( - BOX_WIDTH + 1 - ) + "║", - "╚" + "═".repeat(BOX_WIDTH) + "╝", + topBorder, + formatLine(reqData.title), + midBorder, + formatLine("DESCRIPTION"), + formatLine("─".repeat(TEXT_WIDTH)), + + // Description body (fully boxed, paragraph-safe) + ...formatMultilineText(reqData.description), + + emptyLine, + formatLine( + `Critical Lines : ${reqData.importantLineStart} - ${reqData.importantLineEnd}` + ), + bottomBorder, "", ].join("\n"); } @@ -763,18 +809,29 @@ export async function showRequirementPeekBox( } }, 0); - // Listen for when the peek window is closed and clear highlights - const disposable = vscode.window.onDidChangeVisibleTextEditors((editors) => { - const peekWindowOpen = editors.some( - (editor) => editor.document.uri.scheme === "requirement-info" - ); + // Listen for when the peek window is closed and clear highlights + exit review mode + const disposable = vscode.window.onDidChangeVisibleTextEditors( + async (editors) => { + const peekWindowOpen = editors.some( + (editor) => editor.document.uri.scheme === "requirement-info" + ); + + if (!peekWindowOpen) { + // Clear the highlight decoration + if (activeHighlightDecoration) { + activeHighlightDecoration.dispose(); + setActiveHighlightDecoration(null); + } - if (!peekWindowOpen && activeHighlightDecoration) { - activeHighlightDecoration.dispose(); - setActiveHighlightDecoration(null); - disposable.dispose(); // Clean up this listener + // Exit review mode and refresh normal coverage + await exitReviewMode(); + updateDisplayedCoverage(); + + // Clean up this listener + disposable.dispose(); + } } - }); + ); context.subscriptions.push(disposable); @@ -790,7 +847,8 @@ export async function showRequirementPeekBox( export async function openSourceFileWithHighlight( sourceFilePath: string, reqData: RequirementData, - context: vscode.ExtensionContext + context: vscode.ExtensionContext, + testName: string ): Promise { const sourceFileUri = vscode.Uri.file(sourceFilePath); const document = await vscode.workspace.openTextDocument(sourceFileUri); @@ -808,8 +866,22 @@ export async function openSourceFileWithHighlight( selection: new vscode.Range(peekPosition, peekPosition), }); + // Enter review mode BEFORE applying decorations + enterReviewMode( + testName, + reqData.expectedLines || [], + reqData.actualLines || [], + sourceFilePath + ); + + // Apply the green highlight to critical lines highlightCriticalLines(editor, document, reqData); + + // Show the peek box await showRequirementPeekBox(sourceFileUri, peekPosition, reqData, context); + + // Update coverage decorations to show review mode coverage + await updateDisplayedCoverage(); } /** From ebb08e61b7cfa6015df73840d770b5775c7fb035 Mon Sep 17 00:00:00 2001 From: Den1552 Date: Tue, 3 Feb 2026 17:58:13 +0100 Subject: [PATCH 05/20] Added reqs check for each testnode --- package.json | 2 +- python/vTestInterface.py | 1 + src/editorDecorator.ts | 1 + src/extension.ts | 3 +++ src/testData.ts | 3 +++ src/testPane.ts | 14 ++++++++++++++ src/vcastTestInterface.ts | 1 + 7 files changed, 24 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3eb460ef..435453da 100644 --- a/package.json +++ b/package.json @@ -1092,7 +1092,7 @@ { "command": "vectorcastTestExplorer.openReqsCoverageReview", "group": "vcast.enviroManagement", - "when": "testId =~ /^vcast:.*$/ && !(testId =~ /^.*<>.*$/) && !(testId =~ /^.*<>.*$/) && !(testId =~ /.*coded_tests_driver.*/) && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && testId not in vectorcastTestExplorer.vcastEnviroList" + "when": "testId =~ /^vcast:.*$/ && !(testId =~ /^.*<>.*$/) && !(testId =~ /^.*<>.*$/) && !(testId =~ /.*coded_tests_driver.*/) && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && testId not in vectorcastTestExplorer.vcastEnviroList && testId in vectorcastTestExplorer.testNodesWithRequirements" }, { "command": "vectorcastTestExplorer.insertBasisPathTests", diff --git a/python/vTestInterface.py b/python/vTestInterface.py index 0e5f1bc6..6f289cc4 100644 --- a/python/vTestInterface.py +++ b/python/vTestInterface.py @@ -155,6 +155,7 @@ def generateTestInfo(enviroPath, test): testInfo["time"] = getTime(test.start_time) testInfo["status"] = textStatus(test.status) testInfo["passfail"] = getPassFailString(test) + testInfo["requirements"] = str(test.requirements) # New to support coded tests in vc24 if vpythonHasCodedTestSupport() and test.coded_tests_file: diff --git a/src/editorDecorator.ts b/src/editorDecorator.ts index c5e9139b..05853031 100644 --- a/src/editorDecorator.ts +++ b/src/editorDecorator.ts @@ -146,6 +146,7 @@ export function buildTestNodeForFunction(args: any): testNodeType | undefined { enviroName: unitData.enviroName, unitName: unitData.unitName, functionName: functionName, + requirements: "", notes: "", testName: "", testFile: "", diff --git a/src/extension.ts b/src/extension.ts index 78a33480..9f60bd77 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1488,6 +1488,9 @@ function configureExtension(context: vscode.ExtensionContext) { // Open TST script beside source file const scriptPath = testNode.enviroPath + ".tst"; await openTstScriptAtTest(testNode, scriptPath); + vscode.window.showWarningMessage( + `You are currently in REVIEW Mode for the Requirement Test ${testName}. Only the Coverage for this Test will be shown in the file. In Order to exit this mode, close the Box with the Requirement Description in the Source file.` + ); } ); diff --git a/src/testData.ts b/src/testData.ts index d6da90f9..3929c110 100644 --- a/src/testData.ts +++ b/src/testData.ts @@ -42,6 +42,7 @@ export interface testNodeType { // initially will be used for coded-tests testFile: string; testStartLine: number; + requirements: any; notes: string; } // this is a lookup table for the nodes in the test tree @@ -60,6 +61,7 @@ export function createTestNodeInCache( testName: string = "", testFile: string = "", notes: string = "", + requirements: any = "", testStartLine: number = 1 ) { let testNode: testNodeType = { @@ -71,6 +73,7 @@ export function createTestNodeInCache( testName: testName, testFile: testFile, notes: notes, + requirements: requirements, testStartLine: testStartLine, }; // set will over-write if nodeID exists diff --git a/src/testPane.ts b/src/testPane.ts index 3c7281fe..69fcadf0 100644 --- a/src/testPane.ts +++ b/src/testPane.ts @@ -165,6 +165,7 @@ function addTestNodes( passfail: testList[testIndex].passfail, time: testList[testIndex].time, notes: testList[testIndex].notes, + requirements: testList[testIndex].requirements, resultFilePath: "", stdout: "", compoundOnly: testList[testIndex].compoundOnly, @@ -187,10 +188,16 @@ function addTestNodes( testNodeForCache.testFile = testData.testFile; testNodeForCache.testStartLine = testData.testStartLine; testNodeForCache.notes = testData.notes; + testNodeForCache.requirements = testData.requirements; addTestNodeToCache(testNodeID, testNodeForCache); globalTestStatusArray[testNodeID] = testData; + // TODO: Make usable also for multiple reqs --> also see data strucutre.. + if (testNodeForCache.requirements !== "[]") { + testNodesWithRequirements.push(testNodeID); + } + // currently we only use the Uri and Range for Coded Tests let testURI: vscode.Uri | undefined = undefined; let testRange: vscode.Range | undefined = undefined; @@ -235,6 +242,12 @@ function addTestNodes( vcastHasCodedTestsList ); + vscode.commands.executeCommand( + "setContext", + "vectorcastTestExplorer.testNodesWithRequirements", + testNodesWithRequirements + ); + addTestNodeToCache(parentNodeID, parentNodeForCache); } @@ -701,6 +714,7 @@ export function makeEnviroNodeID(buildDirectory: string): string { } let vcastHasCodedTestsList: string[] = []; +let testNodesWithRequirements: string[] = []; // Global cache for workspace-wide env data // Used to avoid redundant API calls during refresh diff --git a/src/vcastTestInterface.ts b/src/vcastTestInterface.ts index bf107ad5..bfefb616 100644 --- a/src/vcastTestInterface.ts +++ b/src/vcastTestInterface.ts @@ -149,6 +149,7 @@ export interface testDataType { resultFilePath: string; stdout: string; notes: string; + requirements: any; compoundOnly: boolean; testFile: string; testStartLine: number; From 85d2654caf409ec4bb850eccfa2165b494085302 Mon Sep 17 00:00:00 2001 From: Den1552 Date: Tue, 3 Feb 2026 18:04:21 +0100 Subject: [PATCH 06/20] Removed unused imports --- src/coverage.ts | 3 --- src/extension.ts | 4 ---- src/requirements/requirementsUtils.ts | 13 ++----------- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/coverage.ts b/src/coverage.ts index 191d31e0..120dcf46 100644 --- a/src/coverage.ts +++ b/src/coverage.ts @@ -343,7 +343,6 @@ export async function updateDisplayedCoverage() { // Review mode state let reviewModeActive: boolean = false; -let reviewModeTestName: string | null = null; let reviewModeExpectedLines: number[] = []; let reviewModeActualLines: number[] = []; let reviewModeFilePath: string | null = null; @@ -382,7 +381,6 @@ export function enterReviewMode( filePath: string ): void { reviewModeActive = true; - reviewModeTestName = testName; reviewModeExpectedLines = expectedLines; reviewModeActualLines = actualLines; reviewModeFilePath = filePath; @@ -395,7 +393,6 @@ export function enterReviewMode( export async function exitReviewMode(): Promise { reviewModeActive = false; - reviewModeTestName = null; reviewModeExpectedLines = []; reviewModeActualLines = []; reviewModeFilePath = null; diff --git a/src/extension.ts b/src/extension.ts index 9f60bd77..581aca2f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -79,7 +79,6 @@ import { forceLowerCaseDriveLetter, decodeVar, getFullEnvReport, - requirementsTestData, } from "./utilities"; import { @@ -90,7 +89,6 @@ import { rebuildEnvironment, openProjectInVcast, deleteLevel, - dumpTestScriptFile, } from "./vcastAdapter"; import { @@ -147,9 +145,7 @@ import { generateTestsFromRequirements, importRequirementsFromGateway, initializeReqs2X, - LLM2CHECK_EXECUTABLE_PATH, logCliError, - logCliOperation, populateRequirementsGateway, TEST2CHECK_EXECUTABLE_PATH, } from "./requirements/requirementsOperations"; diff --git a/src/requirements/requirementsUtils.ts b/src/requirements/requirementsUtils.ts index 6d4655d3..ddc10bd9 100644 --- a/src/requirements/requirementsUtils.ts +++ b/src/requirements/requirementsUtils.ts @@ -1,18 +1,9 @@ import * as vscode from "vscode"; -import { - DecorationRenderOptions, - TextEditorDecorationType, - workspace, -} from "vscode"; +import { workspace } from "vscode"; import { ChildProcessWithoutNullStreams, spawn } from "node:child_process"; import { parse as csvParse } from "csv-parse/sync"; import { vcastInstallationDirectory } from "../vcastInstallation"; -import { - exeFilename, - getRangeOption, - normalizePath, - showSettings, -} from "../utilities"; +import { exeFilename, normalizePath, showSettings } from "../utilities"; import { LLM2CHECK_EXECUTABLE_PATH, logCliError, From 955a0e7da35db05bcc3cbf0e62233155eed111e5 Mon Sep 17 00:00:00 2001 From: Den1552 Date: Thu, 5 Feb 2026 10:12:39 +0100 Subject: [PATCH 07/20] Fixed Windows Path issue for reqs cov review --- src/coverage.ts | 11 ++++++++--- src/extension.ts | 14 ++++++++++---- src/requirements/requirementsUtils.ts | 1 + 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/coverage.ts b/src/coverage.ts index 120dcf46..51dbe16b 100644 --- a/src/coverage.ts +++ b/src/coverage.ts @@ -9,13 +9,14 @@ import { getListOfFilesWithCoverage, } from "./vcastTestInterface"; -import { getRangeOption } from "./utilities"; +import { getRangeOption, normalizePath } from "./utilities"; import { fileDecorator } from "./fileDecorator"; import { currentActiveUnitMCDCLines, updateCurrentActiveUnitMCDCLines, } from "./editorDecorator"; +import { vectorMessage } from "./messagePane"; // these are defined as globals so that the deactivate function has access // to dispose of them when the coverage id turned off @@ -426,7 +427,11 @@ export function updateReviewModeDecorations(): void { } // Only apply review decorations to the specific file - if (activeEditor.document.uri.fsPath !== reviewModeFilePath) { + if ( + !reviewModeFilePath || + normalizePath(activeEditor.document.uri.fsPath) !== + normalizePath(reviewModeFilePath) + ) { return; } @@ -450,7 +455,7 @@ export function updateReviewModeDecorations(): void { ...reviewModeExpectedLines, ...reviewModeActualLines, ]); - + vectorMessage(`${JSON.stringify(expectedOrActual)}`); // Any covered line NOT in expected/actual becomes uncovered for (const line of covered) { if (!expectedOrActual.has(line - 1)) { diff --git a/src/extension.ts b/src/extension.ts index 581aca2f..391eaf00 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1403,9 +1403,14 @@ function configureExtension(context: vscode.ExtensionContext) { const expectedCoverage = testResult.expected_coverage || {}; const actualCoverage = testResult.actual_coverage || []; - const unitCoverage = actualCoverage.find( - (cov: any) => cov.unit === unitName - ); + const unitCoverage = actualCoverage.find((cov: any) => { + // Remove file extension from both for comparison + const covUnitBase = path.basename( + cov.unit, + path.extname(cov.unit) + ); + return covUnitBase === unitName || cov.unit === unitName; + }); let expectedLines: number[] = []; for (const funcKey in expectedCoverage) { @@ -1435,7 +1440,7 @@ function configureExtension(context: vscode.ExtensionContext) { return { title: testResult.name || testName, description: `${testNode.notes}`, - lineNumber: minLine, + lineNumber: minLine - 1, importantLineStart: minLine, importantLineEnd: maxLine, coverageStatus: status, @@ -1461,6 +1466,7 @@ function configureExtension(context: vscode.ExtensionContext) { ); return; } + // Find the matching unit's source file const matchingUnit = envData.unitData.find((unit: { path: string }) => { if (!unit.path) return false; diff --git a/src/requirements/requirementsUtils.ts b/src/requirements/requirementsUtils.ts index ddc10bd9..8c5f1b3f 100644 --- a/src/requirements/requirementsUtils.ts +++ b/src/requirements/requirementsUtils.ts @@ -18,6 +18,7 @@ import { exitReviewMode, updateDisplayedCoverage, } from "../coverage"; +import { vectorMessage } from "../messagePane"; const path = require("path"); const fs = require("fs"); From a821eb968b872a1db7347c3318afd752b3380926 Mon Sep 17 00:00:00 2001 From: Den1552 Date: Fri, 6 Feb 2026 15:05:42 +0100 Subject: [PATCH 08/20] Added setting, Updated Highlighting, Removed Critical Lines form Box --- package.json | 8 +++++++- src/requirements/requirementsUtils.ts | 15 +-------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 435453da..4faade08 100644 --- a/package.json +++ b/package.json @@ -463,6 +463,12 @@ "type": "string", "description": "Path to folder containing Reqs2X executables (reqs2tests, code2reqs and panreq), if unset search for the executables in the main VectorCAST installation folder instead" }, + "vectorcastTestExplorer.reqs2x.enableRequirementsCoverageReview": { + "order": 2, + "type": "boolean", + "description": "Enable the Requirements Coverage Review feature (requires a Requirements Beta release)", + "default": false + }, "vectorcastTestExplorer.reqs2x.decomposeRequirements": { "type": "boolean", "order": 3, @@ -1092,7 +1098,7 @@ { "command": "vectorcastTestExplorer.openReqsCoverageReview", "group": "vcast.enviroManagement", - "when": "testId =~ /^vcast:.*$/ && !(testId =~ /^.*<>.*$/) && !(testId =~ /^.*<>.*$/) && !(testId =~ /.*coded_tests_driver.*/) && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && testId not in vectorcastTestExplorer.vcastEnviroList && testId in vectorcastTestExplorer.testNodesWithRequirements" + "when": "testId =~ /^vcast:.*$/ && !(testId =~ /^.*<>.*$/) && !(testId =~ /^.*<>.*$/) && !(testId =~ /.*coded_tests_driver.*/) && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && testId not in vectorcastTestExplorer.vcastEnviroList && testId in vectorcastTestExplorer.testNodesWithRequirements && config.vectorcastTestExplorer.reqs2x.enableRequirementsCoverageReview" }, { "command": "vectorcastTestExplorer.insertBasisPathTests", diff --git a/src/requirements/requirementsUtils.ts b/src/requirements/requirementsUtils.ts index 8c5f1b3f..3ee32e98 100644 --- a/src/requirements/requirementsUtils.ts +++ b/src/requirements/requirementsUtils.ts @@ -18,7 +18,6 @@ import { exitReviewMode, updateDisplayedCoverage, } from "../coverage"; -import { vectorMessage } from "../messagePane"; const path = require("path"); const fs = require("fs"); @@ -654,7 +653,6 @@ export function createRequirementInfoBox(reqData: RequirementData): string { const topBorder = "╔" + "═".repeat(BOX_WIDTH) + "╗"; const midBorder = "╠" + "═".repeat(BOX_WIDTH) + "╣"; const bottomBorder = "╚" + "═".repeat(BOX_WIDTH) + "╝"; - const emptyLine = "║" + " ".repeat(BOX_WIDTH) + "║"; const formatLine = (text = "") => "║ " + text.padEnd(TEXT_WIDTH) + " ║"; @@ -690,11 +688,6 @@ export function createRequirementInfoBox(reqData: RequirementData): string { // Description body (fully boxed, paragraph-safe) ...formatMultilineText(reqData.description), - - emptyLine, - formatLine( - `Critical Lines : ${reqData.importantLineStart} - ${reqData.importantLineEnd}` - ), bottomBorder, "", ].join("\n"); @@ -729,15 +722,9 @@ export function highlightCriticalLines( activeHighlightDecoration.dispose(); } - // Create highlight decoration + // Highlight decoration activeHighlightDecoration = vscode.window.createTextEditorDecorationType({ backgroundColor: "rgba(0,255,0,0.15)", - before: { - contentText: "", - border: "4px solid", - borderColor: "#00ff00", - margin: "0 10px 0 0", - }, }); // Apply highlight to critical lines From aad84f85a78b390f7e613bf6aa68f4e16d704a1a Mon Sep 17 00:00:00 2001 From: Den1552 Date: Fri, 6 Feb 2026 15:35:24 +0100 Subject: [PATCH 09/20] Added check for multiple requirements --- src/extension.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/extension.ts b/src/extension.ts index 391eaf00..3ec7c082 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1399,6 +1399,12 @@ function configureExtension(context: vscode.ExtensionContext) { if (!Array.isArray(jsonData) || jsonData.length === 0) return null; + if (jsonData.length > 1) { + vscode.window.showInformationMessage( + "This test is associated with multiple requirements. Coverage Review currently supports only one requirement per test." + ); + return null; + } const testResult = jsonData[0]; const expectedCoverage = testResult.expected_coverage || {}; const actualCoverage = testResult.actual_coverage || []; From 1a1c3f22b8dc9b7c9d30a074305603ded42abe95 Mon Sep 17 00:00:00 2001 From: Den1552 Date: Mon, 9 Feb 2026 08:36:42 +0100 Subject: [PATCH 10/20] Release notes --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25c01bdb..8bdbf7cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to the "vectorcastTestExplorer" extension will be documented in this file. +## [1.0.29] - 2026-02-11 + +### Added +- Added a Test Review for Requirement Tests + ## [1.0.28] - 2026-01-27 ### Added diff --git a/package.json b/package.json index 4faade08..ce9d2058 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vectorcasttestexplorer", "displayName": "VectorCAST Test Explorer", "description": "VectorCAST Test Explorer for VS Code", - "version": "1.0.28", + "version": "1.0.29", "license": "MIT", "repository": { "type": "git", From 6bcce53dd3ee54727cf1a1580e2b5e22383d101a Mon Sep 17 00:00:00 2001 From: Den1552 Date: Mon, 9 Feb 2026 09:04:01 +0100 Subject: [PATCH 11/20] Refactoring --- src/extension.ts | 161 ++++---------------------- src/requirements/requirementsUtils.ts | 131 ++++++++++++++++++++- src/testData.ts | 4 +- src/testPane.ts | 2 +- src/utilities.ts | 5 - 5 files changed, 151 insertions(+), 152 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 3ec7c082..722fdc3e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -137,6 +137,7 @@ import { requirementsFileWatcher, updateRequirementsAvailability, spawnWithVcastEnv, + fetchRequirementCoverageData, } from "./requirements/requirementsUtils"; import { @@ -1316,159 +1317,41 @@ function configureExtension(context: vscode.ExtensionContext) { ); context.subscriptions.push(openSourceFileFromTestpaneCommand); - /** - * Command: Opens the requirements coverage review interface - * Shows source file with highlighting and TST script side-by-side - */ let openReqsCoverageReview = vscode.commands.registerCommand( "vectorcastTestExplorer.openReqsCoverageReview", async (args: any) => { if (!args) return; const testNode: testNodeType = getTestNode(args.id); - if (!testNode) return; + if (!testNode) { + vscode.window.showWarningMessage( + `Failed to retrieve node test data for ${args.id}. Aborting Test Review.` + ); + } - // Get environment and unit data const { enviroPath, unitName } = testNode; const envData = await getEnvironmentData(enviroPath); const envRGWPath = findRelevantRequirementGateway(enviroPath); const testName = testNode.testName; - if (!envData?.unitData) return; - - // Execute llm2check to get requirement coverage data - let reqData: RequirementData | null = null; - - if (envRGWPath && testName) { - reqData = await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: "Retrieving requirement coverage data", - cancellable: false, - }, - async (progress) => { - progress.report({ message: "Running test2check…" }); - - if (!envRGWPath || !testName) { - return null; - } - - const commandArgs = [ - "-e", - enviroPath, - envRGWPath, - "-f", - testName, - "--json", - ]; - - try { - const process = await spawnWithVcastEnv( - TEST2CHECK_EXECUTABLE_PATH, - commandArgs - ); - - const stdoutData: string[] = []; - const stderrData: string[] = []; - - process.stdout.on("data", (data) => { - stdoutData.push(data.toString()); - }); - - process.stderr.on("data", (data) => { - stderrData.push(data.toString()); - logCliError(`test2check: ${data.toString()}`); - }); - - await new Promise((resolve, reject) => { - process.on("close", (code: number) => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`test2check exited with code ${code}`)); - } - }); - }); - - progress.report({ message: "Processing coverage results…" }); - - const output = stdoutData.join(""); - if (!output.trim()) return null; - - const jsonData = JSON.parse(output); - if (!Array.isArray(jsonData) || jsonData.length === 0) - return null; - - if (jsonData.length > 1) { - vscode.window.showInformationMessage( - "This test is associated with multiple requirements. Coverage Review currently supports only one requirement per test." - ); - return null; - } - const testResult = jsonData[0]; - const expectedCoverage = testResult.expected_coverage || {}; - const actualCoverage = testResult.actual_coverage || []; - - const unitCoverage = actualCoverage.find((cov: any) => { - // Remove file extension from both for comparison - const covUnitBase = path.basename( - cov.unit, - path.extname(cov.unit) - ); - return covUnitBase === unitName || cov.unit === unitName; - }); - - let expectedLines: number[] = []; - for (const funcKey in expectedCoverage) { - const funcCoverage = expectedCoverage[funcKey]; - if (Array.isArray(funcCoverage)) { - const unitExpected = funcCoverage.find( - (cov: any) => cov.unit === unitName - ); - if (unitExpected?.lines) { - expectedLines = unitExpected.lines; - } - } - } - - const minLine = expectedLines.length - ? Math.min(...expectedLines) - : 0; - const maxLine = expectedLines.length - ? Math.max(...expectedLines) - : 0; - - let status = "covered"; - if (testResult.name?.includes("REVIEW-NEEDED")) { - status = "review-needed"; - } - - return { - title: testResult.name || testName, - description: `${testNode.notes}`, - lineNumber: minLine - 1, - importantLineStart: minLine, - importantLineEnd: maxLine, - coverageStatus: status, - expectedLines, - actualLines: unitCoverage?.lines || [], - fullData: testResult, - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - vscode.window.showWarningMessage( - `Failed to get requirement data: ${msg}` - ); - logCliError(msg); - return null; - } - } + if (!envData?.unitData || !envRGWPath || !testName) { + vscode.window.showWarningMessage( + "Failed to retrieve environment test data. Aborting Test Review." ); + return; } + const reqData = await fetchRequirementCoverageData( + enviroPath, + envRGWPath, + testName, + unitName, + testNode + ); + if (!reqData) { vscode.window.showWarningMessage( - `Failed to retrieve requirements test data. Aborting Test Review.` + "Failed to retrieve requirements test data. Aborting Test Review." ); return; } @@ -1496,15 +1379,13 @@ function configureExtension(context: vscode.ExtensionContext) { // Open TST script beside source file const scriptPath = testNode.enviroPath + ".tst"; await openTstScriptAtTest(testNode, scriptPath); + vscode.window.showWarningMessage( - `You are currently in REVIEW Mode for the Requirement Test ${testName}. Only the Coverage for this Test will be shown in the file. In Order to exit this mode, close the Box with the Requirement Description in the Source file.` + `You are currently in Review Mode for the requirement test ${testName}. Only coverage for this test is shown. To exit Review Mode, close the requirement description box in the source file.` ); } ); - /** - * Command: Closes requirement highlights and peek boxes - */ let closeRequirementBoxes = vscode.commands.registerCommand( "vectorcastTestExplorer.closeRequirementBoxes", () => { diff --git a/src/requirements/requirementsUtils.ts b/src/requirements/requirementsUtils.ts index 3ee32e98..c501f7fc 100644 --- a/src/requirements/requirementsUtils.ts +++ b/src/requirements/requirementsUtils.ts @@ -8,6 +8,7 @@ import { LLM2CHECK_EXECUTABLE_PATH, logCliError, logCliOperation, + TEST2CHECK_EXECUTABLE_PATH, } from "./requirementsOperations"; import { makeEnviroNodeID } from "../testPane"; import { dumpTestScriptFile } from "../vcastAdapter"; @@ -609,10 +610,9 @@ export interface RequirementData { lineNumber: number; importantLineStart: number; importantLineEnd: number; - coverageStatus: any; - expectedLines: any; - actualLines: any; - fullData: any; + coverageStatus: string; + expectedLines: number[]; + actualLines: number[]; } // State Management @@ -905,3 +905,126 @@ export function setActiveHighlightDecoration( ): void { activeHighlightDecoration = decoration; } + +/** + * Fetches the requirements Data for a specific Test + */ +export async function fetchRequirementCoverageData( + enviroPath: string, + envRGWPath: string, + testName: string, + unitName: string, + testNode: testNodeType +): Promise { + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Retrieving requirement coverage data", + cancellable: false, + }, + async (progress) => { + progress.report({ message: "Running test2check…" }); + + const commandArgs = [ + "-e", + enviroPath, + envRGWPath, + "-f", + testName, + "--json", + ]; + + try { + const process = await spawnWithVcastEnv( + TEST2CHECK_EXECUTABLE_PATH, + commandArgs + ); + + const stdoutData: string[] = []; + const stderrData: string[] = []; + + process.stdout.on("data", (data) => { + stdoutData.push(data.toString()); + }); + + process.stderr.on("data", (data) => { + stderrData.push(data.toString()); + logCliError(`test2check: ${data.toString()}`); + }); + + await new Promise((resolve, reject) => { + process.on("close", (code: number) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`test2check exited with code ${code}`)); + } + }); + }); + + progress.report({ message: "Processing coverage results…" }); + + const output = stdoutData.join(""); + if (!output.trim()) return null; + + const jsonData = JSON.parse(output); + if (!Array.isArray(jsonData) || jsonData.length === 0) return null; + + if (jsonData.length > 1) { + vscode.window.showInformationMessage( + "This test is associated with multiple requirements. Coverage Review currently supports only one requirement per test." + ); + return null; + } + + const testResult = jsonData[0]; + const expectedCoverage = testResult.expected_coverage || {}; + const actualCoverage = testResult.actual_coverage || []; + + const unitCoverage = actualCoverage.find((cov: any) => { + const covUnitBase = path.basename(cov.unit, path.extname(cov.unit)); + return covUnitBase === unitName || cov.unit === unitName; + }); + + let expectedLines: number[] = []; + for (const funcKey in expectedCoverage) { + const funcCoverage = expectedCoverage[funcKey]; + if (Array.isArray(funcCoverage)) { + const unitExpected = funcCoverage.find( + (cov: any) => cov.unit === unitName + ); + if (unitExpected?.lines) { + expectedLines = unitExpected.lines; + } + } + } + + const minLine = expectedLines.length ? Math.min(...expectedLines) : 0; + const maxLine = expectedLines.length ? Math.max(...expectedLines) : 0; + + let status = "covered"; + if (testResult.name?.includes("REVIEW-NEEDED")) { + status = "review-needed"; + } + + return { + title: testResult.name || testName, + description: `${testNode.notes}`, + lineNumber: minLine - 1, + importantLineStart: minLine, + importantLineEnd: maxLine, + coverageStatus: status, + expectedLines, + actualLines: unitCoverage?.lines || [], + }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + vscode.window.showWarningMessage( + `Failed to get requirement data: ${msg}` + ); + logCliError(msg); + return null; + } + } + ); +} diff --git a/src/testData.ts b/src/testData.ts index 3929c110..6863ae88 100644 --- a/src/testData.ts +++ b/src/testData.ts @@ -42,7 +42,7 @@ export interface testNodeType { // initially will be used for coded-tests testFile: string; testStartLine: number; - requirements: any; + requirements: string; notes: string; } // this is a lookup table for the nodes in the test tree @@ -61,7 +61,7 @@ export function createTestNodeInCache( testName: string = "", testFile: string = "", notes: string = "", - requirements: any = "", + requirements: string = "", testStartLine: number = 1 ) { let testNode: testNodeType = { diff --git a/src/testPane.ts b/src/testPane.ts index 69fcadf0..a0815ca4 100644 --- a/src/testPane.ts +++ b/src/testPane.ts @@ -193,7 +193,7 @@ function addTestNodes( globalTestStatusArray[testNodeID] = testData; - // TODO: Make usable also for multiple reqs --> also see data strucutre.. + // Empty list --> No requirements --> Button should not appear on that Test Node if (testNodeForCache.requirements !== "[]") { testNodesWithRequirements.push(testNodeID); } diff --git a/src/utilities.ts b/src/utilities.ts index 154150c1..7087a279 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -41,11 +41,6 @@ export interface jsonDataType { jsonDataAsString: string; } -export interface requirementsTestData { - title: string; - description: string; -} - /** * Retrieves the environment path associated with a given file path. * From 6079c9327d6d3c531e9ee591725674cec5ae7714 Mon Sep 17 00:00:00 2001 From: Den1552 Date: Mon, 9 Feb 2026 09:23:05 +0100 Subject: [PATCH 12/20] Refactor 2 --- src/extension.ts | 40 +++++++++++++++++++-------- src/requirements/requirementsUtils.ts | 19 ++++++------- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 722fdc3e..302fee33 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1327,9 +1327,10 @@ function configureExtension(context: vscode.ExtensionContext) { vscode.window.showWarningMessage( `Failed to retrieve node test data for ${args.id}. Aborting Test Review.` ); + return; } - const { enviroPath, unitName } = testNode; + const { enviroPath, unitName, functionName } = testNode; const envData = await getEnvironmentData(enviroPath); const envRGWPath = findRelevantRequirementGateway(enviroPath); const testName = testNode.testName; @@ -1341,12 +1342,38 @@ function configureExtension(context: vscode.ExtensionContext) { return; } + // Find the matching unit's source file + const matchingUnit = envData.unitData.find((unit: { path: string }) => { + if (!unit.path) return false; + const unitBaseName = path.basename(unit.path, path.extname(unit.path)); + return unitBaseName === unitName; + }); + + if (!matchingUnit?.path) { + vscode.window.showWarningMessage( + `Could not find source file for unit: ${unitName}` + ); + return; + } + + // Determine the line number to open at + let lineNumber = 0; + if (functionName && matchingUnit.functionList) { + for (const func of matchingUnit.functionList) { + if (func.name === functionName && func.startLine !== undefined) { + lineNumber = func.startLine; + break; + } + } + } + const reqData = await fetchRequirementCoverageData( enviroPath, envRGWPath, testName, unitName, - testNode + testNode, + lineNumber ); if (!reqData) { @@ -1356,15 +1383,6 @@ function configureExtension(context: vscode.ExtensionContext) { return; } - // Find the matching unit's source file - const matchingUnit = envData.unitData.find((unit: { path: string }) => { - if (!unit.path) return false; - const unitBaseName = path.basename(unit.path, path.extname(unit.path)); - return unitBaseName === unitName; - }); - - if (!matchingUnit?.path) return; - // Close sidebar for more screen space await vscode.commands.executeCommand("workbench.action.closeSidebar"); diff --git a/src/requirements/requirementsUtils.ts b/src/requirements/requirementsUtils.ts index c501f7fc..e6936a01 100644 --- a/src/requirements/requirementsUtils.ts +++ b/src/requirements/requirementsUtils.ts @@ -834,7 +834,7 @@ export async function openSourceFileWithHighlight( // Position cursor near the requirement line const peekPosition = new vscode.Position( - Math.max(0, reqData.lineNumber - 2), + Math.max(0, reqData.lineNumber - 1), 0 ); @@ -914,7 +914,8 @@ export async function fetchRequirementCoverageData( envRGWPath: string, testName: string, unitName: string, - testNode: testNodeType + testNode: testNodeType, + functionStartLine: number = 0 ): Promise { return vscode.window.withProgress( { @@ -1002,18 +1003,14 @@ export async function fetchRequirementCoverageData( const minLine = expectedLines.length ? Math.min(...expectedLines) : 0; const maxLine = expectedLines.length ? Math.max(...expectedLines) : 0; - let status = "covered"; - if (testResult.name?.includes("REVIEW-NEEDED")) { - status = "review-needed"; - } - return { title: testResult.name || testName, description: `${testNode.notes}`, - lineNumber: minLine - 1, - importantLineStart: minLine, - importantLineEnd: maxLine, - coverageStatus: status, + lineNumber: functionStartLine, + // + 1 Because Critical LInes are 0 Indexed + importantLineStart: minLine + 1, + importantLineEnd: maxLine + 1, + coverageStatus: "covered", expectedLines, actualLines: unitCoverage?.lines || [], }; From b54a8540b32d61ee24a1e8d60803b86410bdcfe4 Mon Sep 17 00:00:00 2001 From: Den1552 Date: Mon, 9 Feb 2026 10:59:27 +0100 Subject: [PATCH 13/20] Removed debug log --- src/coverage.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/coverage.ts b/src/coverage.ts index 51dbe16b..38f6d9ba 100644 --- a/src/coverage.ts +++ b/src/coverage.ts @@ -445,7 +445,6 @@ export function updateReviewModeDecorations(): void { return; } - // Base coverage data (ONLY coverable lines) const covered = new Set(coverageData.covered); const uncovered = new Set(coverageData.uncovered); const partiallyCovered = new Set(coverageData.partiallyCovered); @@ -455,7 +454,7 @@ export function updateReviewModeDecorations(): void { ...reviewModeExpectedLines, ...reviewModeActualLines, ]); - vectorMessage(`${JSON.stringify(expectedOrActual)}`); + // Any covered line NOT in expected/actual becomes uncovered for (const line of covered) { if (!expectedOrActual.has(line - 1)) { From 89268dd1a0b116790933e6772396d878c856c15a Mon Sep 17 00:00:00 2001 From: Den1552 Date: Mon, 9 Feb 2026 11:10:56 +0100 Subject: [PATCH 14/20] Removed unused imports --- src/coverage.ts | 1 - src/extension.ts | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/coverage.ts b/src/coverage.ts index 38f6d9ba..2dde8cbf 100644 --- a/src/coverage.ts +++ b/src/coverage.ts @@ -16,7 +16,6 @@ import { currentActiveUnitMCDCLines, updateCurrentActiveUnitMCDCLines, } from "./editorDecorator"; -import { vectorMessage } from "./messagePane"; // these are defined as globals so that the deactivate function has access // to dispose of them when the coverage id turned off diff --git a/src/extension.ts b/src/extension.ts index 302fee33..1fea5e9c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -133,10 +133,8 @@ import { openTstScriptAtTest, parseRequirementsFromFile, performLLMProviderUsableCheck, - RequirementData, requirementsFileWatcher, updateRequirementsAvailability, - spawnWithVcastEnv, fetchRequirementCoverageData, } from "./requirements/requirementsUtils"; @@ -146,9 +144,7 @@ import { generateTestsFromRequirements, importRequirementsFromGateway, initializeReqs2X, - logCliError, populateRequirementsGateway, - TEST2CHECK_EXECUTABLE_PATH, } from "./requirements/requirementsOperations"; import { From 6a0e63535572396bd29c123b17d1aea64c1d8113 Mon Sep 17 00:00:00 2001 From: Den1552 Date: Thu, 12 Feb 2026 08:42:01 +0100 Subject: [PATCH 15/20] Adapted highlighting and text box wrap up --- src/requirements/requirementsUtils.ts | 208 ++++++++++++++++++++------ 1 file changed, 159 insertions(+), 49 deletions(-) diff --git a/src/requirements/requirementsUtils.ts b/src/requirements/requirementsUtils.ts index e6936a01..a19a3ed2 100644 --- a/src/requirements/requirementsUtils.ts +++ b/src/requirements/requirementsUtils.ts @@ -19,6 +19,7 @@ import { exitReviewMode, updateDisplayedCoverage, } from "../coverage"; +import { getCoverageDataForFile } from "../vcastTestInterface"; const path = require("path"); const fs = require("fs"); @@ -620,59 +621,81 @@ export let activeHighlightDecoration: vscode.TextEditorDecorationType | null = null; /** - * Wraps text to a specified width, breaking at word boundaries + * Wraps a single line of text into an array of lines, each <= maxWidth chars */ -export function wrapText(text: string, width: number): string[] { - const words = text.split(" "); +function wrapText(text: string, maxWidth: number): string[] { + if (text.length <= maxWidth) { + return [text]; + } + const lines: string[] = []; + const words = text.split(" "); let current = ""; for (const word of words) { - if ((current + word).length > width) { - lines.push(current.trimEnd()); - current = word + " "; + // Word itself is longer than maxWidth — hard break it + if (word.length > maxWidth) { + if (current) { + lines.push(current); + current = ""; + } + let remaining = word; + while (remaining.length > maxWidth) { + lines.push(remaining.slice(0, maxWidth)); + remaining = remaining.slice(maxWidth); + } + current = remaining; + continue; + } + + const candidate = current ? `${current} ${word}` : word; + if (candidate.length <= maxWidth) { + current = candidate; } else { - current += word + " "; + lines.push(current); + current = word; } } - if (current.trim()) { - lines.push(current.trimEnd()); + if (current) { + lines.push(current); } return lines; } /** - * Creates a formatted text box containing requirement information + * Creates a formatted text box containing requirement information. + * Guarantees nothing escapes the box — title and description are both wrapped. */ export function createRequirementInfoBox(reqData: RequirementData): string { - const BOX_WIDTH = 66; // inner width (between borders) - const TEXT_WIDTH = BOX_WIDTH - 4; // "║ " + text + " ║" + const BOX_WIDTH = 66; // total inner width (between the ║ borders) + const PADDING = 2; // spaces on each side inside the border + const TEXT_WIDTH = BOX_WIDTH - PADDING * 2; // usable text width = 62 const topBorder = "╔" + "═".repeat(BOX_WIDTH) + "╗"; const midBorder = "╠" + "═".repeat(BOX_WIDTH) + "╣"; const bottomBorder = "╚" + "═".repeat(BOX_WIDTH) + "╝"; + const divider = "║ " + "─".repeat(TEXT_WIDTH) + " ║"; - const formatLine = (text = "") => "║ " + text.padEnd(TEXT_WIDTH) + " ║"; - - /** - * Wrap text that may contain newlines into boxed lines - */ - const formatMultilineText = (text: string): string[] => { - const paragraphs = text.split(/\r?\n/); + const formatLine = (text = ""): string => { + // Should never exceed TEXT_WIDTH after wrapping + const safe = text.length > TEXT_WIDTH ? text.slice(0, TEXT_WIDTH) : text; + return "║ " + safe.padEnd(TEXT_WIDTH) + " ║"; + }; + // Wraps a block of text (may contain \n) and returns boxed lines + const formatBlock = (text: string): string[] => { const lines: string[] = []; - for (const para of paragraphs) { - if (!para.trim()) { - // preserve blank lines as empty boxed lines + for (const paragraph of text.split(/\r?\n/)) { + if (!paragraph.trim()) { lines.push(formatLine()); continue; } - - const wrapped = wrapText(para, TEXT_WIDTH); - wrapped.forEach((w) => lines.push(formatLine(w))); + for (const wrapped of wrapText(paragraph, TEXT_WIDTH)) { + lines.push(formatLine(wrapped)); + } } return lines; @@ -681,13 +704,11 @@ export function createRequirementInfoBox(reqData: RequirementData): string { return [ "", topBorder, - formatLine(reqData.title), + ...formatBlock(reqData.title), midBorder, formatLine("DESCRIPTION"), - formatLine("─".repeat(TEXT_WIDTH)), - - // Description body (fully boxed, paragraph-safe) - ...formatMultilineText(reqData.description), + divider, + ...formatBlock(reqData.description), bottomBorder, "", ].join("\n"); @@ -709,34 +730,123 @@ export function findTestNameLine(tstContent: string, testName: string): number { return 0; // Default to top of file if not found } +// Decoration Types for highlighted critical lines +let activeUncoveredDecoration: vscode.TextEditorDecorationType | undefined; +let activePartiallyCoveredDecoration: + | vscode.TextEditorDecorationType + | undefined; +let activeCoveredDecoration: vscode.TextEditorDecorationType | undefined; + +/** + * Creates a decoration type for a given coverage state + */ +function createCoverageDecoration( + bgColor: string, + gutterColor: string +): vscode.TextEditorDecorationType { + return vscode.window.createTextEditorDecorationType({ + backgroundColor: bgColor, + isWholeLine: true, + before: { + contentText: "", + border: `4px solid ${gutterColor}`, + margin: "0 6px 0 0", + }, + }); +} + +/** + * Converts a 1-based line number to a single-line vscode.Range + */ +function lineToRange( + document: vscode.TextDocument, + lineNumber: number +): vscode.Range { + const line = lineNumber - 1; // Convert to 0-based + return new vscode.Range( + new vscode.Position(line, 0), + new vscode.Position(line, document.lineAt(line).text.length) + ); +} + /** - * Highlights the critical lines in the source file + * Highlights critical lines individually based on coverage status */ export function highlightCriticalLines( editor: vscode.TextEditor, document: vscode.TextDocument, - reqData: RequirementData + reqData: RequirementData, + sourceFilePath: string ): void { - // Dispose previous highlight if exists - if (activeHighlightDecoration) { - activeHighlightDecoration.dispose(); - } + // Dispose all previous decorations + activeUncoveredDecoration?.dispose(); + activePartiallyCoveredDecoration?.dispose(); + activeCoveredDecoration?.dispose(); - // Highlight decoration - activeHighlightDecoration = vscode.window.createTextEditorDecorationType({ - backgroundColor: "rgba(0,255,0,0.15)", - }); + const coverageData = getCoverageDataForFile(sourceFilePath); - // Apply highlight to critical lines - const blockRange = new vscode.Range( - new vscode.Position(reqData.importantLineStart - 1, 0), - new vscode.Position( - reqData.importantLineEnd - 1, - document.lineAt(reqData.importantLineEnd - 1).text.length - ) + if (!coverageData?.hasCoverageData) { + return; + } + + // --- Decoration types --- + activeUncoveredDecoration = createCoverageDecoration( + "rgba(243, 74, 51, 0.15)", // red bg + "#f34a33" // red gutter + ); + activePartiallyCoveredDecoration = createCoverageDecoration( + "rgba(245, 166, 35, 0.15)", // orange/yellow bg + "#f5a623" // orange/yellow gutter + ); + activeCoveredDecoration = createCoverageDecoration( + "rgba(87, 184, 89, 0.15)", // green bg + "#57b859" // green gutter ); - editor.setDecorations(activeHighlightDecoration, [blockRange]); + // Build a Set of critical line numbers for fast lookup + const criticalLines = new Set(); + for ( + let line = reqData.importantLineStart; + line <= reqData.importantLineEnd; + line++ + ) { + criticalLines.add(line); + } + + // Bucket each critical line into its coverage category + const uncoveredSet = new Set(coverageData.uncovered); + const partiallyCoveredSet = new Set(coverageData.partiallyCovered); + const coveredSet = new Set(coverageData.covered); + + const uncoveredRanges: vscode.Range[] = []; + const partiallyCoveredRanges: vscode.Range[] = []; + const coveredRanges: vscode.Range[] = []; + + for (const line of criticalLines) { + // Guard: skip lines beyond the document + if (line > document.lineCount) { + continue; + } + + const range = lineToRange(document, line); + + if (uncoveredSet.has(line)) { + uncoveredRanges.push(range); + } else if (partiallyCoveredSet.has(line)) { + partiallyCoveredRanges.push(range); + } else if (coveredSet.has(line)) { + coveredRanges.push(range); + } + // Lines not present in any coverage array are left un-decorated + } + + // Apply all three decoration sets in one pass + editor.setDecorations(activeUncoveredDecoration, uncoveredRanges); + editor.setDecorations( + activePartiallyCoveredDecoration, + partiallyCoveredRanges + ); + editor.setDecorations(activeCoveredDecoration, coveredRanges); } /** @@ -854,7 +964,7 @@ export async function openSourceFileWithHighlight( ); // Apply the green highlight to critical lines - highlightCriticalLines(editor, document, reqData); + highlightCriticalLines(editor, document, reqData, sourceFilePath); // Show the peek box await showRequirementPeekBox(sourceFileUri, peekPosition, reqData, context); From cd2337b76ed5b722b0007f01b6cc8c4337097aee Mon Sep 17 00:00:00 2001 From: Den1552 Date: Thu, 12 Feb 2026 10:55:00 +0100 Subject: [PATCH 16/20] First Fix for button not appearing + Fix for Highlighting not disappearing --- src/extension.ts | 15 ++++++++++++++ src/requirements/requirementsOperations.ts | 4 ++-- src/requirements/requirementsUtils.ts | 24 ++++++++++++++++------ 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 1fea5e9c..44b37a27 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -68,6 +68,7 @@ import { setGlobalProjectIsOpenedChecker, setGlobalCompilerAndTestsuites, loadTestScriptButton, + runTests, } from "./testPane"; import { @@ -170,6 +171,7 @@ import { import fs = require("fs"); import { compilerTagList, + findTestItemInController, getNonce, resolveWebviewBase, setCompilerList, @@ -512,6 +514,19 @@ function configureExtension(context: vscode.ExtensionContext) { enviroPath, testNode.functionName || testNode.unitName || null ); + + // We need to execute the test and refresh the extension data so that the tests get the + // Reqs review button (see openReqsCoverageReview). On normal envs, after generating the tests, + // we fetch the data from the env but the tests still not seem to have the data. + // Executing them and refreshing the extension solves this. + const testItem = findTestItemInController(testNode.enviroNodeID); + if (testItem) { + const request = new vscode.TestRunRequest([testItem]); + await runTests(request, new vscode.CancellationTokenSource().token); + } else { + vectorMessage(`TestItem for ${testNode.enviroNodeID} not found`); + } + await refreshAllExtensionData(); } } ); diff --git a/src/requirements/requirementsOperations.ts b/src/requirements/requirementsOperations.ts index 11da0a97..dad1dcb3 100644 --- a/src/requirements/requirementsOperations.ts +++ b/src/requirements/requirementsOperations.ts @@ -7,7 +7,7 @@ import { spawnWithVcastEnv, updateRequirementsAvailability, } from "./requirementsUtils"; -import { refreshAllExtensionData } from "../testPane"; +import { refreshAllExtensionData, updateTestPane } from "../testPane"; import { loadTestScriptIntoEnvironment } from "../vcastAdapter"; const path = require("path"); @@ -460,7 +460,7 @@ export async function generateTestsFromRequirements( `reqs2tests completed successfully with code ${code}` ); await loadTestScriptIntoEnvironment(envName.split(".")[0], tstPath); - await refreshAllExtensionData(); + await updateTestPane(enviroPath); vscode.window.showInformationMessage( "Successfully generated tests for the requirements!" diff --git a/src/requirements/requirementsUtils.ts b/src/requirements/requirementsUtils.ts index a19a3ed2..e2008673 100644 --- a/src/requirements/requirementsUtils.ts +++ b/src/requirements/requirementsUtils.ts @@ -778,10 +778,8 @@ export function highlightCriticalLines( reqData: RequirementData, sourceFilePath: string ): void { - // Dispose all previous decorations - activeUncoveredDecoration?.dispose(); - activePartiallyCoveredDecoration?.dispose(); - activeCoveredDecoration?.dispose(); + // Dispose all previous decorations via shared helper + disposeCriticalLineDecorations(); const coverageData = getCoverageDataForFile(sourceFilePath); @@ -906,7 +904,10 @@ export async function showRequirementPeekBox( ); if (!peekWindowOpen) { - // Clear the highlight decoration + // Clear ALL coverage highlight decorations + disposeCriticalLineDecorations(); + + // Also clear the legacy single decoration if somehow still set if (activeHighlightDecoration) { activeHighlightDecoration.dispose(); setActiveHighlightDecoration(null); @@ -921,7 +922,6 @@ export async function showRequirementPeekBox( } } ); - context.subscriptions.push(disposable); // Clean up provider after peek window is shown @@ -930,6 +930,18 @@ export async function showRequirementPeekBox( }, 1000); } +/** + * Disposes all active critical line decorations (all three coverage states) + */ +export function disposeCriticalLineDecorations(): void { + activeUncoveredDecoration?.dispose(); + activePartiallyCoveredDecoration?.dispose(); + activeCoveredDecoration?.dispose(); + activeUncoveredDecoration = undefined; + activePartiallyCoveredDecoration = undefined; + activeCoveredDecoration = undefined; +} + /** * Opens the source file with requirement highlighting */ From 0bce330c0d684f70b32536307649bcbd5fa4d9b8 Mon Sep 17 00:00:00 2001 From: Patrick Bareiss Date: Mon, 16 Mar 2026 19:23:35 +0100 Subject: [PATCH 17/20] Render correct requirements coverage information --- src/requirements/requirementsUtils.ts | 62 +++++++++------------------ 1 file changed, 21 insertions(+), 41 deletions(-) diff --git a/src/requirements/requirementsUtils.ts b/src/requirements/requirementsUtils.ts index e2008673..260fb291 100644 --- a/src/requirements/requirementsUtils.ts +++ b/src/requirements/requirementsUtils.ts @@ -19,7 +19,6 @@ import { exitReviewMode, updateDisplayedCoverage, } from "../coverage"; -import { getCoverageDataForFile } from "../vcastTestInterface"; const path = require("path"); const fs = require("fs"); @@ -732,26 +731,17 @@ export function findTestNameLine(tstContent: string, testName: string): number { // Decoration Types for highlighted critical lines let activeUncoveredDecoration: vscode.TextEditorDecorationType | undefined; -let activePartiallyCoveredDecoration: - | vscode.TextEditorDecorationType - | undefined; let activeCoveredDecoration: vscode.TextEditorDecorationType | undefined; /** * Creates a decoration type for a given coverage state */ function createCoverageDecoration( - bgColor: string, - gutterColor: string + bgColor: string ): vscode.TextEditorDecorationType { return vscode.window.createTextEditorDecorationType({ backgroundColor: bgColor, isWholeLine: true, - before: { - contentText: "", - border: `4px solid ${gutterColor}`, - margin: "0 6px 0 0", - }, }); } @@ -771,19 +761,17 @@ function lineToRange( /** * Highlights critical lines individually based on coverage status + * Uses test-specific actualLines from tests2check rather than global coverage */ export function highlightCriticalLines( editor: vscode.TextEditor, document: vscode.TextDocument, - reqData: RequirementData, - sourceFilePath: string + reqData: RequirementData ): void { // Dispose all previous decorations via shared helper disposeCriticalLineDecorations(); - const coverageData = getCoverageDataForFile(sourceFilePath); - - if (!coverageData?.hasCoverageData) { + if (!reqData.actualLines || reqData.actualLines.length === 0) { return; } @@ -792,16 +780,12 @@ export function highlightCriticalLines( "rgba(243, 74, 51, 0.15)", // red bg "#f34a33" // red gutter ); - activePartiallyCoveredDecoration = createCoverageDecoration( - "rgba(245, 166, 35, 0.15)", // orange/yellow bg - "#f5a623" // orange/yellow gutter - ); activeCoveredDecoration = createCoverageDecoration( "rgba(87, 184, 89, 0.15)", // green bg "#57b859" // green gutter ); - // Build a Set of critical line numbers for fast lookup + // Build a Set of critical line numbers for fast lookup (1-based) const criticalLines = new Set(); for ( let line = reqData.importantLineStart; @@ -811,13 +795,17 @@ export function highlightCriticalLines( criticalLines.add(line); } - // Bucket each critical line into its coverage category - const uncoveredSet = new Set(coverageData.uncovered); - const partiallyCoveredSet = new Set(coverageData.partiallyCovered); - const coveredSet = new Set(coverageData.covered); + // actualLines from tests2check are 0-based, convert to 1-based + const actualLinesSet = new Set( + reqData.actualLines.map((line) => line + 1) + ); + + // expectedLines from tests2check are 0-based, convert to 1-based + const expectedLinesSet = new Set( + (reqData.expectedLines || []).map((line) => line + 1) + ); const uncoveredRanges: vscode.Range[] = []; - const partiallyCoveredRanges: vscode.Range[] = []; const coveredRanges: vscode.Range[] = []; for (const line of criticalLines) { @@ -828,22 +816,16 @@ export function highlightCriticalLines( const range = lineToRange(document, line); - if (uncoveredSet.has(line)) { - uncoveredRanges.push(range); - } else if (partiallyCoveredSet.has(line)) { - partiallyCoveredRanges.push(range); - } else if (coveredSet.has(line)) { + if (actualLinesSet.has(line)) { coveredRanges.push(range); + } else if (expectedLinesSet.has(line)) { + uncoveredRanges.push(range); } - // Lines not present in any coverage array are left un-decorated + // Lines not in expectedLines or actualLines are non-executable — skip them } - // Apply all three decoration sets in one pass + // Apply both decoration sets in one pass editor.setDecorations(activeUncoveredDecoration, uncoveredRanges); - editor.setDecorations( - activePartiallyCoveredDecoration, - partiallyCoveredRanges - ); editor.setDecorations(activeCoveredDecoration, coveredRanges); } @@ -931,14 +913,12 @@ export async function showRequirementPeekBox( } /** - * Disposes all active critical line decorations (all three coverage states) + * Disposes all active critical line decorations */ export function disposeCriticalLineDecorations(): void { activeUncoveredDecoration?.dispose(); - activePartiallyCoveredDecoration?.dispose(); activeCoveredDecoration?.dispose(); activeUncoveredDecoration = undefined; - activePartiallyCoveredDecoration = undefined; activeCoveredDecoration = undefined; } @@ -976,7 +956,7 @@ export async function openSourceFileWithHighlight( ); // Apply the green highlight to critical lines - highlightCriticalLines(editor, document, reqData, sourceFilePath); + highlightCriticalLines(editor, document, reqData); // Show the peek box await showRequirementPeekBox(sourceFileUri, peekPosition, reqData, context); From 8e498a7810c7a8f1261623b07a56e1b89441ff40 Mon Sep 17 00:00:00 2001 From: Patrick Bareiss Date: Tue, 17 Mar 2026 13:49:20 +0100 Subject: [PATCH 18/20] Add mostly working proper implementation of test-specific coverage --- python/vTestInterface.py | 71 +++++++++++ src/coverage.ts | 164 +++----------------------- src/extension.ts | 5 +- src/requirements/requirementsUtils.ts | 13 +- src/vcastTestInterface.ts | 75 ++++++++++++ 5 files changed, 171 insertions(+), 157 deletions(-) diff --git a/python/vTestInterface.py b/python/vTestInterface.py index 6f289cc4..1552c925 100644 --- a/python/vTestInterface.py +++ b/python/vTestInterface.py @@ -303,6 +303,7 @@ def getUnitData(api): unitInfo["covered"] = covered unitInfo["uncovered"] = uncovered unitInfo["partiallyCovered"] = partiallyCovered + unitInfo["perTestCoverage"] = getPerTestCoverageData(sourceObject) unitList.append(unitInfo) elif len(sourcePath) > 0: @@ -316,6 +317,7 @@ def getUnitData(api): unitInfo["covered"] = "" unitInfo["uncovered"] = "" unitInfo["partiallyCovered"] = "" + unitInfo["perTestCoverage"] = {} unitList.append(unitInfo) return unitList @@ -391,6 +393,75 @@ def getCoverageKind(sourceObject): return CoverageKind.ignore +def _buildResultNameCache(sourceObject): + """ + Pre-builds a mapping from Result.id to test case identifier string. + This avoids repeated result.unit_test lookups when the same Result + appears on many lines during per-test coverage collection. + + Returns: dict {result_id: "unit.function.testname"} + """ + cache = {} + try: + for result in sourceObject.cover_data.results: + test_case = result.unit_test + if test_case: + name = ( + f"{test_case.unit_display_name}" + f".{test_case.function_display_name}" + f".{test_case.name}" + ) + cache[result.id] = name + else: + # Fallback for non-unit-test results (e.g. cover-only or imported) + cache[result.id] = result.name + except Exception: + # If the API doesn't support this (older DataAPI versions), return empty + pass + return cache + + +def getPerTestCoverageData(sourceObject): + """ + Returns a dict mapping line numbers to lists of test case names + that cover each line. Uses SourceLine.results from the DataAPI + to determine which Results (test cases) hit each line. + + Format: {"lineNum": ["unit.function.testname", ...], ...} + Only lines with at least one covering test are included. + + Returns an empty dict if the source is not instrumented, the file + doesn't exist on disk, or the API doesn't support per-result queries. + """ + perTestCoverage = {} + if not (sourceObject and sourceObject.is_instrumented): + return perTestCoverage + + if not os.path.exists(sourceObject.path): + return perTestCoverage + + resultNameCache = _buildResultNameCache(sourceObject) + if not resultNameCache: + return perTestCoverage + + try: + for line in sourceObject.iterate_coverage(): + lineResults = line.results + if lineResults: + testNames = [] + for result in lineResults: + name = resultNameCache.get(result.id) + if name: + testNames.append(name) + if testNames: + perTestCoverage[str(line.line_number)] = testNames + except Exception: + # Gracefully degrade if line.results is not available + pass + + return perTestCoverage + + def getCoverageData(sourceObject): """ This function will use the data interface to diff --git a/src/coverage.ts b/src/coverage.ts index 2dde8cbf..3d19b861 100644 --- a/src/coverage.ts +++ b/src/coverage.ts @@ -6,10 +6,11 @@ import { } from "vscode"; import { getCoverageDataForFile, + getCoverageDataForFileAndTest, getListOfFilesWithCoverage, } from "./vcastTestInterface"; -import { getRangeOption, normalizePath } from "./utilities"; +import { getRangeOption } from "./utilities"; import { fileDecorator } from "./fileDecorator"; import { @@ -83,8 +84,6 @@ export function initializeCodeCoverageFeatures( //fontWeight: "bold", gutterIconPath: context.asAbsolutePath("./images/light/cover-icon.svg"), }; - - initializeReviewModeDecorations(context); } // global decoration arrays @@ -185,15 +184,15 @@ export async function updateCOVdecorations() { ) { const filePath = url.fileURLToPath(activeEditor.document.uri.toString()); - // Check if we're in review mode for this file - if (isReviewModeActive() && filePath === getReviewModeFilePath()) { - // In review mode, use review mode decorations instead - updateReviewModeDecorations(); - return; - } + // In review mode, show only the selected test's coverage for the target file + const coverageData = + reviewModeActive && reviewModeTestId && reviewModeFilePath === filePath + ? getCoverageDataForFileAndTest(filePath, reviewModeTestId) + : getCoverageDataForFile(filePath); - // this returns the cached coverage data for this file - const coverageData = getCoverageDataForFile(filePath); + vscode.window.showInformationMessage( + `coverage data for ${filePath}: ${JSON.stringify(coverageData)}` + ); if (coverageData.hasCoverageData) { // there is coverage data and it matches the file checksum @@ -341,158 +340,29 @@ export async function updateDisplayedCoverage() { if (coverageOn) await updateCOVdecorations(); } -// Review mode state +// Review mode state //////////////////////////////////////////////// let reviewModeActive: boolean = false; -let reviewModeExpectedLines: number[] = []; -let reviewModeActualLines: number[] = []; +let reviewModeTestId: string | null = null; let reviewModeFilePath: string | null = null; - -// Review mode decoration types -let reviewCoveredDecorationType: TextEditorDecorationType; -let reviewUncoveredDecorationType: TextEditorDecorationType; - -let reviewCoveredRenderOptions: DecorationRenderOptions; -let reviewUncoveredRenderOptions: DecorationRenderOptions; - -export function initializeReviewModeDecorations( - context: vscode.ExtensionContext -) { - reviewUncoveredRenderOptions = { - gutterIconPath: context.asAbsolutePath("./images/light/no-cover-icon.svg"), - }; - - reviewCoveredRenderOptions = { - gutterIconPath: context.asAbsolutePath("./images/light/cover-icon.svg"), - }; -} +///////////////////////////////////////////////////////////////////// export function isReviewModeActive(): boolean { return reviewModeActive; } -export function getReviewModeFilePath(): string | null { - return reviewModeFilePath; -} - -export function enterReviewMode( - testName: string, - expectedLines: number[], - actualLines: number[], - filePath: string -): void { +export function enterReviewMode(testId: string, filePath: string): void { reviewModeActive = true; - reviewModeExpectedLines = expectedLines; - reviewModeActualLines = actualLines; + reviewModeTestId = testId; reviewModeFilePath = filePath; - - resetGlobalDecorations(); - - // Immediately update decorations for the active editor - updateReviewModeDecorations(); } export async function exitReviewMode(): Promise { reviewModeActive = false; - reviewModeExpectedLines = []; - reviewModeActualLines = []; + reviewModeTestId = null; reviewModeFilePath = null; - // Clear review mode decorations - clearReviewModeDecorations(); - - // restore normal coverage + // Refresh normal coverage display if (coverageOn) { await updateCOVdecorations(); } } - -function clearReviewModeDecorations(): void { - if (reviewCoveredDecorationType) { - reviewCoveredDecorationType.dispose(); - } - if (reviewUncoveredDecorationType) { - reviewUncoveredDecorationType.dispose(); - } -} - -export function updateReviewModeDecorations(): void { - if (!reviewModeActive) { - return; - } - - const activeEditor = vscode.window.activeTextEditor; - if (!activeEditor) { - return; - } - - // Only apply review decorations to the specific file - if ( - !reviewModeFilePath || - normalizePath(activeEditor.document.uri.fsPath) !== - normalizePath(reviewModeFilePath) - ) { - return; - } - - // Clear previous decorations - clearReviewModeDecorations(); - - const filePath = activeEditor.document.uri.fsPath; - const coverageData = getCoverageDataForFile(filePath); - - if (!coverageData.hasCoverageData) { - return; - } - - const covered = new Set(coverageData.covered); - const uncovered = new Set(coverageData.uncovered); - const partiallyCovered = new Set(coverageData.partiallyCovered); - - // Lines that should count as "covered" in review mode - const expectedOrActual = new Set([ - ...reviewModeExpectedLines, - ...reviewModeActualLines, - ]); - - // Any covered line NOT in expected/actual becomes uncovered - for (const line of covered) { - if (!expectedOrActual.has(line - 1)) { - covered.delete(line); - uncovered.add(line); - } - } - - const coveredDecorations: vscode.DecorationOptions[] = []; - const uncoveredDecorations: vscode.DecorationOptions[] = []; - - // Only iterate over lines that actually have coverage data - const allCoverableLines = new Set([ - ...covered, - ...uncovered, - ...partiallyCovered, - ]); - - for (const lineNumber of allCoverableLines) { - const lineIndex = lineNumber - 1; - if (covered.has(lineNumber)) { - coveredDecorations.push(getRangeOption(lineIndex)); - } else { - // uncovered + partial both render as uncovered in review mode - uncoveredDecorations.push(getRangeOption(lineIndex)); - } - } - - // Apply decorations - reviewCoveredDecorationType = vscode.window.createTextEditorDecorationType( - reviewCoveredRenderOptions - ); - reviewUncoveredDecorationType = vscode.window.createTextEditorDecorationType( - reviewUncoveredRenderOptions - ); - - activeEditor.setDecorations(reviewCoveredDecorationType, coveredDecorations); - activeEditor.setDecorations( - reviewUncoveredDecorationType, - uncoveredDecorations - ); -} diff --git a/src/extension.ts b/src/extension.ts index 44b37a27..22df7ab8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1397,12 +1397,15 @@ function configureExtension(context: vscode.ExtensionContext) { // Close sidebar for more screen space await vscode.commands.executeCommand("workbench.action.closeSidebar"); + // Use the test node ID directly from the test explorer + const testId = args.id; + // Open source file with requirement highlighting await openSourceFileWithHighlight( matchingUnit.path, reqData, context, - testName + testId ); // Open TST script beside source file diff --git a/src/requirements/requirementsUtils.ts b/src/requirements/requirementsUtils.ts index 260fb291..0f2245c0 100644 --- a/src/requirements/requirementsUtils.ts +++ b/src/requirements/requirementsUtils.ts @@ -371,8 +371,8 @@ export function isLLMProviderEnvironmentUsable(): Promise<{ const result = JSON.parse(output); resolve({ usable: result.usable, problem: result.problem || null }); } catch (e) { - console.error(`Failed to parse llm2check output: ${e}`); - resolve({ usable: false, problem: "Failed to parse llm2check output" }); + console.error(`Failed to parse llm2check output: ${e} ${output}`); + resolve({ usable: false, problem: `Failed to parse llm2check output: ${output}` }); } }); }); @@ -929,7 +929,7 @@ export async function openSourceFileWithHighlight( sourceFilePath: string, reqData: RequirementData, context: vscode.ExtensionContext, - testName: string + testId: string ): Promise { const sourceFileUri = vscode.Uri.file(sourceFilePath); const document = await vscode.workspace.openTextDocument(sourceFileUri); @@ -948,12 +948,7 @@ export async function openSourceFileWithHighlight( }); // Enter review mode BEFORE applying decorations - enterReviewMode( - testName, - reqData.expectedLines || [], - reqData.actualLines || [], - sourceFilePath - ); + enterReviewMode(testId, sourceFilePath); // Apply the green highlight to critical lines highlightCriticalLines(editor, document, reqData); diff --git a/src/vcastTestInterface.ts b/src/vcastTestInterface.ts index bfefb616..e3864616 100644 --- a/src/vcastTestInterface.ts +++ b/src/vcastTestInterface.ts @@ -193,6 +193,7 @@ interface coverageDataType { covered: number[]; uncovered: number[]; partiallyCovered: number[]; + perTestCoverage: Record; // "lineNum" -> ["unit.func.test", ...] } interface fileCoverageType { @@ -279,6 +280,77 @@ export function getCoverageDataForFile(filePath: string): coverageSummaryType { return returnData; } +////////////////////////////////////////////////////////////////////// +export function getCoverageDataForFileAndTest( + filePath: string, + testId: string +): coverageSummaryType { + // Returns coverage data for a specific test case on a specific file. + // Uses the perTestCoverage data to determine which lines the test covers. + // Lines in perTestCoverage that include testId are "covered". + // All other coverable lines (from aggregate data) are "uncovered". + + let returnData: coverageSummaryType = { + hasCoverageData: false, + statusString: "", + covered: [], + uncovered: [], + partiallyCovered: [], + }; + + const dataForThisFile = globalCoverageData.get(filePath); + if (!dataForThisFile || !dataForThisFile.hasCoverage) { + return returnData; + } + + const checksum: number = getChecksum(filePath); + + // Collect per-test coverage and aggregate coverable lines across all enviros + const coveredByTest = new Set(); + const allCoverableLines = new Set(); + + for (const [enviroPath, enviroData] of dataForThisFile.enviroList.entries()) { + if (enviroData.crc32Checksum != checksum) { + continue; + } + + // All coverable lines from aggregate data + for (const line of enviroData.covered) allCoverableLines.add(line); + for (const line of enviroData.uncovered) allCoverableLines.add(line); + for (const line of enviroData.partiallyCovered) allCoverableLines.add(line); + + // Lines covered by this specific test + // Python produces short names like "unit.function.testname"; + // compose the full test node ID to match the VS Code test explorer format + for (const [lineStr, testNames] of Object.entries( + enviroData.perTestCoverage + )) { + for (const testName of testNames) { + const fullTestNodeId = `vcast:${enviroPath}|${testName}`; + if (fullTestNodeId === testId) { + coveredByTest.add(Number(lineStr)); + } + } + } + } + + if (allCoverableLines.size === 0) { + returnData.statusString = "Coverage Out of Date"; + return returnData; + } + + returnData.hasCoverageData = true; + returnData.covered = [...coveredByTest]; + // Uncovered = all coverable lines minus those covered by this test + returnData.uncovered = [...allCoverableLines].filter( + (line) => !coveredByTest.has(line) + ); + // partiallyCovered is empty — per-test coverage is binary + returnData.partiallyCovered = []; + + return returnData; +} + export function checksumMatchesEnvironment( filePath: string, enviroPath: string @@ -339,11 +411,14 @@ export function updateGlobalDataForFile(enviroPath: string, fileList: any[]) { .map(Number); const checksum = fileList[fileIndex].cmcChecksum; + const perTestCoverage: Record = + fileList[fileIndex].perTestCoverage || {}; let coverageData: coverageDataType = { crc32Checksum: checksum, covered: coveredList, uncovered: uncoveredList, partiallyCovered: partiallyCoveredList, + perTestCoverage: perTestCoverage, }; let fileData: fileCoverageType | undefined = From c41e7567d7f950114b54076aaeb941f82b39331a Mon Sep 17 00:00:00 2001 From: Patrick Bareiss Date: Tue, 17 Mar 2026 14:05:28 +0100 Subject: [PATCH 19/20] Fix partial coverage, mcdc and so on for individual test rendering --- python/vTestInterface.py | 283 ++++++++++++++++++++++++++++++++++++-- src/vcastTestInterface.ts | 33 +++-- 2 files changed, 290 insertions(+), 26 deletions(-) diff --git a/python/vTestInterface.py b/python/vTestInterface.py index 1552c925..dc9a2641 100644 --- a/python/vTestInterface.py +++ b/python/vTestInterface.py @@ -421,14 +421,238 @@ def _buildResultNameCache(sourceObject): return cache +def _buildBranchAndMcdcByLine(sourceObject): + """ + Iterates the InstrumentedFile's LIS data to build mappings from + source line numbers to Branch and MCDCDecision objects. + + Returns: (branches_by_line, mcdc_by_line) + branches_by_line: {int line_number: [Branch, ...]} + mcdc_by_line: {int line_number: MCDCDecision} + """ + branches_by_line = {} + mcdc_by_line = {} + try: + for instrumented_file in sourceObject.cover_data.instrumented_files: + for lis_data in instrumented_file.iterate_coverage(): + line_num_str = lis_data.line_number + if not line_num_str: + continue + line_num = int(line_num_str) + if lis_data.branch is not None: + branches_by_line.setdefault(line_num, []).append(lis_data.branch) + if lis_data.mcdc is not None: + mcdc_by_line[line_num] = lis_data.mcdc + except Exception: + pass + return branches_by_line, mcdc_by_line + + +def _resultCoversBranch(branch, result_id): + """ + Checks how many of the branch's directions (T/F) are covered by a + specific result. Returns (covered_count, total_count). + + A Branch has num_conditions == 1 (T-only or F-only) or 2 (both T and F). + """ + total = branch.num_conditions + covered = 0 + try: + true_ids = {r.id for r in branch.get_true_results()} + if result_id in true_ids: + covered += 1 + except Exception: + pass + if total >= 2: + try: + false_ids = {r.id for r in branch.get_false_results()} + if result_id in false_ids: + covered += 1 + except Exception: + pass + return covered, total + + +def _resultCoversMcdcDecision(mcdc, result_id): + """ + Checks how a specific result covers an MCDC decision. + Returns (covered_pairs, total_pairs, covered_branches, total_branches). + + For per-result MCDC coverage, we check: + 1. Branch directions of the decision (T/F via mcdc.branch or mcdc.conditions + where is_branch==True) + 2. Independence pairs for each condition + """ + covered_pairs = 0 + total_pairs = 0 + covered_branches = 0 + total_branches = 0 + + try: + for condition in mcdc.conditions: + if condition.is_branch: + # This is the T/F branch for the decision itself + nc = condition.num_conditions + total_branches += nc + try: + if result_id in {r.id for r in condition.get_true_results()}: + covered_branches += 1 + except Exception: + pass + if nc >= 2: + try: + if result_id in {r.id for r in condition.get_false_results()}: + covered_branches += 1 + except Exception: + pass + else: + # This is an actual MCDC condition — check its pairs + try: + for pair in condition.covered_pairs: + total_pairs += 1 + try: + pair_result_ids = {r.id for r in pair.get_results()} + if result_id in pair_result_ids: + covered_pairs += 1 + except Exception: + pass + except Exception: + pass + except Exception: + pass + + return covered_pairs, total_pairs, covered_branches, total_branches + + +def _classifyResultForLine( + result_id, + line_number, + coverageKind, + branches_by_line, + mcdc_by_line, + functionLineSet, +): + """ + Classifies a single result's coverage on a single line. + Returns "covered", "partiallyCovered", or None (if the result + doesn't meaningfully cover the line for this coverage kind). + + Mirrors the classification logic in coverageGutter.py but for + a single result instead of aggregate metrics. + """ + + if coverageKind == CoverageKind.statement: + # Statement-only: hitting the line means covered (binary) + return "covered" + + elif coverageKind == CoverageKind.statementBranch: + # Statement + Branch + branches = branches_by_line.get(line_number) + if branches: + total_dirs = 0 + covered_dirs = 0 + for branch in branches: + c, t = _resultCoversBranch(branch, result_id) + covered_dirs += c + total_dirs += t + if total_dirs == 0: + return "covered" + elif covered_dirs == total_dirs: + return "covered" + elif covered_dirs > 0: + return "partiallyCovered" + else: + # Result hit the line (is in line.results) but covered + # no branch directions — still a statement hit on a branch line. + # The aggregate handler classifies this as uncovered (red) + # because no branches are covered, so we follow suit. + return None + else: + # Pure statement line, no branches — hitting it means covered + return "covered" + + elif coverageKind == CoverageKind.branch: + # Branch-only (no statement coverage) + if line_number in functionLineSet: + # Function header lines are filtered out for branch-only + return None + branches = branches_by_line.get(line_number) + if branches: + total_dirs = 0 + covered_dirs = 0 + for branch in branches: + c, t = _resultCoversBranch(branch, result_id) + covered_dirs += c + total_dirs += t + if total_dirs == 0: + return None + elif covered_dirs == total_dirs: + return "covered" + elif covered_dirs > 0: + return "partiallyCovered" + else: + return None + else: + # Non-branch line in branch-only mode — not coverable + return None + + elif coverageKind == CoverageKind.statementMcdc: + # Statement + MCDC + mcdc = mcdc_by_line.get(line_number) + if mcdc is not None: + # MCDC decision line — check branches + pairs + cp, tp, cb, tb = _resultCoversMcdcDecision(mcdc, result_id) + total = tp + tb + covered = cp + cb + if total == 0: + # No coverable branches/pairs on this decision + return "covered" + elif covered == total: + return "covered" + elif covered > 0: + return "partiallyCovered" + else: + # Result hit the statement but no branch/pair coverage + # Aggregate handler treats this as uncovered for MCDC lines + return None + else: + # Non-MCDC statement line — hitting means covered + return "covered" + + elif coverageKind == CoverageKind.mcdc: + # MCDC-only (no statement coverage) + mcdc = mcdc_by_line.get(line_number) + if mcdc is not None: + cp, tp, cb, tb = _resultCoversMcdcDecision(mcdc, result_id) + total = tp + tb + covered = cp + cb + if total == 0: + return None + elif covered == total: + return "covered" + elif covered > 0: + return "partiallyCovered" + else: + return None + else: + # Non-MCDC line in MCDC-only mode — not coverable + return None + + # Unknown coverage kind + return None + + def getPerTestCoverageData(sourceObject): """ - Returns a dict mapping line numbers to lists of test case names - that cover each line. Uses SourceLine.results from the DataAPI - to determine which Results (test cases) hit each line. + Returns a dict mapping line numbers to dicts of test case coverage + statuses. Uses SourceLine.results and Branch/MCDC per-result APIs + from the DataAPI to determine coverage classification per test. - Format: {"lineNum": ["unit.function.testname", ...], ...} + Format: {"lineNum": {"unit.function.testname": "covered"|"partiallyCovered", ...}, ...} Only lines with at least one covering test are included. + Status is "covered" when the test covers all coverable objectives on + the line, "partiallyCovered" when it covers some but not all (branch + directions or MCDC pairs). Returns an empty dict if the source is not instrumented, the file doesn't exist on disk, or the API doesn't support per-result queries. @@ -444,19 +668,52 @@ def getPerTestCoverageData(sourceObject): if not resultNameCache: return perTestCoverage + coverageKind = getCoverageKind(sourceObject) + if coverageKind == CoverageKind.ignore: + return perTestCoverage + + # Pre-build branch/MCDC-by-line mappings for non-statement-only kinds + branches_by_line = {} + mcdc_by_line = {} + if coverageKind in ( + CoverageKind.branch, + CoverageKind.statementBranch, + CoverageKind.mcdc, + CoverageKind.statementMcdc, + ): + branches_by_line, mcdc_by_line = _buildBranchAndMcdcByLine(sourceObject) + + # Build function start line set for branch-only filtering + functionLineSet = set() + if coverageKind == CoverageKind.branch: + for function in sourceObject.cover_data.functions: + functionLineSet.add(function.start_line) + try: for line in sourceObject.iterate_coverage(): lineResults = line.results - if lineResults: - testNames = [] - for result in lineResults: - name = resultNameCache.get(result.id) - if name: - testNames.append(name) - if testNames: - perTestCoverage[str(line.line_number)] = testNames + if not lineResults: + continue + line_num = line.line_number + testStatuses = {} + for result in lineResults: + name = resultNameCache.get(result.id) + if not name: + continue + status = _classifyResultForLine( + result.id, + line_num, + coverageKind, + branches_by_line, + mcdc_by_line, + functionLineSet, + ) + if status: + testStatuses[name] = status + if testStatuses: + perTestCoverage[str(line_num)] = testStatuses except Exception: - # Gracefully degrade if line.results is not available + # Gracefully degrade if APIs are not available pass return perTestCoverage diff --git a/src/vcastTestInterface.ts b/src/vcastTestInterface.ts index e3864616..6b966832 100644 --- a/src/vcastTestInterface.ts +++ b/src/vcastTestInterface.ts @@ -193,7 +193,7 @@ interface coverageDataType { covered: number[]; uncovered: number[]; partiallyCovered: number[]; - perTestCoverage: Record; // "lineNum" -> ["unit.func.test", ...] + perTestCoverage: Record>; // "lineNum" -> {"unit.func.test": "covered"|"partiallyCovered", ...} } interface fileCoverageType { @@ -286,9 +286,9 @@ export function getCoverageDataForFileAndTest( testId: string ): coverageSummaryType { // Returns coverage data for a specific test case on a specific file. - // Uses the perTestCoverage data to determine which lines the test covers. - // Lines in perTestCoverage that include testId are "covered". - // All other coverable lines (from aggregate data) are "uncovered". + // Uses the perTestCoverage data to determine which lines the test covers + // and whether coverage is full or partial (for branch/MCDC environments). + // Lines not covered by this test are marked as uncovered. let returnData: coverageSummaryType = { hasCoverageData: false, @@ -307,6 +307,7 @@ export function getCoverageDataForFileAndTest( // Collect per-test coverage and aggregate coverable lines across all enviros const coveredByTest = new Set(); + const partiallyCoveredByTest = new Set(); const allCoverableLines = new Set(); for (const [enviroPath, enviroData] of dataForThisFile.enviroList.entries()) { @@ -322,13 +323,19 @@ export function getCoverageDataForFileAndTest( // Lines covered by this specific test // Python produces short names like "unit.function.testname"; // compose the full test node ID to match the VS Code test explorer format - for (const [lineStr, testNames] of Object.entries( + for (const [lineStr, testStatusMap] of Object.entries( enviroData.perTestCoverage )) { - for (const testName of testNames) { + for (const [testName, status] of Object.entries(testStatusMap)) { const fullTestNodeId = `vcast:${enviroPath}|${testName}`; if (fullTestNodeId === testId) { - coveredByTest.add(Number(lineStr)); + const lineNum = Number(lineStr); + if (status === "partiallyCovered") { + partiallyCoveredByTest.add(lineNum); + } else { + // "covered" or any other status defaults to covered + coveredByTest.add(lineNum); + } } } } @@ -341,12 +348,11 @@ export function getCoverageDataForFileAndTest( returnData.hasCoverageData = true; returnData.covered = [...coveredByTest]; - // Uncovered = all coverable lines minus those covered by this test + returnData.partiallyCovered = [...partiallyCoveredByTest]; + // Uncovered = all coverable lines minus those covered or partially covered returnData.uncovered = [...allCoverableLines].filter( - (line) => !coveredByTest.has(line) + (line) => !coveredByTest.has(line) && !partiallyCoveredByTest.has(line) ); - // partiallyCovered is empty — per-test coverage is binary - returnData.partiallyCovered = []; return returnData; } @@ -411,8 +417,9 @@ export function updateGlobalDataForFile(enviroPath: string, fileList: any[]) { .map(Number); const checksum = fileList[fileIndex].cmcChecksum; - const perTestCoverage: Record = - fileList[fileIndex].perTestCoverage || {}; + const perTestCoverage: Record> = fileList[ + fileIndex + ].perTestCoverage || {}; let coverageData: coverageDataType = { crc32Checksum: checksum, covered: coveredList, From 626dff1ac5908f43294b50bfbaa520a30c67f60c Mon Sep 17 00:00:00 2001 From: Patrick Bareiss Date: Tue, 17 Mar 2026 14:21:44 +0100 Subject: [PATCH 20/20] Remove debug print --- src/coverage.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/coverage.ts b/src/coverage.ts index 3d19b861..68694197 100644 --- a/src/coverage.ts +++ b/src/coverage.ts @@ -190,10 +190,6 @@ export async function updateCOVdecorations() { ? getCoverageDataForFileAndTest(filePath, reviewModeTestId) : getCoverageDataForFile(filePath); - vscode.window.showInformationMessage( - `coverage data for ${filePath}: ${JSON.stringify(coverageData)}` - ); - if (coverageData.hasCoverageData) { // there is coverage data and it matches the file checksum // Reset the global decoration arrays