blob: eedf2dcf71276ee80e80cc11297e6e8f33e8f0f3 [file]
# @file MarkdownLintCheck.py
#
# An edk2-pytool based plugin wrapper for markdownlint
#
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: BSD-2-Clause-Patent
##
import logging
from io import StringIO
import os
from typing import List
from edk2toolext.environment.plugintypes.ci_build_plugin import ICiBuildPlugin
from edk2toollib.utility_functions import RunCmd
from edk2toolext.environment.var_dict import VarDict
from edk2toolext.environment import version_aggregator
class MarkdownLintCheck(ICiBuildPlugin):
"""
A CiBuildPlugin that uses the markdownlint-cli node module to scan the files
from the package being tested for linter errors.
The linter config file (.markdownlint.yaml) must be present in one of the defined
locations otherwise the test will be skipped. These locations were picked to also
align with how editors and tools will find the config file.
1st priority location - At the package root
2nd Priority location - At the workspace root of the build (suggested location unless override needed)
Configuration options:
"MarkdownLintCheck": {
"AuditOnly": False, # If True, log all errors and then mark as skipped
"IgnoreFiles": [] # package root relative file, folder, or glob pattern to ignore
}
"""
CONFIG_FILE_NAME = ".markdownlint.yaml"
def GetTestName(self, packagename: str, environment: VarDict) -> tuple:
""" Provide the testcase name and classname for use in reporting
Args:
packagename: string containing name of package to build
environment: The VarDict for the test to run in
Returns:
a tuple containing the testcase name and the classname
(testcasename, classname)
testclassname: a descriptive string for the testcase can include whitespace
classname: should be patterned <packagename>.<plugin>.<optionally any unique condition>
"""
return ("Lint Markdown files in " + packagename, packagename + ".markdownlint")
##
# External function of plugin. This function is used to perform the task of the CiBuild Plugin
#
# - package is the edk2 path to package. This means workspace/packagepath relative.
# - edk2path object configured with workspace and packages path
# - PkgConfig Object (dict) for the pkg
# - EnvConfig Object
# - Plugin Manager Instance
# - Plugin Helper Obj Instance
# - Junit Logger
# - output_stream the StringIO output stream from this plugin via logging
def RunBuildPlugin(self, packagename, Edk2pathObj, pkgconfig, environment, PLM, PLMHelper, tc, output_stream=None):
abs_pkg_path = Edk2pathObj.GetAbsolutePathOnThisSystemFromEdk2RelativePath(
packagename)
if abs_pkg_path is None:
tc.SetSkipped()
tc.LogStdError("No package {0}".format(packagename))
return -1
# check for node
return_buffer = StringIO()
ret = RunCmd("node", "--version", outstream=return_buffer)
if ret != 0:
tc.SetSkipped()
tc.LogStdError("NodeJs not installed. Test can't run")
logging.warning("NodeJs not installed. Test can't run")
return -1
node_version = return_buffer.getvalue().strip() # format vXX.XX.XX
tc.LogStdOut(f"Node version: {node_version}")
# Check for markdownlint-cli
return_buffer = StringIO()
ret = RunCmd("markdownlint", "--version", outstream=return_buffer)
if ret != 0:
tc.SetSkipped()
tc.LogStdError("markdownlint not installed. Test can't run")
logging.warning("markdownlint not installed. Test can't run")
return -1
mdl_version = return_buffer.getvalue().strip() # format XX.XX.XX
tc.LogStdOut(f"MarkdownLint version: {mdl_version}")
version_aggregator.GetVersionAggregator().ReportVersion(
"MarkDownLint", mdl_version, version_aggregator.VersionTypes.INFO)
# Get relative path for the root of package to use with ignore and path parameters
relpath = os.path.relpath(abs_pkg_path)
# Newer versions of markdownlint don't understand backslashes
relpath = relpath.replace(os.path.sep, '/')
#
# check for any package specific ignore patterns defined by package config
#
Ignores = []
if "IgnoreFiles" in pkgconfig:
for i in pkgconfig["IgnoreFiles"]:
Ignores.append(f"{relpath}/{i}")
#
# Make the path string to check
#
path_to_check = f'{relpath}/**/*.md'
# get path to config file -
# Currently there is support for two different config files
# If the config file is not found then the test case is skipped
#
# 1st - At the package root
# 2nd - At the workspace root of the build
config_file_path = None
# 1st check to see if the config file is at package root
if os.path.isfile(os.path.join(abs_pkg_path, MarkdownLintCheck.CONFIG_FILE_NAME)):
config_file_path = os.path.join(abs_pkg_path, MarkdownLintCheck.CONFIG_FILE_NAME)
# 2nd check to see if at workspace root
elif os.path.isfile(os.path.join(Edk2pathObj.WorkspacePath, MarkdownLintCheck.CONFIG_FILE_NAME)):
config_file_path = os.path.join(Edk2pathObj.WorkspacePath, MarkdownLintCheck.CONFIG_FILE_NAME)
# If not found - skip test
else:
tc.SetSkipped()
tc.LogStdError(f"{MarkdownLintCheck.CONFIG_FILE_NAME} not found. Skipping test")
logging.warning(f"{MarkdownLintCheck.CONFIG_FILE_NAME} not found. Skipping test")
return -1
# Run the linter
results = self._check_markdown(path_to_check, config_file_path, Ignores)
for r in results:
tc.LogStdError(r.strip())
# add result to test case
overall_status = len(results)
if overall_status != 0:
if "AuditOnly" in pkgconfig and pkgconfig["AuditOnly"]:
# set as skipped if AuditOnly
tc.SetSkipped()
return -1
else:
tc.SetFailed("Markdown Lint Check {0} Failed. Errors {1}".format(
packagename, overall_status), "CHECK_FAILED")
else:
tc.SetSuccess()
return overall_status
def _check_markdown(self, rel_file_to_check: os.PathLike, abs_config_file_to_use: os.PathLike, Ignores: List[str]) -> List[str]:
""" Run markdownlint against the given path and return any errors found.
Args:
rel_file_to_check: package root relative glob pattern of markdown files to check
abs_config_file_to_use: absolute path to the .markdownlint.yaml config file to use
Ignores: list of package root relative file, folder, or glob patterns to ignore
Returns:
a list of error strings reported by markdownlint. Empty if no errors found.
"""
output = StringIO()
param = f"--config {abs_config_file_to_use}"
for a in Ignores:
param += f' --ignore "{a}"'
param += f' "{rel_file_to_check}"'
ret = RunCmd("markdownlint", param, outstream=output)
if ret == 0:
return []
else:
return output.getvalue().strip().splitlines()