diff --git a/package.json b/package.json index aaecc9e8..c7b4602f 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "main": "out/extension.js", "activationEvents": [ "workspaceContains:**/UNITDATA.VCD", - "workspaceContains:**/*.vcm" + "workspaceContains:**/*.vcm", + "workspaceContains:**/*.vcp" ], "contributes": { "viewsWelcome": [ @@ -94,6 +95,36 @@ "category": "VectorCAST Test Explorer", "title": "Re-Build Environment" }, + { + "command": "vectorcastTestExplorer.coverRemoveAllResults", + "category": "VectorCAST Test Explorer", + "title": "Cover: Remove All Results" + }, + { + "command": "vectorcastTestExplorer.coverRemoveResult", + "category": "VectorCAST Test Explorer", + "title": "Cover: Remove Result File" + }, + { + "command": "vectorcastTestExplorer.coverAddResult", + "category": "VectorCAST Test Explorer", + "title": "Cover: Add Result File" + }, + { + "command": "vectorcastTestExplorer.coverEnableInstrumentation", + "category": "VectorCAST Test Explorer", + "title": "Cover: Enable Instrumentation" + }, + { + "command": "vectorcastTestExplorer.coverDisableInstrumentation", + "category": "VectorCAST Test Explorer", + "title": "Cover: Disable Instrumentation" + }, + { + "command": "vectorcastTestExplorer.configureCoverageFilter", + "title": "Configure Coverage Filter", + "category": "VectorCAST Text Explorer" + }, { "command": "vectorcastTestExplorer.buildProjectEnviro", "category": "VectorCAST Test Explorer", @@ -664,6 +695,10 @@ "command": "vectorcastTestExplorer.buildExecuteIncremental", "when": "never" }, + { + "command": "vectorcastTestExplorer.configureCoverageFilter", + "when": "never" + }, { "command": "vectorcastTestExplorer.buildProjectEnviro", "when": "never" @@ -684,6 +719,26 @@ "command": "vectorcastTestExplorer.importEnviroToProject", "when": "never" }, + { + "command": "vectorcastTestExplorer.coverRemoveAllResults", + "when": "never" + }, + { + "command": "vectorcastTestExplorer.coverRemoveResult", + "when": "never" + }, + { + "command": "vectorcastTestExplorer.coverAddResult", + "when": "never" + }, + { + "command": "vectorcastTestExplorer.coverEnableInstrumentation", + "when": "never" + }, + { + "command": "vectorcastTestExplorer.coverDisableInstrumentation", + "when": "never" + }, { "command": "vectorcastTestExplorer.updateProjectEnvironment", "when": "never" @@ -884,6 +939,11 @@ "when": "(resourceLangId == cpp || resourceLangId == c) && vectorcastTestExplorer.globalProjectIsOpenedChecker", "group": "2_workspace" }, + { + "command": "vectorcastTestExplorer.configureCoverageFilter", + "group": "2_workspace", + "when": "resourceLangId == cpp || resourceLangId == c" + }, { "command": "vectorcastTestExplorer.addLaunchConfiguration", "when": "vectorcastTestExplorer.configured && resourceFilename==launch.json", @@ -973,7 +1033,7 @@ { "command": "vectorcastTestExplorer.openProjectInVectorCAST", "group": "vcast.project", - "when": "testId =~ /\\.vcm$/ || testId in vectorcastTestExplorer.globalProjectCompilers || testId in vectorcastTestExplorer.globalProjectTestsuites" + "when": "testId =~ /\\.vcm$/ || testId =~ /\\.vcp$/ || testId in vectorcastTestExplorer.globalProjectCompilers || testId in vectorcastTestExplorer.globalProjectTestsuites" }, { "command": "vectorcastTestExplorer.addCompilerToProject", @@ -998,102 +1058,127 @@ { "command": "vectorcastTestExplorer.updateProjectEnvironment", "group": "vcast.project", - "when": "(testId =~ /^vcast:.*$/ && (testId in vectorcastTestExplorer.vcastUnbuiltEnviroList || testId in vectorcastTestExplorer.vcastEnviroList) && vectorcastTestExplorer.globalProjectIsOpenedChecker) && !config.vectorcastTestExplorer.automaticallyUpdateManageProject" + "when": "(testId =~ /^vcast:.*$/ && (testId in vectorcastTestExplorer.vcastUnbuiltEnviroList || testId in vectorcastTestExplorer.vcastEnviroList) && vectorcastTestExplorer.globalProjectIsOpenedChecker) && !config.vectorcastTestExplorer.automaticallyUpdateManageProject && !(testId =~ /\\.vcp$/)" + }, + { + "command": "vectorcastTestExplorer.coverRemoveAllResults", + "group": "vcast.project", + "when": "testId =~ /::results$/" + }, + { + "command": "vectorcastTestExplorer.coverRemoveResult", + "group": "vcast.project", + "when": "testId in vectorcastTestExplorer.coverResultNodeIDList" + }, + { + "command": "vectorcastTestExplorer.coverAddResult", + "group": "vcast.project", + "when": "testId =~ /::results$/" + }, + { + "command": "vectorcastTestExplorer.coverEnableInstrumentation", + "group": "vcast.project", + "when": "testId =~ /\\.vcp$/ && testId not in vectorcastTestExplorer.coverInPlaceList" + }, + { + "command": "vectorcastTestExplorer.coverDisableInstrumentation", + "group": "vcast.project", + "when": "testId =~ /\\.vcp$/ && testId in vectorcastTestExplorer.coverInPlaceList" }, { "command": "vectorcastTestExplorer.buildProjectEnviro", "group": "vcast.build", - "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastUnbuiltEnviroList" + "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastUnbuiltEnviroList && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.getEnvFullReport", "group": "vcast.build", - "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastEnviroList" + "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastEnviroList && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.buildExecuteIncremental", "group": "vcast.build", - "when": "vectorcastTestExplorer.globalProjectIsOpenedChecker && ( testId =~ /\\.vcm$/ || testId in vectorcastTestExplorer.globalProjectCompilers || testId in vectorcastTestExplorer.globalProjectTestsuites || (testId =~ /^vcast:.*$/ && ( testId in vectorcastTestExplorer.vcastEnviroList || testId in vectorcastTestExplorer.vcastUnbuiltEnviroList)))" + "when": "vectorcastTestExplorer.globalProjectIsOpenedChecker && ( testId =~ /\\.vcm$/ || testId in vectorcastTestExplorer.globalProjectCompilers || testId in vectorcastTestExplorer.globalProjectTestsuites || (testId =~ /^vcast:.*$/ && ( testId in vectorcastTestExplorer.vcastEnviroList || testId in vectorcastTestExplorer.vcastUnbuiltEnviroList))) && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.rebuildEnviro", "group": "vcast.build", - "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastEnviroList && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList" + "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastEnviroList && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.cleanEnviro", "group": "vcast.build", - "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastEnviroList && vectorcastTestExplorer.globalProjectIsOpenedChecker && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList" + "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastEnviroList && vectorcastTestExplorer.globalProjectIsOpenedChecker && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.openVCAST", "group": "vcast.enviroManagement", - "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastEnviroList && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList" + "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastEnviroList && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.deleteEnviro", "group": "vcast.enviroManagement", - "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastEnviroList && !vectorcastTestExplorer.globalProjectIsOpenedChecker" + "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastEnviroList && !vectorcastTestExplorer.globalProjectIsOpenedChecker && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.deleteEnviroFromProject", "group": "vcast.enviroManagement", - "when": "testId =~ /^vcast:.*$/ && (testId in vectorcastTestExplorer.vcastUnbuiltEnviroList || testId in vectorcastTestExplorer.vcastEnviroList) && vectorcastTestExplorer.globalProjectIsOpenedChecker" + "when": "testId =~ /^vcast:.*$/ && (testId in vectorcastTestExplorer.vcastUnbuiltEnviroList || testId in vectorcastTestExplorer.vcastEnviroList) && vectorcastTestExplorer.globalProjectIsOpenedChecker && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.removeTestsuite", "group": "vcast.enviroManagement", - "when": "testId =~ /^vcast:.*$/ && (testId in vectorcastTestExplorer.vcastUnbuiltEnviroList || testId in vectorcastTestExplorer.vcastEnviroList) && vectorcastTestExplorer.globalProjectIsOpenedChecker" + "when": "testId =~ /^vcast:.*$/ && (testId in vectorcastTestExplorer.vcastUnbuiltEnviroList || testId in vectorcastTestExplorer.vcastEnviroList) && vectorcastTestExplorer.globalProjectIsOpenedChecker && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.editTestScript", "group": "vcast.testScript", - "when": "testId =~ /^vcast:.*$/ && !(testId =~ /.*coded_tests_driver.*/) && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList" + "when": "testId =~ /^vcast:.*$/ && !(testId =~ /.*coded_tests_driver.*/) && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.createTestScript", "group": "vcast.testScript", - "when": "testId =~ /^vcast:.*$/ && testId not in vectorcastTestExplorer.vcastEnviroList && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && !(testId =~ /.*coded_tests_driver.*/ )" + "when": "testId =~ /^vcast:.*$/ && testId not in vectorcastTestExplorer.vcastEnviroList && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && !(testId =~ /.*coded_tests_driver.*/ ) && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.editCodedTest", "group": "vcast.testScript", - "when": "testId =~ /^vcast:.*$/ && testId not in vectorcastTestExplorer.vcastEnviroList && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && testId =~ /.*coded_tests_driver.+/" + "when": "testId =~ /^vcast:.*$/ && testId not in vectorcastTestExplorer.vcastEnviroList && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && testId =~ /.*coded_tests_driver.+/ && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.addCodedTests", "group": "vcast.testScript", - "when": "testId =~ /^vcast:.*$/ && testId =~ /.*coded_tests_driver$/ && testId not in vectorcastTestExplorer.vcastHasCodedTestsList" + "when": "testId =~ /^vcast:.*$/ && testId =~ /.*coded_tests_driver$/ && testId not in vectorcastTestExplorer.vcastHasCodedTestsList && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.generateCodedTests", "group": "vcast.testScript", - "when": "testId =~ /^vcast:.*$/ && testId =~ /.*coded_tests_driver$/ && testId not in vectorcastTestExplorer.vcastHasCodedTestsList" + "when": "testId =~ /^vcast:.*$/ && testId =~ /.*coded_tests_driver$/ && testId not in vectorcastTestExplorer.vcastHasCodedTestsList && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.removeCodedTests", "group": "vcast.testScript", - "when": "testId =~ /^vcast:.*$/ && testId =~ /.*coded_tests_driver$/ && testId in vectorcastTestExplorer.vcastHasCodedTestsList" + "when": "testId =~ /^vcast:.*$/ && testId =~ /.*coded_tests_driver$/ && testId in vectorcastTestExplorer.vcastHasCodedTestsList && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.openSourceFileFromTestpaneCommand", "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 =~ /::file$/) && !(testId =~ /^.*<>.*$/) && !(testId =~ /^.*<>.*$/) && !(testId =~ /.*coded_tests_driver.*/) && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && testId not in vectorcastTestExplorer.vcastEnviroList" }, { "command": "vectorcastTestExplorer.insertBasisPathTests", "group": "vcast.testGeneration", - "when": "testId =~ /^vcast:.*$/ && !(testId =~ /^.*<>.*$/) && !(testId =~ /^.*<>.*$/) && !(testId =~ /.*coded_tests_driver.*/) && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList" + "when": "testId =~ /^vcast:.*$/ && !(testId =~ /^.*<>.*$/) && !(testId =~ /^.*<>.*$/) && !(testId =~ /.*coded_tests_driver.*/) && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.insertATGTests", "group": "vcast.testGeneration", - "when": "testId =~ /^vcast:.*$/ && vectorcastTestExplorer.atgAvailable && !(testId =~ /^.*<>.*$/) && !(testId =~ /^.*<>.*$/) && !(testId =~ /.*coded_tests_driver.*/) && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList" + "when": "testId =~ /^vcast:.*$/ && vectorcastTestExplorer.atgAvailable && !(testId =~ /^.*<>.*$/) && !(testId =~ /^.*<>.*$/) && !(testId =~ /.*coded_tests_driver.*/) && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.deleteTest", "group": "vcast.delete", - "when": "testId =~ /^vcast:.*$/ && !(testId =~ /.*coded_tests_driver.*/) && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList" + "when": "testId =~ /^vcast:.*$/ && !(testId =~ /.*coded_tests_driver.*/) && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.deleteTestsuite", @@ -1108,37 +1193,37 @@ { "command": "vectorcastTestExplorer.importRequirementsFromGateway", "group": "vcast@8", - "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastEnviroList && vectorcastTestExplorer.reqs2xFeatureEnabled && testId not in vectorcastTestExplorer.vcastRequirementsAvailable && vectorcastTestExplorer.generateRequirementsEnabled" + "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastEnviroList && vectorcastTestExplorer.reqs2xFeatureEnabled && testId not in vectorcastTestExplorer.vcastRequirementsAvailable && vectorcastTestExplorer.generateRequirementsEnabled && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.generateRequirements", "group": "vcast@8", - "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastEnviroList && vectorcastTestExplorer.reqs2xFeatureEnabled && testId not in vectorcastTestExplorer.vcastRequirementsAvailable && vectorcastTestExplorer.generateRequirementsEnabled" + "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastEnviroList && vectorcastTestExplorer.reqs2xFeatureEnabled && testId not in vectorcastTestExplorer.vcastRequirementsAvailable && vectorcastTestExplorer.generateRequirementsEnabled && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.showRequirements", "group": "vcast@9", - "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastEnviroList && vectorcastTestExplorer.reqs2xFeatureEnabled && testId in vectorcastTestExplorer.vcastRequirementsAvailable && vectorcastTestExplorer.generateRequirementsEnabled" + "when": "testId =~ /^vcast:.*$/ && testId in vectorcastTestExplorer.vcastEnviroList && vectorcastTestExplorer.reqs2xFeatureEnabled && testId in vectorcastTestExplorer.vcastRequirementsAvailable && vectorcastTestExplorer.generateRequirementsEnabled && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.generateTestsFromRequirements", "group": "vcast@10", - "when": "testId =~ /^vcast:.*$/ && vectorcastTestExplorer.reqs2xFeatureEnabled" + "when": "testId =~ /^vcast:.*$/ && vectorcastTestExplorer.reqs2xFeatureEnabled && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.removeRequirements", "group": "vcast@9", - "when": "testId =~ /^vcast:.*$/ && vectorcastTestExplorer.reqs2xFeatureEnabled && testId in vectorcastTestExplorer.vcastRequirementsAvailable && vectorcastTestExplorer.generateRequirementsEnabled" + "when": "testId =~ /^vcast:.*$/ && vectorcastTestExplorer.reqs2xFeatureEnabled && testId in vectorcastTestExplorer.vcastRequirementsAvailable && vectorcastTestExplorer.generateRequirementsEnabled && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.populateRequirementsGateway", "group": "vcast@9", - "when": "testId =~ /^vcast:.*$/ && vectorcastTestExplorer.reqs2xFeatureEnabled && testId in vectorcastTestExplorer.vcastRequirementsAvailable && vectorcastTestExplorer.generateRequirementsEnabled" + "when": "testId =~ /^vcast:.*$/ && vectorcastTestExplorer.reqs2xFeatureEnabled && testId in vectorcastTestExplorer.vcastRequirementsAvailable && vectorcastTestExplorer.generateRequirementsEnabled && !(testId =~ /\\.vcp$/)" }, { "command": "vectorcastTestExplorer.viewResults", "group": "vcast.results", - "when": "testId =~ /^vcast:.*$/ && testId not in vectorcastTestExplorer.vcastEnviroList && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList" + "when": "testId =~ /^vcast:.*$/ && testId not in vectorcastTestExplorer.vcastEnviroList && testId not in vectorcastTestExplorer.vcastUnbuiltEnviroList && !(testId =~ /\\.vcp$/)" } ] }, diff --git a/python/coverageGutter.py b/python/coverageGutter.py index 35e84057..f3c6581d 100644 --- a/python/coverageGutter.py +++ b/python/coverageGutter.py @@ -15,28 +15,36 @@ def getMCDCLineDic(sourceObject): unitFile = sourceObject.cover_data.name unit = unitFile.rsplit(".", 1)[0] - for mcdc in sourceObject.cover_data.mcdc_decisions: + + all_decisions = [] + covered_mcdc_pair_found = False + decision_has_covered_pairs_for_all_conditions = True + # If we overwrite a function, we need all "overwritten decisions" + for inst_file in sourceObject.instrumented_files: + all_decisions.extend(inst_file.mcdc_decisions) + for mcdc in all_decisions: # If it s not a mcdc pair --> continue if not mcdc.num_conditions: continue - + start_line = mcdc.start_line + # Per default, we set the line to be uncovered temp_line_coverage_dic[start_line] = MCDCLineCoverage.uncovered mcdc_unit_line_dic[unit] = temp_line_coverage_dic + + for condition in mcdc.conditions: + covered_pair = condition.get_covered_pair() - covered_mcdc_found = False - uncovered_mcdc_found = False - - for row in mcdc.rows: - if row.has_any_coverage != 0: - covered_mcdc_found = True + # We have at least 1 covered mcdc pair + if(covered_pair == None): + decision_has_covered_pairs_for_all_conditions = False else: - uncovered_mcdc_found = True + covered_mcdc_pair_found = True - if covered_mcdc_found == True: + if covered_mcdc_pair_found == True: # We found covered and uncovered mcdc pairs --> Partially covered - if uncovered_mcdc_found == True: + if decision_has_covered_pairs_for_all_conditions == False: temp_line_coverage_dic[start_line] = MCDCLineCoverage.partially_covered else: # We found only covered mcdc pairs --> Fully covered @@ -85,7 +93,6 @@ def handleMcdcCoverage( if use_mcdc else metrics.max_covered_branches + metrics.max_annotations_branches ) - has_branch_coverage = covered_branches > 0 # First check for the branch coverage. If it has none, it can not be partially covered / covered if has_branch_coverage: @@ -96,13 +103,13 @@ def handleMcdcCoverage( # To be fully mcdc covered: All Branches + All MCDC pairs is_fully_mcdc_covered = ( covered_branches == branch_total - and mcdc_line_coverage == MCDCLineCoverage.covered + and mcdc_line_coverage == MCDCLineCoverage.covered ) # If it's fully covered --> It's an mcdc line and fully covered --> green if is_fully_mcdc_covered: coveredString += f"{line.line_number}," - # Partially covered mcdc line --> orange - elif mcdc_line_coverage == MCDCLineCoverage.partially_covered: + # Not everything is covered but we have at least 1 covered mcdc pair --> Partially covered mcdc line --> orange + elif metrics.covered_mcdc_pairs > 0: partiallyCoveredString += f"{line.line_number}," # If it has branches covered but not mcdc pair else: @@ -156,17 +163,21 @@ def handleStatementMcdcCoverage( total_statements = metrics.statements if mcdc_line_coverage is not None: + # To be fully mcdc covered: All Statements + All Branches + All MCDC pairs + # and the line either has to be flagged covered or partially covered from getMCDCLineDic + # here we find out if it's really 100% covered or only partially covered is_fully_mcdc_covered = ( covered_statements == total_statements and covered_branches == branch_total - and mcdc_line_coverage == MCDCLineCoverage.covered + and mcdc_line_coverage == MCDCLineCoverage.covered ) # If it's fully covered --> It's an mcdc line and fully covered --> green if is_fully_mcdc_covered: coveredString += f"{line_number}," - # Partially covered mcdc line --> orange + + # Not everything is covered but we have at least 1 covered mcdc pair --> Partially covered mcdc line --> orange elif mcdc_line_coverage == MCDCLineCoverage.partially_covered: partiallyCoveredString += f"{line_number}," # a mcdc line that has no coverage --> Red diff --git a/python/custom/sections/per_line_mcdc.py b/python/custom/sections/per_line_mcdc.py index 7552d8e3..649f4850 100644 --- a/python/custom/sections/per_line_mcdc.py +++ b/python/custom/sections/per_line_mcdc.py @@ -11,14 +11,19 @@ def __init__(self, api, orig): def __get__(self, instance, owner): data = self.__orig.__get__(instance, owner) + mcdc_filter = self.__api.mcdc_filter + unit_filter = mcdc_filter.get("unit") + line_filter = mcdc_filter["line"] # Filter the data new_data = [] for decn in data: - if ( - decn.function.instrumented_file.name == self.__api.mcdc_filter["unit"] - and decn.start_line == self.__api.mcdc_filter["line"] - ): - new_data.append(decn) + if decn.start_line != line_filter: + continue + # When a unit is specified, also match the instrumented file name. + # When omitted (included-header case), accept all TUs. + if unit_filter and decn.function.instrumented_file.name != unit_filter: + continue + new_data.append(decn) return new_data @@ -62,5 +67,11 @@ def prepare_data(self): # Older versions only have the public method super().prepare_data() + # Expose whether this is a template-instantiation report + # (no specific unit → included-header with multiple TUs). + self.section_context["show_subprogram"] = ( + "unit" not in self.api.mcdc_filter + ) + # EOF diff --git a/python/custom/templates/PerLineMcdc/main.html.template b/python/custom/templates/PerLineMcdc/main.html.template index cba0f61b..cfc3c55e 100644 --- a/python/custom/templates/PerLineMcdc/main.html.template +++ b/python/custom/templates/PerLineMcdc/main.html.template @@ -8,8 +8,6 @@ {%- endif %} {%- for function in obj.functions -%} - {% if function.mcdcs %} - {% endif %} {%- for mcdc in function.mcdcs %} {% if is_sfp -%}
@@ -26,6 +24,12 @@ {% trans %}File{% endtrans %} {{mcdc.unit_name|e}} + {%- if show_subprogram %} + + {% trans %}Subprogram{% endtrans %} + {{function.name|e}} + + {%- endif %} {% if is_sfp -%}
{% trans %}Source line{% endtrans %} diff --git a/python/mcdcReport.py b/python/mcdcReport.py index 3c632695..ba8b3957 100644 --- a/python/mcdcReport.py +++ b/python/mcdcReport.py @@ -1,10 +1,12 @@ import argparse import pathlib import sys +import os from vector.apps.DataAPI.unit_test_api import UnitTestApi +from vector.apps.DataAPI.cover_api import CoverApi -from pythonUtilities import monkeypatch_custom_css +from pythonUtilities import monkeypatch_custom_css, get_api_context def parse_args(): @@ -32,15 +34,21 @@ def parse_args(): def get_mcdc_lines(env): all_lines_with_data = {} - with UnitTestApi(env) as api: - for unit in api.Unit.filter(): - for mcdc_dec in unit.cover_data.mcdc_decisions: - if not mcdc_dec.num_conditions: - continue - if unit.name not in all_lines_with_data: - all_lines_with_data[unit.name] = [] - if mcdc_dec.start_line not in all_lines_with_data[unit.name]: - all_lines_with_data[unit.name].append(mcdc_dec.start_line) + # Check if normal env or Cover --> Different API + ApiClass, entity_attr = get_api_context(env) + with ApiClass(env) as api: + sourceObjects = api.SourceFile.all() + for sourceObject in sourceObjects: + unit_file = sourceObject.cover_data.name + unit = os.path.splitext(unit_file)[0] + if sourceObject.is_instrumented: + for mcdc_dec in sourceObject.cover_data.mcdc_decisions: + if not mcdc_dec.num_conditions: + continue + if unit not in all_lines_with_data: + all_lines_with_data[unit] = [] + if mcdc_dec.start_line not in all_lines_with_data[unit]: + all_lines_with_data[unit].append(mcdc_dec.start_line) return all_lines_with_data @@ -63,16 +71,26 @@ def generate_mcdc_report(env, unit_filter, line_filter, output): # Patch get_option to use our CSS without setting the CFG option monkeypatch_custom_css(custom_css) - # Open-up the unit test API - with UnitTestApi(env) as api: - # Find and check for our unit + # We have to check whether env is the path to a "Normal" env or to a "Cover Project" + # and therefore use a different API + ApiClass, entity_attr = get_api_context(env) + + with ApiClass(env) as api: + sourceObjects = api.SourceFile.all() unit_found = False - for unit in api.Unit.filter(name=unit_filter): + for sourceObject in sourceObjects: + # Find and check for our unit + unit_file = sourceObject.cover_data.name + unit_name = os.path.splitext(unit_file)[0] + + if unit_name != unit_filter: + continue + unit_found = True - + # Spin through all MCDC decisions looking for the one on our line line_found = False - for mcdc_dec in unit.cover_data.mcdc_decisions: + for mcdc_dec in sourceObject.cover_data.mcdc_decisions: # If it has no conditions, then it generates an empty report # # TODO: do we want to just generate an empty MCDC report? @@ -89,9 +107,16 @@ def generate_mcdc_report(env, unit_filter, line_filter, output): # Record in the API instance the line number we're interested # in # - # NOTE: custom/sections/mini_mcdc.py reads this attribute to - # know what to filter! - api.mcdc_filter = {"unit": unit_filter, "line": line_filter} + # NOTE: custom/sections/per_line_mcdc.py reads this attribute + # to know what to filter! + # + # If the decision lives in a different instrumented file + # (e.g. template instantiations across TUs), filter by + # line only so all instantiations are included. + if mcdc_dec.function.instrumented_file.name == unit_filter: + api.mcdc_filter = {"unit": unit_filter, "line": line_filter} + else: + api.mcdc_filter = {"line": line_filter} # Generate our report api.report( @@ -102,15 +127,16 @@ def generate_mcdc_report(env, unit_filter, line_filter, output): ) break - # If we don't find our line, report an error - if not line_found: - raise RuntimeError(f"Could not find line {line}") + if unit_found: + # If we don't find our line, report an error + if not line_found: + raise RuntimeError(f"Could not find line {line_filter} in unit {unit_filter}") + break - # If we don't find our unit, report an error - if not unit_found: - raise RuntimeError( - f"Could not find unit {unit} (units should not have extensions)" - ) + if not unit_found: + raise RuntimeError( + f"Could not find unit {unit_filter}" + ) def main(): diff --git a/python/pythonUtilities.py b/python/pythonUtilities.py index cc70ce89..7999ea7e 100644 --- a/python/pythonUtilities.py +++ b/python/pythonUtilities.py @@ -6,6 +6,10 @@ import re from vector.apps.DataAPI.configuration import EnvironmentMixin +from vector.apps.DataAPI.unit_test_api import UnitTestApi +from vector.apps.DataAPI.cover_api import CoverApi + + # This contains the clicast command that was used to start the data server globalClicastCommand = "" @@ -233,3 +237,20 @@ def repl(match): return match.group(0) return env_var_pattern.sub(repl, path) + + +def get_api_context(env_path): + """ + Determines if the environment is a Cover Project or a standard Unit Test env. + And returns what API we need to use. + """ + clean_path = os.path.normpath(env_path) + + # Check if it's a Cover project by checking if there is a vcp file + # with the same name on the same level like the build dir (env_path) + vcp_file = clean_path + ".vcp" + if os.path.isfile(vcp_file): + return CoverApi, "File" + + # If there is no vcp file --> normal env + return UnitTestApi, "Unit" diff --git a/python/vTestInterface.py b/python/vTestInterface.py index 0e5f1bc6..af801567 100644 --- a/python/vTestInterface.py +++ b/python/vTestInterface.py @@ -36,6 +36,7 @@ from vector.apps.DataAPI.manage_api import VCProjectApi from vector.apps.DataAPI.vcproject_models import EnvironmentType from vector.apps.DataAPI.unit_test_api import UnitTestApi +from vector.apps.DataAPI.cover_api import CoverApi from vector.lib.core.system import cd from vector.enums import COVERAGE_TYPE_TYPE_T @@ -352,16 +353,19 @@ class CoverageKind: COVERAGE_TYPE_TYPE_T.STATEMENT, COVERAGE_TYPE_TYPE_T.STATEMENT_FUNCTION_CALL, COVERAGE_TYPE_TYPE_T.STATEMENT_BRANCH_FUNCTION_CALL, + COVERAGE_TYPE_TYPE_T.STATEMENT_BRANCH_FUNCTION_FUNCTION_CALL, ] mcdcCoverageList = [ COVERAGE_TYPE_TYPE_T.STATEMENT_MCDC, COVERAGE_TYPE_TYPE_T.STATEMENT_MCDC_FUNCTION_CALL, + COVERAGE_TYPE_TYPE_T.STATEMENT_MCDC_FUNCTION_FUNCTION_CALL, ] branchCoverageList = [ COVERAGE_TYPE_TYPE_T.STATEMENT_BRANCH, COVERAGE_TYPE_TYPE_T.STATEMENT_BRANCH_FUNCTION_CALL, + COVERAGE_TYPE_TYPE_T.STATEMENT_BRANCH_FUNCTION_FUNCTION_CALL, ] @@ -389,7 +393,6 @@ def getCoverageKind(sourceObject): else: return CoverageKind.ignore - def getCoverageData(sourceObject): """ This function will use the data interface to @@ -482,8 +485,121 @@ def getCoverageData(sourceObject): uncoveredString = uncoveredString[:-1] partiallyCoveredString = partiallyCoveredString[:-1] + # Remap coverage from closing brace to function start for empty functions + coveredString, partiallyCoveredString, uncoveredString = ( + remapEmptyFunctionCoverage( + sourceObject, + coveredString, + partiallyCoveredString, + uncoveredString, + ) + ) + return coveredString, uncoveredString, partiallyCoveredString, checksum +def lineInString(line_number, coverage_string): + """ + Exact token match in a comma-separated coverage string. + Avoids false positives like '8' matching inside '18'. + """ + if not coverage_string: + return False + + all_lines = coverage_string.split(",") + return str(line_number) in all_lines + + +def removeLine(line_number, coverage_string): + """ + Removes a line number from a comma-separated coverage string. + """ + if not coverage_string: + return coverage_string + + line_number_str = str(line_number) + all_lines = coverage_string.split(",") + + filtered_lines = [] + for entry in all_lines: + if entry != line_number_str: + filtered_lines.append(entry) + + return ",".join(filtered_lines) + + +def addLine(line_number, coverage_string): + """ + Appends a line number to a comma-separated coverage string (no duplicates). + """ + if not coverage_string: + return str(line_number) + + line_number_str = str(line_number) + all_lines = coverage_string.split(",") + already_present = line_number_str in all_lines + + if not already_present: + all_lines.append(line_number_str) + + return ",".join(all_lines) + + +def remapEmptyFunctionCoverage( + sourceObject, + coveredString, + partiallyCoveredString, + uncoveredString, +): + """ + VectorCAST only instruments the closing '}' of an empty/stub function, + so the coverage icon would appear on the brace instead of the function signature. + + For each function where: + - start_line has NO coverage of any kind, AND + - end_line DOES have coverage (in any List) + + move that entry from end_line to start_line, keeping whichever + state it was in: covered / partially-covered / uncovered. + """ + for function in sourceObject.cover_data.functions: + start_line = function.start_line + end_line = getattr(function, "end_line", None) + + # Nothing to remap if identical or unknown + if end_line is None or end_line == start_line: + continue + + # Skip if start_line already has coverage in any List + start_has_coverage = ( + lineInString(start_line, coveredString) + or lineInString(start_line, partiallyCoveredString) + or lineInString(start_line, uncoveredString) + ) + if start_has_coverage: + continue + + # Determine which List end_line is in + end_is_covered = lineInString(end_line, coveredString) + end_is_partial = lineInString(end_line, partiallyCoveredString) + end_is_uncovered = lineInString(end_line, uncoveredString) + + if not (end_is_covered or end_is_partial or end_is_uncovered): + continue + + # Move end_line -> start_line in the correct List + if end_is_covered: + coveredString = removeLine(end_line, coveredString) + coveredString = addLine(start_line, coveredString) + + elif end_is_partial: + partiallyCoveredString = removeLine(end_line, partiallyCoveredString) + partiallyCoveredString = addLine(start_line, partiallyCoveredString) + + elif end_is_uncovered: + uncoveredString = removeLine(end_line, uncoveredString) + uncoveredString = addLine(start_line, uncoveredString) + + return coveredString, partiallyCoveredString, uncoveredString def executeVCtest(enviroPath, testIDObject): with cd(os.path.dirname(enviroPath)): @@ -720,19 +836,28 @@ def getProjectCompilerData(api): return compilerList -def find_vce_files(root_dir): +def find_environment_files(root_dir): + """ + Scans the directory tree once and returns two lists: + 1. vce_files: Paths to vce files for Unit Test environments + 2. vcp_files: Paths to vcp files for Cover Projects + """ vce_files = [] + vcp_files = [] def scan_dir(path): with os.scandir(path) as entries: for entry in entries: - if entry.is_file() and entry.name.endswith(".vce"): - vce_files.append(entry.path) + if entry.is_file(): + if entry.name.endswith(".vce"): + vce_files.append(entry.path) + elif entry.name.endswith(".vcp"): + vcp_files.append(entry.path) elif entry.is_dir(follow_symlinks=False): scan_dir(entry.path) scan_dir(root_dir) - return vce_files + return vce_files, vcp_files def processCommandLogic(mode, clicast, pathToUse, testString="", options=""): @@ -780,10 +905,14 @@ def processCommandLogic(mode, clicast, pathToUse, testString="", options=""): elif mode == "getWorkspaceEnviroData": enviro_list = [] + vcp_list = [] errors = [] topLevel = {} - vce_files = find_vce_files(pathToUse) + # Scan directory for both file types + vce_files, vcp_files = find_environment_files(pathToUse) + + # Process VCE Files for vce_path in vce_files: try: api = UnitTestApi(vce_path) @@ -800,11 +929,41 @@ def processCommandLogic(mode, clicast, pathToUse, testString="", options=""): "mockingSupport": mocking_support, } ) - except Exception as err: errors.append(f"{vce_path}: {str(err)}") + # Process VCP Files + for vcp_path in vcp_files: + try: + api = CoverApi(vcp_path) + test_data = getVCPResultsList(api) + unit_data = getUnitData(api) + mocking_support = False # Cover projects do not support mocking + in_place = api.environment.instrumenting_in_place + + vcp_list.append( + { + "vcpPath": normalize_path(vcp_path), + "testData": test_data, + "unitData": unit_data, + "mockingSupport": mocking_support, + "inPlace": in_place + } + ) + except Exception as err: + errors.append(f"{vcp_path}: {str(err)}") + + # Construct Top Level Object + # Defaulting top-level data to the first VCE environment found (if any) + topLevel["testData"] = enviro_list[0]["testData"] if enviro_list else [] + topLevel["unitData"] = enviro_list[0]["unitData"] if enviro_list else [] + + if not topLevel["unitData"] and vcp_list: + topLevel["unitData"] = vcp_list[0]["unitData"] + topLevel["enviro"] = enviro_list + topLevel["vcp"] = vcp_list + if errors: topLevel["errors"] = errors @@ -888,6 +1047,23 @@ def processCommandLogic(mode, clicast, pathToUse, testString="", options=""): return returnCode, returnObject +def getVCPResultsList(api): + """ + Returns a list of all result file paths in the VCP project + """ + resultsList = [] + + try: + for result in api.Result.all(): + result_path = result.absolute_path + if result_path: + resultsList.append(result_path) + + except Exception as e: + print(f"Error retrieving results from VCP: {e}") + + return resultsList + def processCommand(mode, clicast, pathToUse, testString="", options=""): """ This is a wrapper for process command logic, so that we can process diff --git a/src/coverage.ts b/src/coverage.ts index e4618054..a4a6adef 100644 --- a/src/coverage.ts +++ b/src/coverage.ts @@ -1,4 +1,6 @@ import * as vscode from "vscode"; +import * as fs from "fs"; +import path = require("node:path"); import { DecorationRenderOptions, TextEditorDecorationType, @@ -9,7 +11,7 @@ import { getListOfFilesWithCoverage, } from "./vcastTestInterface"; -import { getRangeOption } from "./utilities"; +import { fileIsVCPAndInPlace, getRangeOption } from "./utilities"; import { fileDecorator } from "./fileDecorator"; import { @@ -83,6 +85,8 @@ export function initializeCodeCoverageFeatures( //fontWeight: "bold", gutterIconPath: context.asAbsolutePath("./images/light/cover-icon.svg"), }; + + initCoverageFilterStatusBarItem(context); } // global decoration arrays @@ -183,10 +187,14 @@ export async function updateCOVdecorations() { ) { const filePath = url.fileURLToPath(activeEditor.document.uri.toString()); + // We have to check if the source file is part of a Cover project AND + // whether it is instrumented in_place. If so, we do not want to show coverage. + const fileIsPartOfVCPAndInPlace = fileIsVCPAndInPlace(filePath); + // this returns the cached coverage data for this file const coverageData = getCoverageDataForFile(filePath); - if (coverageData.hasCoverageData) { + if (coverageData.hasCoverageData && !fileIsPartOfVCPAndInPlace) { // there is coverage data and it matches the file checksum // Reset the global decoration arrays resetGlobalDecorations(); @@ -273,6 +281,7 @@ export async function updateCOVdecorations() { } else { // we get here for non-C/C++ files coverageStatusBarObject.hide(); + hideCoverageFilterStatusBar(); } } @@ -331,3 +340,138 @@ export async function toggleCoverageAction() { export async function updateDisplayedCoverage() { if (coverageOn) await updateCOVdecorations(); } + +// ── Types ──────────────────────────────────────────────────── + +/** + * Shape of coverageFilter.json on disk: + * { "": ["", ...], ... } + * + * A source file that is present in the JSON has an explicit list of + * enabled enviros. A source file that is absent is treated as + * "all enviros enabled" (the default). + */ +type CoverageFilterJson = Record; + +// ── File-path helper ───────────────────────────────────────── + +/** + * Returns the path to the coverageFilter.json file for a given workspace root. + */ +export function getCoverageFilterJsonPath(workspaceRoot: string): string { + return path.join(workspaceRoot, ".vscode", "coverageFilter.json"); +} + +// ── Read / Write helpers ───────────────────────────────────── + +/** + * Reads and parses coverageFilter.json for the given workspace root. + * Returns an empty object if the file does not exist or is corrupt. + */ +export function readCoverageFilterFile( + workspaceRoot: string +): CoverageFilterJson { + const filePath = getCoverageFilterJsonPath(workspaceRoot); + if (!fs.existsSync(filePath)) return {}; + try { + return JSON.parse(fs.readFileSync(filePath, "utf8")) as CoverageFilterJson; + } catch { + // Corrupt JSON – start fresh + return {}; + } +} + +/** + * Writes data back to coverageFilter.json for the given workspace root. + * Creates .vscode/ if it does not exist yet. + */ +export function writeCoverageFilterFile( + workspaceRoot: string, + data: CoverageFilterJson +): void { + const vscodeDir = path.join(workspaceRoot, ".vscode"); + if (!fs.existsSync(vscodeDir)) { + fs.mkdirSync(vscodeDir, { recursive: true }); + } + fs.writeFileSync( + getCoverageFilterJsonPath(workspaceRoot), + JSON.stringify(data, null, 2), + "utf8" + ); +} + +// ── Public state accessor used by getCoverageDataForFile ───── + +/** + * Returns the set of enabled enviro paths for a given source file by reading + * the coverageFilter.json that belongs to that file's workspace folder. + * + * Returns undefined when no filter entry exists for this file, which means + * all enviros are enabled (the default behaviour). + */ +export function getEnabledEnvirosForFile( + filePath: string +): Set | undefined { + const workspaceFolder = vscode.workspace.getWorkspaceFolder( + vscode.Uri.file(filePath) + ); + if (!workspaceFolder) return undefined; + + const data = readCoverageFilterFile(workspaceFolder.uri.fsPath); + if (!(filePath in data)) { + // No entry yet – all enviros are enabled + return undefined; + } + return new Set(data[filePath]); +} + +let coverageFilterStatusBarItem: vscode.StatusBarItem; + +export function initCoverageFilterStatusBarItem( + context: vscode.ExtensionContext +): void { + coverageFilterStatusBarItem = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + // Adjust the priority number so the item sits where you want it relative + // to other status bar entries (higher = further right). + 99 + ); + coverageFilterStatusBarItem.color = new vscode.ThemeColor("charts.yellow"); + coverageFilterStatusBarItem.tooltip = + "Not all environments are included in the displayed coverage.\n" + + "Click to configure the coverage filter."; + coverageFilterStatusBarItem.command = + "vectorcastTestExplorer.configureCoverageFilter"; + context.subscriptions.push(coverageFilterStatusBarItem); +} + +export function updateCoverageFilterStatusBar( + totalEnviros: number, + enabledEnviros: Set | undefined +): void { + if (enabledEnviros === undefined) { + // No filter set for this file – all enviros are shown, nothing to warn about + coverageFilterStatusBarItem.hide(); + return; + } + + // Count only enabled enviros that still exist (guards against stale JSON entries + // left over after an enviro has been deleted from the project) + const enabledCount = enabledEnviros.size; + + if (enabledCount >= totalEnviros) { + // Every enviro is enabled – hide the warning + hideCoverageFilterStatusBar(); + return; + } + + // At least one enviro is filtered out – show the yellow warning + coverageFilterStatusBarItem.text = `$(warning) File Coverage: (${enabledCount}/${totalEnviros}) Environments / Projects`; + coverageFilterStatusBarItem.show(); +} + +export function hideCoverageFilterStatusBar() { + if (coverageFilterStatusBarItem) { + coverageFilterStatusBarItem.hide(); + } +} diff --git a/src/editorDecorator.ts b/src/editorDecorator.ts index 9719e6a0..6167294d 100644 --- a/src/editorDecorator.ts +++ b/src/editorDecorator.ts @@ -3,7 +3,12 @@ import { DecorationRenderOptions, TextEditorDecorationType } from "vscode"; import { testNodeType } from "./testData"; -import { getEnvPathForFilePath, getRangeOption } from "./utilities"; +import { + getEnvPathForFilePath, + getRangeOption, + normalizePath, + resolveVcpPaths, +} from "./utilities"; import { checksumMatchesEnvironment } from "./vcastTestInterface"; import { getMCDCCoverageLines } from "./vcastAdapter"; @@ -32,12 +37,14 @@ export async function updateCurrentActiveUnitMCDCLines() { let activeEditor = vscode.window.activeTextEditor; if (activeEditor) { // First we need to get the env name from the active file - const filePath = activeEditor.document.uri.fsPath; - const enviroPath = getEnvPathForFilePath(filePath); - + const filePath = normalizePath(activeEditor.document.uri.fsPath); + let enviroPath = getEnvPathForFilePath(filePath); // Get the unit name based on the file name without extension const fullPath = activeEditor.document.fileName; - const unitName = path.basename(fullPath, path.extname(fullPath)); + let unitName = path.basename(fullPath, path.extname(fullPath)); + + // If the file is in a cover project, we need adapt the paths + enviroPath = resolveVcpPaths(enviroPath, unitName, fullPath); // Get all mcdc lines for every unit and parse it into JSON if (enviroPath) { diff --git a/src/extension.ts b/src/extension.ts index 7d859e6c..baaa806f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -20,6 +20,9 @@ import { toggleCoverageAction, updateDisplayedCoverage, updateCOVdecorations, + readCoverageFilterFile, + writeCoverageFilterFile, + getCoverageFilterJsonPath, } from "./coverage"; import { @@ -49,7 +52,9 @@ import { getEnviroNodeData, getEnviroPathFromID, getTestNode, + getVcpTestNode, testNodeType, + vcpNodeType, } from "./testData"; import { @@ -68,6 +73,7 @@ import { setGlobalProjectIsOpenedChecker, setGlobalCompilerAndTestsuites, loadTestScriptButton, + getVcpDataFromCache, } from "./testPane"; import { @@ -79,6 +85,8 @@ import { forceLowerCaseDriveLetter, decodeVar, getFullEnvReport, + resolveVcpPaths, + openFileAtLine, } from "./utilities"; import { @@ -122,6 +130,7 @@ import { launchFile, globalPathToSupportFiles, initializeInstallerFiles, + clicastCommandToUse, } from "./vcastInstallation"; import { @@ -149,6 +158,7 @@ import { newTestScript, openCodedTest, ProjectEnvParameters, + globalCoverageData, } from "./vcastTestInterface"; import { @@ -169,8 +179,10 @@ import { resolveWebviewBase, setCompilerList, } from "./manage/manageSrc/manageUtils"; +import { executeWithRealTimeEchoNoCallback } from "./vcastCommandRunner"; const path = require("path"); +const url = require("url"); /** * Decodes a Base64-encoded variable name. @@ -469,6 +481,300 @@ function configureExtension(context: vscode.ExtensionContext) { ); context.subscriptions.push(insertATGTestsCommand); + // Command: vectorcastTestExplorer.coverRemoveAllResults//////////////////////////////////////////////////////// + let coverRemoveAllResultsCommand = vscode.commands.registerCommand( + "vectorcastTestExplorer.coverRemoveAllResults", + async (args: any) => { + const resultNode = getVcpTestNode(args.id); + const coverProjectName = resultNode.projectName.split(".vcp")[0]; + const cwd = path.dirname(resultNode.projectPath); + const commandArgs = [ + "-e", + coverProjectName, + "cover", + "RESult", + "REMove", + "all", + ]; + const vscodeInfoMEssage = `Removing all Results from ${cwd}`; + await executeWithRealTimeEchoNoCallback( + clicastCommandToUse, + commandArgs, + cwd, + vscodeInfoMEssage + ); + await refreshAllExtensionData(); + } + ); + context.subscriptions.push(coverRemoveAllResultsCommand); + + // Command: vectorcastTestExplorer.coverRemoveResult//////////////////////////////////////////////////////// + let coverRemoveResultCommand = vscode.commands.registerCommand( + "vectorcastTestExplorer.coverRemoveResult", + async (args: any) => { + const singleResultNode = getVcpTestNode(args.id); + const resultFilePath = singleResultNode.sourceFilePath; + const resultFileName = path.basename(resultFilePath); + const coverProjectName = singleResultNode.projectName.split(".vcp")[0]; + const cwd = path.dirname(singleResultNode.projectPath); + const commandArgs = [ + "-e", + coverProjectName, + "cover", + "RESult", + "REMove", + resultFileName, + ]; + const vscodeInfoMEssage = `Removing Result ${resultFileName} from ${cwd}`; + await executeWithRealTimeEchoNoCallback( + clicastCommandToUse, + commandArgs, + cwd, + vscodeInfoMEssage + ); + await refreshAllExtensionData(); + } + ); + context.subscriptions.push(coverRemoveResultCommand); + + // Command: vectorcastTestExplorer.coverAddResult//////////////////////////////////////////////////////// + let coverAddResultCommand = vscode.commands.registerCommand( + "vectorcastTestExplorer.coverAddResult", + async (args: any) => { + const resultNode = getVcpTestNode(args.id); + const coverProjectName = resultNode.projectName.split(".vcp")[0]; + const cwd = path.dirname(resultNode.projectPath); + + const selectedFiles = await vscode.window.showOpenDialog({ + canSelectMany: true, + canSelectFolders: false, + title: "Select Result File(s) to Add", + }); + + if (!selectedFiles || selectedFiles.length === 0) return; + + for (const file of selectedFiles) { + const resultFileName = path.basename(file.fsPath); + const commandArgs = [ + "-e", + coverProjectName, + "cover", + "RESult", + "ADD", + resultFileName, + ]; + const vscodeInfoMessage = `Adding result ${resultFileName} to ${cwd}`; + await executeWithRealTimeEchoNoCallback( + clicastCommandToUse, + commandArgs, + cwd, + vscodeInfoMessage + ); + } + + await refreshAllExtensionData(); + } + ); + context.subscriptions.push(coverAddResultCommand); + + // Command: vectorcastTestExplorer.coverEnableInstrumentation//////////////////////////////////////////////////////// + let coverEnableInstrumentationCommand = vscode.commands.registerCommand( + "vectorcastTestExplorer.coverEnableInstrumentation", + async (args: any) => { + const vcpNode = getVcpDataFromCache(args.id); + const projectPath = vcpNode.vcpPath.split(".vcp")[0]; + const coverProjectName = path.basename(projectPath); + const cwd = path.dirname(projectPath); + const commandArgs = [ + "-e", + coverProjectName, + "cover", + "environment", + "enable_instrumentation", + ]; + const vscodeInfoMessage = `Enabling instrumentation for ${coverProjectName}`; + await executeWithRealTimeEchoNoCallback( + clicastCommandToUse, + commandArgs, + cwd, + vscodeInfoMessage + ); + await refreshAllExtensionData(); + } + ); + context.subscriptions.push(coverEnableInstrumentationCommand); + + // Command: vectorcastTestExplorer.coverDisableInstrumentation//////////////////////////////////////////////////////// + let coverDisableInstrumentationCommand = vscode.commands.registerCommand( + "vectorcastTestExplorer.coverDisableInstrumentation", + async (args: any) => { + const vcpNode = getVcpDataFromCache(args.id); + const projectPath = vcpNode.vcpPath.split(".vcp")[0]; + const coverProjectName = path.basename(projectPath); + const cwd = path.dirname(projectPath); + const commandArgs = [ + "-e", + coverProjectName, + "cover", + "environment", + "disable_instrumentation", + ]; + const vscodeInfoMessage = `Disabling instrumentation for ${coverProjectName}`; + await executeWithRealTimeEchoNoCallback( + clicastCommandToUse, + commandArgs, + cwd, + vscodeInfoMessage + ); + await refreshAllExtensionData(); + } + ); + context.subscriptions.push(coverDisableInstrumentationCommand); + + let configureCoverageFilterCommand = vscode.commands.registerCommand( + "vectorcastTestExplorer.configureCoverageFilter", + async (fileUri?: vscode.Uri) => { + // Resolve the target file path – prefer a URI passed by a menu + // contribution, fall back to the currently active editor. + let filePath: string | undefined; + if (fileUri) { + filePath = fileUri.fsPath; + } else { + const editor = vscode.window.activeTextEditor; + if (editor) { + filePath = url.fileURLToPath(editor.document.uri.toString()); + } + } + + if (!filePath) { + vscode.window.showErrorMessage("No active C/C++ file found."); + return; + } + + const ext = path.extname(filePath).toLowerCase(); + if (![".c", ".cpp", ".h"].includes(ext)) { + vscode.window.showErrorMessage( + "Coverage filter is only available for .c, .cpp, and .h files." + ); + return; + } + + const fileData = globalCoverageData.get(filePath); + // The keys of enviroList are the enviro paths – these are the tickable items + const enviros: string[] = fileData ? [...fileData.enviroList.keys()] : []; + + if (enviros.length === 0) { + vscode.window.showInformationMessage( + "No environments are associated with this file." + ); + return; + } + + // Read the persisted state from disk for this file's workspace. + // If the file is not yet in the JSON, default to all enviros enabled. + const workspaceFolder = + vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)) ?? + vscode.workspace.workspaceFolders?.[0]; + + const workspaceRoot = workspaceFolder!.uri.fsPath; + + // Ensure the file exists the first time (all enviros ticked by default) + const vscodeDir = path.join(workspaceRoot, ".vscode"); + const filterFilePath = getCoverageFilterJsonPath(workspaceRoot); + if (!fs.existsSync(filterFilePath)) { + if (!fs.existsSync(vscodeDir)) { + fs.mkdirSync(vscodeDir, { recursive: true }); + } + fs.writeFileSync(filterFilePath, "{}\n", "utf8"); + } + + const filterData = readCoverageFilterFile(workspaceRoot); + // If no entry exists yet, treat all enviros as enabled + const enabledList: string[] = + filePath in filterData ? filterData[filePath] : [...enviros]; + + const baseDir = resolveWebviewBase(context); + const panel = vscode.window.createWebviewPanel( + "coverageFilter", + "Coverage Filter", + vscode.ViewColumn.Active, + { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: [vscode.Uri.file(baseDir)], + } + ); + + // ── Build webview HTML (same pattern as getNewProjectWebviewContent) ── + const cssOnDisk = vscode.Uri.file( + path.join(baseDir, "css", "coverageFilter.css") + ); + const scriptOnDisk = vscode.Uri.file( + path.join(baseDir, "webviewScripts", "coverageFilter.js") + ); + const htmlPath = path.join(baseDir, "html", "coverageFilter.html"); + const cssUri = panel.webview.asWebviewUri(cssOnDisk); + const scriptUri = panel.webview.asWebviewUri(scriptOnDisk); + const nonce = getNonce(); + + let html = fs.readFileSync(htmlPath, "utf8"); + html = html.replace( + //, + ` + + ` + ); + html = html.replace("{{ cssUri }}", cssUri.toString()); + html = html.replace( + "{{ scriptUri }}", + `` + ); + panel.webview.html = html; + + panel.webview.onDidReceiveMessage( + async (msg: { + command: string; + filePath?: string; + enabledEnviros?: string[]; + }) => { + switch (msg.command) { + case "apply": { + if (msg.filePath && msg.enabledEnviros) { + // Write the updated selection back to disk + const updated = readCoverageFilterFile(workspaceRoot); + updated[msg.filePath] = msg.enabledEnviros; + writeCoverageFilterFile(workspaceRoot, updated); + + vscode.window.showInformationMessage( + `Coverage filter updated: ${msg.enabledEnviros.length}/${enviros.length} environment(s) enabled.` + ); + + // Redraw coverage decorations immediately + await updateDisplayedCoverage(); + } + panel.dispose(); + break; + } + + case "cancel": + panel.dispose(); + break; + } + }, + undefined, + context.subscriptions + ); + } + ); + + context.subscriptions.push(configureCoverageFilterCommand); + // Command: vectorcastTestExplorer.insertATGTestsFromEditor//////////////////////////////////////////////////////// let insertATGTestsFromEditorCommand = vscode.commands.registerCommand( "vectorcastTestExplorer.insertATGTestsFromEditor", @@ -1222,10 +1528,14 @@ function configureExtension(context: vscode.ExtensionContext) { const filePath = activeEditor ? activeEditor.document.uri.fsPath : fileFromUri; - const enviroPath = getEnvPathForFilePath(filePath); - const fileName = path.parse(filePath).name; + let enviroPath = getEnvPathForFilePath(filePath); + let unitName = path.parse(filePath).name; + + // If the file is in a cover project, we need adapt the paths + enviroPath = resolveVcpPaths(enviroPath, unitName, filePath); + if (enviroPath) { - viewMCDCReport(enviroPath, fileName, args.lineNumber); + viewMCDCReport(enviroPath, unitName, args.lineNumber); } else { vscode.window.showErrorMessage( `Did not find environment name ${enviroPath} or path for file: ${filePath}` @@ -1240,69 +1550,64 @@ function configureExtension(context: vscode.ExtensionContext) { let openSourceFileFromTestpaneCommand = vscode.commands.registerCommand( "vectorcastTestExplorer.openSourceFileFromTestpaneCommand", async (args: any) => { - if (args) { - const testNode: testNodeType = getTestNode(args.id); - if (testNode) { - const enviroPath = testNode.enviroPath; - const unitName = testNode.unitName; - const functionName = testNode.functionName; - const envData = await getEnvironmentData(enviroPath); - - if (envData.unitData) { - for (const unitInfo of envData.unitData) { - // Extract unit name from path to match against unitName - const pathBasename = path.basename( - unitInfo.path, - path.extname(unitInfo.path) - ); - - if (pathBasename === unitName) { - const sourcePath = unitInfo.path; - const uri = vscode.Uri.file(sourcePath); - - // Determine the line number to open at (0 = top default) - let lineNumber = 0; - - // If functionName is defined, try to find it in the function list - if (functionName && unitInfo.functionList) { - for (const func of unitInfo.functionList) { - if ( - func.name === functionName && - func.startLine !== undefined - ) { - lineNumber = func.startLine; - break; - } - } - } - - // Open the document at the specified line - const document = await vscode.workspace.openTextDocument(uri); - const position = new vscode.Position( - Math.max(0, lineNumber - 1), - 0 - ); - const selection = new vscode.Range(position, position); + if (!args) return; + + const fileID: string = args.id; + const isVcpFile = fileID.endsWith("::file"); + let testNode: testNodeType | vcpNodeType; + // If it's a vcp file, we do not have additional info like function or unit, so we just open it up at line 0 + if (isVcpFile) { + testNode = getVcpTestNode(fileID); + const sourceFilePath = testNode.sourceFilePath; + if (!testNode || !sourceFilePath) { + vscode.window.showErrorMessage( + `Unable to open Source File for Node: ${fileID}` + ); + return; + } + await openFileAtLine(sourceFilePath, 0); + return; + } - await vscode.window.showTextDocument(document, { - preview: false, // open as a real tab - preserveFocus: false, - selection: selection, - }); + // If it's a unit test node, we can open up the source file more specifically + testNode = getTestNode(fileID); + if (!testNode) { + vscode.window.showErrorMessage( + `Unable to open Source File for Node: ${fileID}` + ); + return; + } - break; - } + const envData = await getEnvironmentData(testNode.enviroPath); + if (!envData.unitData) { + vscode.window.showErrorMessage( + `Could not find environment data for: ${testNode.enviroPath}` + ); + return; + } + + for (const unitInfo of envData.unitData) { + const pathBasename = path.basename( + unitInfo.path, + path.extname(unitInfo.path) + ); + if (pathBasename !== testNode.unitName) continue; + + let lineNumber = 0; + if (testNode.functionName && unitInfo.functionList) { + for (const func of unitInfo.functionList) { + if ( + func.name === testNode.functionName && + func.startLine !== undefined + ) { + lineNumber = func.startLine; + break; } - } else { - vscode.window.showErrorMessage( - `Could not find environment data for: ${enviroPath}` - ); } - } else { - vscode.window.showErrorMessage( - `Unable to open Source File for Node: ${args.id}` - ); } + + await openFileAtLine(unitInfo.path, lineNumber); + break; } } ); diff --git a/src/manage/webviews/css/coverageFilter.css b/src/manage/webviews/css/coverageFilter.css new file mode 100644 index 00000000..f1a0833b --- /dev/null +++ b/src/manage/webviews/css/coverageFilter.css @@ -0,0 +1,135 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background-color: #1e1e1e; + color: #d4d4d4; + display: flex; + align-items: flex-start; + justify-content: center; + padding: 30px 20px; + min-height: 100vh; +} + +.modal { + width: 750px; + background-color: #252526; + padding: 25px; + border-radius: 8px; + box-shadow: 0 0 10px rgba(0,0,0,0.3); +} + +.modal h2 { + text-align: center; + color: #ffffff; + font-size: 22px; + margin-bottom: 8px; +} + +.subtitle { + text-align: center; + color: #9cdcfe; + font-size: 12px; + margin-bottom: 18px; + word-break: break-all; +} + +.enviro-list { + display: flex; + flex-direction: column; + gap: 6px; + max-height: 55vh; + overflow-y: auto; + margin-bottom: 18px; + border: 1px solid #3c3c3c; + border-radius: 4px; + padding: 10px; + background: #1e1e1e; +} + +.enviro-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + border-radius: 4px; + background: #2d2d2d; + border: 1px solid #3c3c3c; + cursor: pointer; + transition: background 0.15s; +} + +.enviro-item:hover { + background: #37373d; +} + +.enviro-item input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: #007acc; + flex-shrink: 0; +} + +.enviro-item label { + font-size: 13px; + color: #d4d4d4; + cursor: pointer; + word-break: break-all; + flex: 1; + margin: 0; +} + +.enviro-item.checked { + border-color: #007acc44; + background: #1a3a50; +} + +.empty-msg { + color: #888; + font-size: 13px; + text-align: center; + padding: 20px 0; +} + +.button-container { + display: flex; + align-items: center; + gap: 10px; + margin-top: 4px; +} + +.primary-button, +.cancel-button, +.select-button { + padding: 8px 18px; + font-size: 14px; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; +} + +.primary-button { + background-color: #007acc; + color: white; +} +.primary-button:hover { background-color: #005f99; } + +.cancel-button { + background-color: #cc4444; + color: white; +} +.cancel-button:hover { background-color: #992222; } + +.select-button { + background-color: #3c3c3c; + color: #d4d4d4; + border: 1px solid #555; +} +.select-button:hover { background-color: #505050; } +.select-button.deselect { } diff --git a/src/manage/webviews/html/coverageFilter.html b/src/manage/webviews/html/coverageFilter.html new file mode 100644 index 00000000..0358d80f --- /dev/null +++ b/src/manage/webviews/html/coverageFilter.html @@ -0,0 +1,29 @@ + + + + + + Coverage Filter + + + + + + {{ scriptUri }} + + diff --git a/src/manage/webviews/webviewScripts/coverageFilter.js b/src/manage/webviews/webviewScripts/coverageFilter.js new file mode 100644 index 00000000..7b955d50 --- /dev/null +++ b/src/manage/webviews/webviewScripts/coverageFilter.js @@ -0,0 +1,90 @@ +const vscode = acquireVsCodeApi(); + +window.addEventListener("DOMContentLoaded", () => { + /** @type {string[]} */ + const enviros = window.enviroData || []; + /** @type {string[]} */ + const enabledEnviros = window.enabledEnviros || []; + /** @type {string} */ + const filePath = window.filePath || ""; + + document.getElementById("fileLabel").textContent = filePath; + + const listEl = document.getElementById("enviroList"); + + // Track state: map enviroPath -> checked boolean + /** @type {Map} */ + const state = new Map(); + for (const e of enviros) { + // enabled if present in enabledEnviros (or enabledEnviros is empty → all enabled) + const isEnabled = + enabledEnviros.length === 0 ? true : enabledEnviros.includes(e); + state.set(e, isEnabled); + } + + function renderList() { + listEl.innerHTML = ""; + if (enviros.length === 0) { + const msg = document.createElement("div"); + msg.className = "empty-msg"; + msg.textContent = "No environments found for this file."; + listEl.appendChild(msg); + return; + } + + for (const envPath of enviros) { + const checked = state.get(envPath) ?? true; + + const item = document.createElement("div"); + item.className = "enviro-item" + (checked ? " checked" : ""); + + const cb = document.createElement("input"); + cb.type = "checkbox"; + cb.id = "cb_" + envPath; + cb.checked = checked; + + const lbl = document.createElement("label"); + lbl.htmlFor = cb.id; + lbl.textContent = envPath; + + cb.addEventListener("change", () => { + state.set(envPath, cb.checked); + item.classList.toggle("checked", cb.checked); + }); + + // clicking the row also toggles + item.addEventListener("click", (e) => { + if (e.target === cb || e.target === lbl) return; + cb.checked = !cb.checked; + cb.dispatchEvent(new Event("change")); + }); + + item.appendChild(cb); + item.appendChild(lbl); + listEl.appendChild(item); + } + } + + renderList(); + + document.getElementById("btnSelectAll").addEventListener("click", () => { + for (const k of state.keys()) state.set(k, true); + renderList(); + }); + + document.getElementById("btnDeselectAll").addEventListener("click", () => { + for (const k of state.keys()) state.set(k, false); + renderList(); + }); + + document.getElementById("btnApply").addEventListener("click", () => { + const enabled = [...state.entries()] + .filter(([, v]) => v) + .map(([k]) => k); + vscode.postMessage({ command: "apply", filePath, enabledEnviros: enabled }); + }); + + document.getElementById("btnCancel").addEventListener("click", () => { + vscode.postMessage({ command: "cancel" }); + }); +}); diff --git a/src/testData.ts b/src/testData.ts index d1d01b1d..d13ea69d 100644 --- a/src/testData.ts +++ b/src/testData.ts @@ -1,6 +1,8 @@ import { normalizePath, quote } from "./utilities"; +import * as vscode from "vscode"; export interface environmentNodeDataType { + isVcp: boolean; projectPath: string; buildDirectory: string; isBuilt: boolean; @@ -30,8 +32,92 @@ export function clearEnviroDataCache() { environmentDataCache.clear(); } +export let coverFilesNodeIDList: string[] = []; +export let coverResultNodeIDList: string[] = []; +export let coverInPlaceList: string[] = []; + export const compoundOnlyString = " [compound only]"; +export interface vcpNodeType { + vcpNodeID: string; + projectPath: string; + projectName: string; + isVcpFile: boolean; + isVcpResult: boolean; + projectNodeID: string; + inPlace: boolean; + sourceFilePath?: string; +} + +export const vcpNodeCache = new Map(); + +export function createVcpNodeInCache( + vcpNodeID: string, + projectPath: string, + projectName: string, + projectNodeID: string, + isVcpFile: boolean, + isVcpResult: boolean, + inPlace: boolean, + sourceFilePath?: string +) { + let vcpNode: vcpNodeType = { + vcpNodeID: vcpNodeID, + projectPath: projectPath, + projectName: projectName, + projectNodeID: projectNodeID, + isVcpFile: isVcpFile, + isVcpResult: isVcpResult, + inPlace: inPlace, + sourceFilePath: sourceFilePath, + }; + + // Prepare the lists for the package.json --> 1) All Files, 2) All Results, 3) All VCPs in place for coverage + vcpNodeCache.set(vcpNodeID, vcpNode); + if (vcpNode.isVcpFile && !coverFilesNodeIDList.includes(vcpNodeID)) { + coverFilesNodeIDList.push(vcpNodeID); + } else if (vcpNode.isVcpResult && !coverFilesNodeIDList.includes(vcpNodeID)) { + coverResultNodeIDList.push(vcpNodeID); + } + + if (inPlace) { + if (!coverInPlaceList.includes(projectNodeID)) { + coverInPlaceList.push(projectNodeID); + } + } else { + // Remove it from the list if it was there before + coverInPlaceList = coverInPlaceList.filter((id) => id !== projectNodeID); + } + + setCoverProjectContext(); +} + +export function removeVcpNodeFromCache(nodeID: string) { + vcpNodeCache.delete(nodeID); +} + +export function getVcpTestNode(nodeID: string): vcpNodeType { + return vcpNodeCache.get(nodeID); +} + +export function setCoverProjectContext() { + vscode.commands.executeCommand( + "setContext", + "vectorcastTestExplorer.coverFilesNodeIDList", + coverFilesNodeIDList + ); + vscode.commands.executeCommand( + "setContext", + "vectorcastTestExplorer.coverResultNodeIDList", + coverResultNodeIDList + ); + vscode.commands.executeCommand( + "setContext", + "vectorcastTestExplorer.coverInPlaceList", + coverInPlaceList + ); +} + export interface testNodeType { enviroNodeID: string; enviroPath: string; // the full path including the enviro directory diff --git a/src/testPane.ts b/src/testPane.ts index 4d6183cf..3f844174 100644 --- a/src/testPane.ts +++ b/src/testPane.ts @@ -41,6 +41,7 @@ import { saveEnviroNodeData, testNodeType, testNodeCache, + createVcpNodeInCache, } from "./testData"; import { @@ -136,13 +137,16 @@ type UnitData = { export type EnviroData = { vcePath: string; + vcpPath: string; testData: FileTestData[]; unitData: UnitData[]; mockingSupport: boolean; + inPlace: boolean; }; export type CachedWorkspaceData = { enviro: EnviroData[]; + vcp: EnviroData[]; errors?: string[]; }; @@ -629,6 +633,34 @@ export function addFreeEnvironments( } } +/** + * Adds VCP (VectorCAST Cover Project) files found in the workspace cache to the environment list. + * VCP Nodes are different as they only have a "Files" and "Results" as children. + * @param environmentList A list to push environment data. + * @param workspaceRoot The workspace root directory path. + */ +export function addVcpEnvironments( + environmentList: any[], + workspaceRoot: string +): void { + // Check if the cached data exists and has the 'vcp' key we added in Python + if (cachedWorkspaceEnvData?.vcp) { + for (const vcpData of cachedWorkspaceEnvData.vcp) { + const normalizedPath = normalizePath(vcpData.vcpPath); + const displayName = path.basename(normalizedPath); + environmentList.push({ + projectPath: normalizedPath, // VCP path + buildDirectory: normalizedPath, + isBuilt: true, + displayName: displayName, + workspaceRoot: workspaceRoot, + isVcp: true, + inPlace: vcpData.inPlace, + }); + } + } +} + /** * Checks if the given path is an environment of interest. * @param candidatePath - The path to check @@ -718,13 +750,16 @@ export async function updateTestsForEnvironment( enviroData: environmentNodeDataType, comingFromRefresh: boolean = false ) { - let jsonData: any; + // Handle VCP files specially + if (enviroData.isVcp && parentNode) { + await createVcpChildNodes(parentNode, enviroData); + return; + } - // In case we are refreshing the extension, we do not want to call the API n times (n=|envs|) - // Instead we get the entire env data at once and save us time. + // Regular environment handling + let jsonData: any; jsonData = await loadEnviroData(enviroData, comingFromRefresh); if (!jsonData) return; - await processSingleEnvData(parentNode, enviroData, jsonData); } @@ -738,17 +773,29 @@ async function loadEnviroData( comingFromRefresh: boolean ): Promise { let buildDirDerivedFromVCEPath: string = ""; + let buildDirDerivedFromVCPPath: string = ""; let buildPathDir: string = enviroData.buildDirectory; if (comingFromRefresh) { // If we've already fetched the full workspace data, reuse it if (cachedWorkspaceEnvData) { - const enviroList = cachedWorkspaceEnvData["enviro"]; - vectorMessage(`Processing environment data for: ${buildPathDir}`); - for (const envAPIData of enviroList) { - buildDirDerivedFromVCEPath = envAPIData.vcePath.split(".vce")[0]; - if (buildDirDerivedFromVCEPath === buildPathDir) { - return envAPIData; + if (enviroData.isVcp) { + const vcpList = cachedWorkspaceEnvData["vcp"]; + vectorMessage(`Processing coverage project data for: ${buildPathDir}`); + for (const vcpAPIData of vcpList) { + buildDirDerivedFromVCPPath = vcpAPIData.vcpPath; + if (buildDirDerivedFromVCPPath === buildPathDir) { + return vcpAPIData; + } + } + } else { + const enviroList = cachedWorkspaceEnvData["enviro"]; + vectorMessage(`Processing environment data for: ${buildPathDir}`); + for (const envAPIData of enviroList) { + buildDirDerivedFromVCEPath = envAPIData.vcePath.split(".vce")[0]; + if (buildDirDerivedFromVCEPath === buildPathDir) { + return envAPIData; + } } } } else { @@ -986,6 +1033,9 @@ async function loadAllVCTests( ignoreEnvsInProject, environmentList ); + + // Add VCP (Cover Project) files + addVcpEnvironments(environmentList, workspaceRoot); } if (environmentList.length > 0) { @@ -1827,7 +1877,23 @@ function getParentNodeForEnvironment( return null; } - // Managed project branch. + // VCP Cover Project + if (enviroData.isVcp) { + let projectNode = globalProjectMap.get(enviroData.projectPath); + if (!projectNode) { + const projectDisplayName = path.basename(enviroData.projectPath); + projectNode = globalController.createTestItem( + enviroData.projectPath, + projectDisplayName + ) as vcastTestItem; + projectNode.nodeKind = nodeKind.vcpProject; + globalController.items.add(projectNode); + globalProjectMap.set(enviroData.projectPath, projectNode); + } + return projectNode; + } + + // Managed project let projectNode = globalProjectMap.get(enviroData.projectPath); if (!projectNode) { // If project node doesn't exist, create a new one. @@ -1840,9 +1906,7 @@ function getParentNodeForEnvironment( globalController.items.add(projectNode); globalProjectMap.set(enviroData.projectPath, projectNode); } - let currentParent = createHierarchy(pathParts, projectNode); - return currentParent; } @@ -2072,7 +2136,9 @@ export async function updateCodedTestCases(editor: any) { } // special is for compound and init + export enum nodeKind { + vcpProject, projectGroup, project, environmentGroup, @@ -2083,6 +2149,9 @@ export enum nodeKind { test, compiler, testsuite, + vcpFiles, + vcpResults, + vcpSourceFile, } export interface vcastTestItem extends vscode.TestItem { // this is a simple wrapper that allows us to add additional @@ -2113,3 +2182,134 @@ function checkWorkspaceEnvDataForErrors() { } } } + +/** + * Creates "Files" and "Results" child nodes under a VCP project node + */ +async function createVcpChildNodes( + vcpProjectNode: vcastTestItem, + enviroData: environmentNodeDataType +): Promise { + // Get the cached VCP data to access unitData and testData + const vcpData = getVcpDataFromCache(enviroData.buildDirectory); + saveEnviroNodeData(enviroData.buildDirectory, enviroData); + updateGlobalDataForFile(enviroData.buildDirectory, vcpData.unitData); + const inPlace = vcpData.inPlace; + const projectNodeID = vcpProjectNode.id; + + // Create "Files" node + const filesNodeId = `${enviroData.buildDirectory}::files`; + const projectName = path.basename(enviroData.projectPath); + const filesNode = globalController.createTestItem( + filesNodeId, + "Files" + ) as vcastTestItem; + createVcpNodeInCache( + filesNodeId, + normalizePath(enviroData.projectPath), + projectName, + projectNodeID, + false, + false, + inPlace + ); + filesNode.nodeKind = nodeKind.vcpFiles; + vcpProjectNode.children.add(filesNode); + + // Add source files as children under Files node + if (vcpData?.unitData && Array.isArray(vcpData.unitData)) { + for (const unit of vcpData.unitData) { + if (unit.path) { + const unitFileName = path.basename(unit.path); + const sourceFilePath = unit.path; + const unitNodeId = `${sourceFilePath}::file`; + + const unitNode = globalController.createTestItem( + unitNodeId, + unitFileName, + vscode.Uri.file(unit.path) + ) as vcastTestItem; + + unitNode.nodeKind = nodeKind.vcpSourceFile; + unitNode.canResolveChildren = false; + + filesNode.children.add(unitNode); + + createVcpNodeInCache( + unitNodeId, + normalizePath(enviroData.projectPath), + projectName, + projectNodeID, + true, + false, + inPlace, + normalizePath(sourceFilePath) + ); + } + } + } + + // Create "Results" node + const resultsNodeId = `${enviroData.buildDirectory}::results`; + const resultsNode = globalController.createTestItem( + resultsNodeId, + "Results" + ) as vcastTestItem; + resultsNode.nodeKind = nodeKind.vcpResults; + vcpProjectNode.children.add(resultsNode); + + createVcpNodeInCache( + resultsNodeId, + normalizePath(enviroData.projectPath), + projectName, + projectNodeID, + false, + false, + inPlace + ); + + // Add .lua result files as children under Results node + if (vcpData?.testData && Array.isArray(vcpData.testData)) { + for (const resultFile of vcpData.testData) { + // resultFile should be the full path to all.lua or api.lua + if (resultFile && typeof resultFile === "string") { + const resultFileName = path.basename(resultFile); + const resultNodeId = `${resultFile}::result`; + + const resultNode = globalController.createTestItem( + resultNodeId, + resultFileName, + vscode.Uri.file(resultFile) + ) as vcastTestItem; + + resultNode.nodeKind = nodeKind.vcpResults; + resultNode.canResolveChildren = false; + + resultsNode.children.add(resultNode); + createVcpNodeInCache( + resultNodeId, + normalizePath(enviroData.projectPath), + projectName, + projectNodeID, + false, + true, + inPlace, + normalizePath(resultFile) + ); + } + } + } +} + +/** + * Helper function to get VCP data from the workspace cache + */ +export function getVcpDataFromCache(vcpPath: string): any { + if (cachedWorkspaceEnvData?.vcp) { + const normalizedPath = normalizePath(vcpPath); + return cachedWorkspaceEnvData.vcp.find( + (vcp: any) => normalizePath(vcp.vcpPath) === normalizedPath + ); + } + return null; +} diff --git a/src/utilities.ts b/src/utilities.ts index 7087a279..64849f57 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -12,6 +12,7 @@ import { rebuildEnvironmentCallback } from "./callbacks"; import { CachedWorkspaceData, EnviroData } from "./testPane"; import { executeWithRealTimeEchoWithProgress } from "./vcastCommandRunner"; import { getVectorCastInstallationLocation } from "./vcastInstallation"; +import { vcpNodeCache, vcpNodeType } from "./testData"; const fs = require("fs"); const glob = require("glob"); @@ -407,7 +408,9 @@ export async function updateCoverageAndRebuildEnv() { } // Now rebuild every env so that the coverage is updated for (let enviroPath of envArray) { - await rebuildEnvironment(enviroPath, rebuildEnvironmentCallback); + if (!enviroPath.endsWith(".vcp")) { + await rebuildEnvironment(enviroPath, rebuildEnvironmentCallback); + } } } @@ -428,11 +431,15 @@ export async function mergeWorkspaceEnvResponses( ): Promise { const allErrors: string[] = []; const allEnvs: EnviroData[] = []; + const allVcp: EnviroData[] = []; for (const resp of responses) { if (resp.errors) { allErrors.push(...resp.errors); } + if (resp.vcp) { + allVcp.push(...resp.vcp); + } if (resp.enviro) { allEnvs.push(...resp.enviro); } @@ -440,6 +447,7 @@ export async function mergeWorkspaceEnvResponses( return { enviro: allEnvs, + vcp: allVcp, errors: allErrors.length ? allErrors : undefined, }; } @@ -480,3 +488,68 @@ export async function getFullEnvReport( // Return the generated HTML file path return htmlReportPath; } + +/** + * Checks if the environment is a Cover Project (.vcp). + * If so, it adjusts the environment path (removing .vcp) and appends the + * file extension to the unit name, as required by the Cover API. + */ +export function resolveVcpPaths( + enviroPath: string | null, + unitName: string, + fullFilePath: string +) { + if (enviroPath?.endsWith(".vcp")) { + // Cover projects require the full filename (e.g. "manager.c" instead of "manager") + unitName = unitName + path.extname(fullFilePath); + + // The build directory for VCP is the path without the .vcp extension + const parsed = path.parse(enviroPath); + enviroPath = path.join(parsed.dir, parsed.name); + } + + return enviroPath; +} + +export async function openFileAtLine( + filePath: string, + lineNumber: number +): Promise { + const uri = vscode.Uri.file(filePath); + const document = await vscode.workspace.openTextDocument(uri); + const position = new vscode.Position(Math.max(0, lineNumber - 1), 0); + const selection = new vscode.Range(position, position); + + await vscode.window.showTextDocument(document, { + preview: false, + preserveFocus: false, + selection: selection, + }); +} + +/** + * Returns true if the given file path is part of a vcp project and is in_place + */ +export function fileIsVCPAndInPlace(filePath: string) { + let vcpNode = getVcpNodeBySourceFilePath(filePath); + + if (vcpNode?.inPlace) { + return true; + } else { + return false; + } +} + +/** + * Returns vcp node for a given source file path + */ +export function getVcpNodeBySourceFilePath( + sourceFilePath: string +): vcpNodeType | undefined { + for (const node of vcpNodeCache.values()) { + if (node.sourceFilePath === sourceFilePath) { + return node; + } + } + return undefined; +} diff --git a/src/vcastCommandRunner.ts b/src/vcastCommandRunner.ts index 28eade48..bd943b4a 100644 --- a/src/vcastCommandRunner.ts +++ b/src/vcastCommandRunner.ts @@ -616,3 +616,84 @@ export async function executeClicastCommandUsingServer( } return commandStatus; } + +export function executeWithRealTimeEchoNoCallback( + command: string, + argList: string[], + CWD: string, + vscodeMessage: string +) { + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: `${vscodeMessage}`, + cancellable: true, + }, + async (progress, token) => { + progress.report({ increment: 10 }); + + let processHandle = spawn(command, argList, { + cwd: CWD, + windowsHide: true, + }); + vectorMessage("-".repeat(100)); + vectorMessage("-".repeat(100)); + let messageFragment: string = ""; + + let progressValue = 10; + const progressInterval = setInterval(() => { + if (progressValue < 90) { + progressValue += 15; + progress.report({ increment: 10 }); + } + }, 3000); + + token.onCancellationRequested(() => { + if (processHandle) { + processHandle.kill(); + vectorMessage(`User cancelled the operation.`); + } + clearInterval(progressInterval); + }); + + await new Promise((resolve) => { + processHandle.stdout.on("data", function (data: any) { + const rawString = data.toString(); + const lineArray = rawString.split(/[\n\r?]/); + + if (messageFragment.length > 0) { + lineArray[0] = messageFragment + lineArray[0]; + messageFragment = ""; + } + + if (!rawString.endsWith("\n") && !rawString.endsWith("\r")) { + messageFragment = lineArray.pop(); + } + + for (const line of lineArray) { + if (line.length > 0) { + vectorMessage(line.replace(/\n/g, "")); + } + } + }); + + processHandle.on("exit", async function (code: any) { + clearInterval(progressInterval); + progress.report({ increment: 100 }); + vectorMessage("-".repeat(100)); + vectorMessage( + `${path.basename(command)}: '${argList.join(" ")}' returned exit code: ${code.toString()}` + ); + vectorMessage("-".repeat(100)); + resolve(); + }); + + processHandle.on("error", (error) => { + clearInterval(progressInterval); + vectorMessage(`Error occurred: ${error.message}`); + resolve(); + }); + }); + } + ); +} diff --git a/src/vcastTestInterface.ts b/src/vcastTestInterface.ts index bf107ad5..3dda4a45 100644 --- a/src/vcastTestInterface.ts +++ b/src/vcastTestInterface.ts @@ -71,6 +71,11 @@ import { closeConnection, globalEnviroDataServerActive, } from "../src-common/vcastServer"; +import { + getEnabledEnvirosForFile, + hideCoverageFilterStatusBar, + updateCoverageFilterStatusBar, +} from "./coverage"; const fs = require("fs"); const path = require("path"); @@ -189,6 +194,7 @@ export function clearTestDataFromStatusArray(): void { // List of source file from all local environments interface coverageDataType { crc32Checksum: number; + isVCP: boolean; covered: number[]; uncovered: number[]; partiallyCovered: number[]; @@ -200,7 +206,7 @@ interface fileCoverageType { } // key is filePath -let globalCoverageData = new Map(); +export let globalCoverageData = new Map(); ///////////////////////////////////////////////////////////////////// export function resetCoverageData() { @@ -244,11 +250,23 @@ export function getCoverageDataForFile(filePath: string): coverageSummaryType { // if there is coverage data, create the x/y status bar message if (dataForThisFile.hasCoverage && dataForThisFile.enviroList.size > 0) { const checksum: number = getChecksum(filePath); + // undefined = all enviros enabled (no filter entry exists for this file) + const enabledEnviros = getEnabledEnvirosForFile(filePath); + let coveredList: number[] = []; let uncoveredList: number[] = []; let partiallyCoveredList: number[] = []; - for (const enviroData of dataForThisFile.enviroList.values()) { - if (enviroData.crc32Checksum == checksum) { + + for (const [ + enviroPath, + enviroData, + ] of dataForThisFile.enviroList.entries()) { + // Skip enviros the user has deselected in the coverage filter webview + if (enabledEnviros !== undefined && !enabledEnviros.has(enviroPath)) { + continue; + } + + if (enviroData.crc32Checksum == checksum || enviroData.isVCP) { coveredList = coveredList.concat(enviroData.covered); uncoveredList = uncoveredList.concat(enviroData.uncovered); partiallyCoveredList = partiallyCoveredList.concat( @@ -257,21 +275,33 @@ export function getCoverageDataForFile(filePath: string): coverageSummaryType { } } - if (coveredList.length == 0 && uncoveredList.length == 0) { + if ( + coveredList.length == 0 && + uncoveredList.length == 0 && + partiallyCoveredList.length == 0 + ) { // This status is for files that have changed since // they were last instrumented returnData.statusString = "Coverage Out of Date"; + hideCoverageFilterStatusBar(); } else { returnData.hasCoverageData = true; // remove duplicates returnData.covered = [...new Set(coveredList)]; returnData.uncovered = [...new Set(uncoveredList)]; returnData.partiallyCovered = [...new Set(partiallyCoveredList)]; + + // update status bar filter indicator + updateCoverageFilterStatusBar( + dataForThisFile.enviroList.size, + enabledEnviros + ); } } else { // This status is for files that are part of // and environment but not instrumented returnData.statusString = "No Coverage Data"; + hideCoverageFilterStatusBar(); } } @@ -338,7 +368,9 @@ export function updateGlobalDataForFile(enviroPath: string, fileList: any[]) { .map(Number); const checksum = fileList[fileIndex].cmcChecksum; + let coverageData: coverageDataType = { + isVCP: enviroPath.endsWith(".vcp"), crc32Checksum: checksum, covered: coveredList, uncovered: uncoveredList, @@ -753,7 +785,6 @@ async function configureWorkspaceAndBuildEnviro( if (projectEnvParameters) { // Create the environment using the provided file list await commonEnvironmentSetup(fileList, envLocation, false); - const envName = createEnvNameFromFiles(fileList); const envFilePath = path.join(envLocation, `${envName}.env`); const testSuites = projectEnvParameters.testsuiteArgs; @@ -882,7 +913,6 @@ async function commonEnvironmentSetup( return; } } - // Build the environment with the valid name await buildEnvironmentVCAST( fileList, @@ -904,7 +934,6 @@ export async function newEnvironment( // file in the list will be a C/C++ file but we need to filter // for the multi-select case. // - let fileList: string[] = []; for (let index = 0; index < URIlist.length; index++) { const filePath = URIlist[index].fsPath; diff --git a/src/vcastUtilities.ts b/src/vcastUtilities.ts index c4e8c7bc..ee68c840 100644 --- a/src/vcastUtilities.ts +++ b/src/vcastUtilities.ts @@ -446,11 +446,10 @@ export function getVcastInterfaceCommandForMCDC( let optionsDict: { [command: string]: string | number } = {}; optionsDict["unitName"] = unitName; optionsDict["lineNumber"] = lineNumber; - const jsonOptions: string = JSON.stringify(optionsDict).replaceAll( - '"', - '\\"' - ); - const testArgument = `--options="${jsonOptions}"`; + + const jsonOptions: string = JSON.stringify(optionsDict); + const testArgument = `--options='${jsonOptions}'`; + return `${commandToRun} ${testArgument}`; } @@ -593,19 +592,27 @@ export function checkIfAnyProjectsAreOpened() { * @param fullPath Full Path to the Project File */ export function getVcmRoot(fullPath: string) { + // Remove vcast: scheme if present + const cleanPath = fullPath.replace(/^vcast:/, ""); + // pre-compile the regex once const vcmRe = /(.*\/)([^/]+\.vcm)(?:\/.*)?$/; + const vcpRe = /(.*\/)([^/]+\.vcp)(?:\/.*)?$/; // use exec() instead of match() (sonarcloud) - const match = vcmRe.exec(fullPath); + let match: RegExpExecArray | null; + if (cleanPath.endsWith(".vcm")) { + match = vcmRe.exec(cleanPath); + } else { + match = vcpRe.exec(cleanPath); + } if (match) { - // match[1] is the directory (with trailing slash), match[2] is the .vcm name + // match[1] is the directory (with trailing slash), match[2] is the .vcm/.vcp name const rootPath = match[1].replace(/\/$/, ""); const vcmName = match[2]; return { rootPath, vcmName }; } - return null; } /** diff --git a/tests/internal/e2e/test/cCoverage/c_cov_example.sh b/tests/internal/e2e/test/cCoverage/c_cov_example.sh new file mode 100644 index 00000000..b51cd3d0 --- /dev/null +++ b/tests/internal/e2e/test/cCoverage/c_cov_example.sh @@ -0,0 +1,55 @@ +#!/bin/sh -ex + +# Always start with a clean working directory +rm -fr work +mkdir work +cd work +export WORK_DIRECTORY=$PWD +# export VECTORCAST_DIR=/home/JOBDATA/VectorCAST/vc20__86806_vcwrap__87490_inst_mod/deliver/linux64/debug + +# Download lua 5.3.5 for instrumentation +time wget https://www.lua.org/ftp/lua-5.4.0.tar.gz +time tar -zxvf lua-5.4.0.tar.gz + + +# Configuration VectorCAST, including the compiler +$VECTORCAST_DIR/clicast template GNU_CPP11_X +$VECTORCAST_DIR/clicast option VCAST_COVERAGE_FOR_HEADERS TRUE +$VECTORCAST_DIR/clicast option VCAST_COVERAGE_FOR_AGGREGATE_INIT TRUE + + +# Create the cover environment +time $VECTORCAST_DIR/clicast cover environment script_run ../env.enc + + +$VECTORCAST_DIR/clicast -e env cover env disable_instrumentation + + +# These commands will be put into a single clicast build command + + # Single step instrument and build + cd lua-5.4.0/src + + # The command that instruments and builds + time make generic -j16 -B + + # Add the instrumentation data into the cover environment + cd $WORK_DIRECTORY + +# Run the tests +mkdir lua_tests +cd lua_tests +time wget https://www.lua.org/tests/lua-5.4.0-tests.tar.gz +time tar -zxvf lua-5.4.0-tests.tar.gz +cd lua-5.4.0-tests +set +e +../../lua-5.4.0/src/lua all.lua +mv TESTINSS.DAT TESTINSS.DAT.all.lua +../../lua-5.4.0/src/lua api.lua +mv TESTINSS.DAT TESTINSS.DAT.api.lua +set -e + +# Add the test suite result to the cover environment +cd $WORK_DIRECTORY +time $VECTORCAST_DIR/clicast -e env cover result add lua_tests/lua-5.4.0-tests/TESTINSS.DAT.all.lua all.lua +time $VECTORCAST_DIR/clicast -e env cover result add lua_tests/lua-5.4.0-tests/TESTINSS.DAT.api.lua api.lua \ No newline at end of file diff --git a/tests/internal/e2e/test/cCoverage/env.enc b/tests/internal/e2e/test/cCoverage/env.enc new file mode 100644 index 00000000..818093db --- /dev/null +++ b/tests/internal/e2e/test/cCoverage/env.enc @@ -0,0 +1,41 @@ + + + 2 + + env + STATEMENT_MCDC + + + SourceRoot + ./lua-5.4.0 + src + + + PROBE_ID: 2 + PROBE_UNIT: luac.c + PROBE_FUNCTION: main + PROBE_LINE: return 0; + PROBE_CONTEXT_BEFORE: + if (lua_pcallk(L, (2), (0), (0), 0, ((void *)0))!=0) fatal(lua_tolstring(L, (-1), ((void *)0))); + lua_close(L); + END_PROBE_CONTEXT_BEFORE: + PROBE_CODE: + VCAST_DUMP_COVERAGE_DATA(); + END_PROBE_CODE: + + + + SourceRoot + src/lua.c + + true + + + + SourceRoot + src/luac.c + + true + + + diff --git a/tests/internal/e2e/test/specs/vcast_c_tests.test.ts b/tests/internal/e2e/test/specs/vcast_c_tests.test.ts new file mode 100644 index 00000000..ac901074 --- /dev/null +++ b/tests/internal/e2e/test/specs/vcast_c_tests.test.ts @@ -0,0 +1,223 @@ +// Test/specs/vcast.test.ts +import { + TextEditor, + type BottomBarPanel, + type Workbench, +} from "wdio-vscode-service"; +import { Key } from "webdriverio"; +import { + expandWorkspaceFolderSectionInExplorer, + updateTestID, + checkIfRequestInLogs, + checkElementExistsInHTML, + findTreeNodeAtLevel, + TIMEOUT, +} from "../test_utils/vcast_utils"; +import { checkForServerRunnability } from "../../../../unit/getToolversion"; + +describe("vTypeCheck VS Code Extension", () => { + let bottomBar: BottomBarPanel; + let workbench: Workbench; + let useDataServer: boolean = true; + before(async () => { + workbench = await browser.getWorkbench(); + // Opening bottom bar and problems view before running any tests + bottomBar = workbench.getBottomBar(); + await bottomBar.toggle(true); + process.env.E2E_TEST_ID = "0"; + let releaseIsSuitableForServer = await checkForServerRunnability(); + if (process.env.VCAST_USE_PYTHON || !releaseIsSuitableForServer) { + useDataServer = false; + } + }); + + it("test 1: should be able to load VS Code", async () => { + await updateTestID(); + expect(await workbench.getTitleBar().getTitle()).toBe( + "[Extension Development Host] vcastTutorial - Visual Studio Code" + ); + }); + + it("should activate vcastAdapter", async () => { + await updateTestID(); + + await browser.keys([Key.Control, Key.Shift, "p"]); + // Typing Vector in the quick input box + // This brings up VectorCAST Test Explorer: Configure + // so just need to hit Enter to activate + for (const character of "vector") { + await browser.keys(character); + } + + await browser.keys(Key.Enter); + + const activityBar = workbench.getActivityBar(); + const viewControls = await activityBar.getViewControls(); + for (const viewControl of viewControls) { + console.log(await viewControl.getTitle()); + } + + await bottomBar.toggle(true); + const outputView = await bottomBar.openOutputView(); + + console.log("Waiting for VectorCAST activation"); + await $("aria/VectorCAST Test Pane Initialization"); + console.log("WAITING FOR TESTING"); + await browser.waitUntil( + async () => (await activityBar.getViewControl("Testing")) !== undefined, + { timeout: TIMEOUT } + ); + console.log("WAITING FOR TEST EXPLORER"); + browser.pause(10000); + await browser.waitUntil(async () => + (await outputView.getChannelNames()) + .toString() + .includes("VectorCAST Test Explorer") + ); + await outputView.selectChannel("VectorCAST Test Explorer"); + console.log("Channel selected"); + console.log("WAITING FOR LANGUAGE SERVER"); + await browser.waitUntil( + async () => + (await outputView.getText()) + .toString() + .includes("Starting the language server"), + { timeout: TIMEOUT } + ); + + const testingView = await activityBar.getViewControl("Testing"); + await testingView?.openView(); + }); + + it("should check for server starting logs if in server mode", async () => { + const outputView = await bottomBar.openOutputView(); + + // Check if server started + if (useDataServer) { + // Check message pane for expected message + await browser.waitUntil( + async () => + (await outputView.getText()) + .toString() + .includes("Started VectorCAST Data Server"), + { timeout: TIMEOUT } + ); + + // Check server logs + const logs = await checkIfRequestInLogs(3, ["port:", "clicast"]); + expect(logs).toBe(true); + } + }); + + it("should check for vcp node", async () => { + const activityBar = workbench.getActivityBar(); + const testingView = await activityBar.getViewControl("Testing"); + await testingView?.openView(); + const vcpNode = await findTreeNodeAtLevel(0, "env.vcp"); + expect(vcpNode).toBeDefined(); + }); + + it("should check for c coverage", async () => { + workbench = await browser.getWorkbench(); + bottomBar = workbench.getBottomBar(); + const activityBar = workbench.getActivityBar(); + const testingView = await activityBar.getViewControl("Explorer"); + await testingView?.openView(); + + await bottomBar.toggle(false); + + const workspaceFolderSection = + await expandWorkspaceFolderSectionInExplorer("vcastTutorial"); + + const workFolder = workspaceFolderSection.findItem("work"); + await (await workFolder).select(); + + const luaFolder = workspaceFolderSection.findItem("lua-5.4.0"); + await (await luaFolder).select(); + + const srcFolder = workspaceFolderSection.findItem("src"); + await (await srcFolder).select(); + + const file = workspaceFolderSection.findItem("lua.c"); + await (await file).select(); + + // Check if the file is already open in the editor + const editorView = workbench.getEditorView(); + const openEditors = await editorView.getOpenEditorTitles(); + const isFileOpen = openEditors.includes("lua.c"); + + if (!isFileOpen) { + await (await file).select(); + } + + // Give VS Code some time to settle (language server, decorations, etc.) + await browser.pause(30_000); + + const icon = "no-cover-icon-with-mcdc"; + const lineNumber = 65; + + const tab = (await editorView.openEditor("lua.c")) as TextEditor; + await tab.moveCursor(lineNumber, 1); + + // Use the EXACT same pattern as the working test + const lineNumberElement = await $(`.line-numbers=${lineNumber}`); + const flaskElement = await ( + await lineNumberElement.parentElement() + ).$(".cgmr.codicon"); + + // Verify the icon + const backgroundImageCSS = + await flaskElement.getCSSProperty("background-image"); + expect(backgroundImageCSS.value.includes(`/${icon}`)).toBe(true); + + // Close bottom bar before context menu + await bottomBar.toggle(false); + await browser.pause(1000); + + // Open context menu + await flaskElement.click({ button: 2 }); + await browser.pause(2000); + await (await $("aria/VectorCAST MC/DC Report")).click(); + + const outputView = await bottomBar.openOutputView(); + + // Wait for report generation + await browser.waitUntil( + async () => + (await outputView.getText()) + .toString() + .includes("Report file path is:"), + { timeout: TIMEOUT } + ); + + // Wait for webview to open + await browser.waitUntil( + async () => (await workbench.getAllWebviews()).length > 0, + { timeout: TIMEOUT } + ); + + const webviews = await workbench.getAllWebviews(); + expect(webviews).toHaveLength(1); + + const webview = webviews[0]; + await webview.open(); + + // Count report blocks in the HTML + const reportBlockCount = await browser.execute(() => { + return document.querySelectorAll("div.report-block").length; + }); + + expect(reportBlockCount).toEqual(1); + + // Validate report content + await expect(await checkElementExistsInHTML("lua.c")).toBe(true); + await expect(await checkElementExistsInHTML("65")).toBe(true); + await expect( + await checkElementExistsInHTML("Pairs satisfied: 0 of 2 ( 0% )") + ).toBe(true); + + // Cleanup + await webview.close(); + await editorView.closeEditor("VectorCAST Report", 1); + }); +}); diff --git a/tests/internal/e2e/test/specs/vcast_manage.test.ts b/tests/internal/e2e/test/specs/vcast_manage.test.ts index 0b462eec..0260a42f 100644 --- a/tests/internal/e2e/test/specs/vcast_manage.test.ts +++ b/tests/internal/e2e/test/specs/vcast_manage.test.ts @@ -814,6 +814,7 @@ describe("vTypeCheck VS Code Extension", () => { const button = await $(`aria/Import OK`); await button.click(); console.log("Checking for Output logs if Environment creation is finished"); + await bottomBar.maximize(); await browser.waitUntil( async () => (await outputView.getText()) @@ -846,6 +847,8 @@ describe("vTypeCheck VS Code Extension", () => { const testsuiteNode = await findTreeNodeAtLevel(3, "DATABASE-MANAGER"); expect(testsuiteNode).toBeDefined(); + await bottomBar.restore(); + // Closing all current notifications for the next test const notificationsCenter = await workbench.openNotificationsCenter(); await notificationsCenter.clearAllNotifications(); diff --git a/tests/internal/e2e/test/specs_config.ts b/tests/internal/e2e/test/specs_config.ts index 251b82e4..f5f95449 100755 --- a/tests/internal/e2e/test/specs_config.ts +++ b/tests/internal/e2e/test/specs_config.ts @@ -170,6 +170,14 @@ export function getSpecGroups(useVcast24: boolean) { }, params: {}, }; + specGroups["c_coverage"] = { + specs: ["./**/**/vcast_c_tests.test.ts"], + env: { + VCAST_USE_PYTHON: "True", + C_COVERAGE: "True", + }, + params: {}, + }; specGroups["basic_user_interactions_server"] = { specs: [ diff --git a/tests/internal/e2e/test/wdio.conf.ts b/tests/internal/e2e/test/wdio.conf.ts index 18af6977..8799dc3e 100644 --- a/tests/internal/e2e/test/wdio.conf.ts +++ b/tests/internal/e2e/test/wdio.conf.ts @@ -335,6 +335,7 @@ export const config: Options.Testrunner = { async () => await buildEnvsWithSpecificReleases(initialWorkdir), ], ["MANAGE_TEST", async () => await testManage(initialWorkdir)], + ["C_COVERAGE", async () => await testCCoverage(initialWorkdir)], ]); // Determine the environment key @@ -392,6 +393,40 @@ export const config: Options.Testrunner = { * ================================================================================================ */ + async function testCCoverage(initialWorkdir: string) { + const workFolder = path.join(initialWorkdir, "test", "vcastTutorial"); + const testInputCCoverage = path.join(initialWorkdir, "test", "cCoverage"); + + await checkVPython(); + clicastExecutablePath = await checkClicast(); + process.env.CLICAST_PATH = clicastExecutablePath; + + await prepareConfig(initialWorkdir, clicastExecutablePath); + + // Create vcastTutorial directory + await mkdir(workFolder, { recursive: true }); + + // Copy the build script to vcastTutorial + const scriptSource = path.join(testInputCCoverage, "c_cov_example.sh"); + const scriptDest = path.join(workFolder, "c_cov_example.sh"); + const envSource = path.join(testInputCCoverage, "env.enc"); + const envDest = path.join(workFolder, "env.enc"); + + if (process.platform === "win32") { + await executeCommand(`copy /y ${scriptSource} ${scriptDest}`); + await executeCommand(`copy /y ${envSource} ${envDest}`); + } else { + await executeCommand(`cp ${scriptSource} ${scriptDest}`); + await executeCommand(`cp ${envSource} ${envDest}`); + } + + // Execute the coverage build script in vcastTutorial + const build_coverage = `cd ${workFolder} && bash ./c_cov_example.sh`; + await executeCommand(build_coverage); + + // No copy needed - everything is put in place by the sh script + } + async function testManage(initialWorkdir: string) { const testInputManage = path.join(initialWorkdir, "test", "manage"); const build_demo = `cd ${testInputManage} && ./demo_build.sh`;