mu_tiano_platforms/Platforms/QemuQ35Pkg/Test/PlatformTest.py

247 строки
11 KiB
Python

# @file
# Script to Build QemuQ35 host-based unit tests.
#
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: BSD-2-Clause-Patent
##
from collections import namedtuple
from edk2toolext.invocables.edk2_platform_build import BuildSettingsManager
from edk2toolext.environment.uefi_build import UefiBuilder
from edk2toollib.utility_functions import RunCmd
from edk2toolext.edk2_logging import SECTION, SUB_SECTION
from edk2toollib.uefi.edk2.parsers.dsc_parser import DscParser
from edk2toollib.database import Edk2DB, Environment, Inf, Source, InstancedInf
from sqlalchemy import func, not_
from pathlib import Path
import logging
import sys
import io
import shutil
import re
sys.path.append(str(Path(__file__).parent.parent))
import PlatformBuild # noqa: E402
PLATFORM_NAME = 'QemuQ35Pkg'
PLATFORM_TEST_DSC = 'QemuQ35Pkg/Test/QemuQ35PkgHostTest.dsc'
PLATFORM_DSC = 'QemuQ35Pkg/QemuQ35Pkg.dsc'
PLATFORMBUILD_DIR = str(Path(__file__).parent.parent)
class TestSettingsManager(PlatformBuild.SettingsManager):
pass
class TestManager(BuildSettingsManager, UefiBuilder):
def AddCommandLineOptions(self, parserObj):
# In an effort to support common server based builds this parameter is added. It is
# checked for correctness but is never uses as this platform only supports a single set of
# architectures.
parserObj.add_argument('-a', "--arch", dest="build_arch", type=str, default="X64",
help="Optional - CSV of architecture to build.")
def RetrieveCommandLineOptions(self, args):
if args.build_arch.upper() != "X64":
raise Exception("Invalid Arch Specified. Please see comments in PlatformBuild.py::PlatformBuilder::AddCommandLineOptions")
def GetLoggingLevel(self, loggerType):
if loggerType == 'con':
return logging.INFO
return logging.DEBUG
def GetWorkspaceRoot(self) -> str:
return str(Path(__file__).parent.parent.parent.parent)
def GetPackagesPath(self):
return PlatformBuild.CommonPlatform.PackagesPath
def GetActiveScopes(self):
return ('qemu', 'qemuq35', 'edk2-build', 'cibuild', 'host-based-test')
def GetName(self):
return f"{PLATFORM_NAME}_HostBasedTest"
def SetPlatformEnv(self):
logging.debug("PlatformBuilder SetPlatformEnv")
self.env.SetValue("ACTIVE_PLATFORM", PLATFORM_TEST_DSC, "Platform Hardcoded.")
self.env.SetValue("TARGET", "NOOPT", "Platform Hardcoded.")
self.env.SetValue("CI_BUILD_TYPE", "host_unit_test", "Platform Hardcoded.")
self.env.SetValue("TARGET_ARCH", "X64", "Platform Hardcoded.")
self.env.SetValue("TOOL_CHAIN_TAG", "VS2022", "Platform Hardcoded.")
# Don't let the host runner reorganize the build. This file will do it by platform.
self.env.SetValue("CC_REORGANIZE", "FALSE", "Platform Hardcoded")
# Must use PlatformFlashImage to generate coverage report as PlatformPostBuild runs before PostBuildPlugins,
# Which generates intitial code coverage.
if self.env.GetValue("CODE_COVERAGE") == "TRUE":
self.FlashImage = True
return 0
def SetPlatformDefaultEnv(self):
Env = namedtuple("Env", ["name", "default", "description"])
return [
Env("CODE_COVERAGE", "FALSE", "Generate Code Coverage Reports"),
Env("REPORTTYPES", "Cobertura", "Code Coverage Report Types"),
Env("CC_FLATTEN", "TRUE", "Group Coverage Results by source file instead of by INF."),
Env("CC_FULL", "FALSE", "Create coverage lines for files without any coverage data.")
]
def PlatformPreBuild(self):
# Make sure code cov tools are installed if they want code coverage reports.
if self.env.GetValue("CODE_COVERAGE") == "TRUE" and not self._verify_code_cov_tools():
return -1
# Parse the platform so we can verify the test dsc is up to date
db_path = Path(self.GetWorkspaceRoot(), "Build", "DATABASE.db")
if not self._parse_platform(db_path, PLATFORMBUILD_DIR):
return -1
if not self._verify_test_dsc(db_path):
return -1
logging.info("Host Based Tests are up to date.")
return 0
def PlatformFlashImage(self):
reporttypes = self.env.GetValue("REPORTTYPES").split(",")
logging.log(SECTION, "Generating Requested Code Coverage Reports")
logging.info(f'Report Types: {",".join(reporttypes)}')
coverage_file = Path(self.env.GetValue("BUILD_OUTPUT_BASE"), "_coverage.xml")
coverage_file = str(coverage_file.replace(coverage_file.parent / f'{PLATFORM_NAME}_coverage.xml'))
if not self._reorganize_coverage_report(coverage_file):
return -1
out_dir = Path(self.env.GetValue("BUILD_OUTPUT_BASE"), "Coverage")
out_dir.mkdir(parents=True, exist_ok=True)
if not self._generate_reports(coverage_file, out_dir, reporttypes):
return -1
return 0
def _verify_test_dsc(self, db_path: Path) -> bool:
"""Compares all source files used by the platform to the source files used by the host based tests."""
logging.log(SECTION, "Verify Host Based Tests are Up to Date")
dscp = DscParser()
dscp.SetEdk2Path(self.edk2path).SetInputVars(self.env.GetAllBuildKeyValues() | self.env.GetAllNonBuildKeyValues())
dscp.ParseFile(self.env.GetValue("ACTIVE_PLATFORM")) # The test DSC
with Edk2DB(db_path).session() as session:
used_tests = set([component for component, _, _ in dscp.Components])
env_id = session.query(Environment).filter(Environment.values.any(key="ACTIVE_PLATFORM", value=PLATFORM_DSC)).order_by(Environment.date.desc()).first().id
host_tests = (
session
.query(Inf.path, Source.path)
.join(Inf.sources)
.filter(Inf.module_type == "HOST_APPLICATION")
.all()
)
source_query = (
session
.query(Source.path)
.join(InstancedInf.sources)
.filter(InstancedInf.env == env_id)
.filter(not_(func.lower(InstancedInf.name).like("%testapp%")))
)
used_source = set([source for source, in source_query.all()])
must_use_tests = set([module for module, source in host_tests if source in used_source])
unused_tests = must_use_tests - used_tests
if len(unused_tests) > 0:
logging.error("The following host based unit tests test files used by the "
"platform, but are not in the host based unit test dsc:")
logging.error("\n ".join(unused_tests))
return False
return True
def _parse_platform(self, db_path: Path, platform_dir: str) -> bool:
"""Parses the platform if necessary."""
if not db_path.exists() or not self._verify_db_data(db_path):
try:
logging.log(SUB_SECTION , "Running stuart_parse")
RunCmd("stuart_parse", '', workingdir = platform_dir, logging_level=logging.DEBUG, raise_exception_on_nonzero=True)
except Exception:
logging.error("Failed to run stuart_parse. Review Build/PARSE_LOG.txt")
return False
else:
logging.warning("Skipping Parse as database contains necessary data.")
return True
def _reorganize_coverage_report(self, cov_file: str) -> bool:
"""Reorganizes a coverage report by platform."""
params = "coverage"
params += f' {cov_file}'
params += f' -ws {self.GetWorkspaceRoot()}'
params += ' --by-platform'
params += f' -d {PLATFORM_DSC}'
params += f' -o {cov_file}'
params += ' --full' * int(self.env.GetValue("CC_FULL") == "TRUE")
params += ' --flatten' * int(self.env.GetValue("CC_FLATTEN") == "TRUE")
try:
RunCmd("stuart_report", params, logging_level=logging.DEBUG, raise_exception_on_nonzero = True)
except Exception:
logging.error("stuart_report Failed to generate a report.")
return False
return True
def _generate_reports(self, cov_file: str, out_dir: str, reporttypes: list) -> bool:
"""Generates one or more coverage report types using reportgenerator."""
if self.env.GetValue("REPORTTYPES") == 'Cobertura':
shutil.copy2(cov_file, out_dir)
else:
params = f'-reports:"{cov_file}"'
params += f' -targetdir:"{str(out_dir)}"'
params += f' -reporttypes:{";".join(reporttypes)}'
try:
RunCmd("reportgenerator", params, logging_level=logging.DEBUG, raise_exception_on_nonzero = True)
except Exception:
logging.error("reportgenerator Failed to generate a report.")
return False
# Clean up the raw coverage file
Path(cov_file).unlink()
return True
def _verify_code_cov_tools(self) -> bool:
"Verifies if the necessary coverage tools are installed."
COV_TOOLS = {
"reportgenerator": ("-h", "Parameters"),
"OpenCppCoverage": ("-h", "Command line only:"),
"lcov": ("--help", "Options:"),
"lcov_cobertura": ("-h", "Options:"),
}
# Register the tools to check
tools = []
if self.env.GetValue("REPORTTYPES") != "Cobertura":
tools.append("reportgenerator")
if sys.platform.startswith("win"):
tools.append("OpenCppCoverage")
else:
tools.extend(["lcov", "lcov_cobertura"])
# Check if the tools are installed
for tool in tools:
params, pattern = COV_TOOLS[tool]
output = io.StringIO()
RunCmd(tool, params, outstream=output, logging_level=logging.DEBUG)
output.seek(0)
match = re.search(pattern, output.getvalue())
if not match:
logging.error(f"You do not have {tool} installed, but current command settings require the tool.")
return False
return True
def _verify_db_data(self, db_path: Path) -> bool:
with Edk2DB(db_path).session() as session:
if len(session.query(Environment).filter(Environment.values.any(key="ACTIVE_PLATFORM", value=PLATFORM_DSC)).all()) == 0:
return False
return True