Skip to content

Commit

Permalink
Add a simple test for component consistency
Browse files Browse the repository at this point in the history
Also introduces a `ssg.components` module to the project which
can be used later in other tools and build system to work with
component files data.

This commit adds a simple test case `test_components.py` which
tests that
- components don't map to rules that don't exist
- all rules are mapped to at least 1 component
- the rule is assigned to component according to templates or
  template parameters
  • Loading branch information
jan-cerny committed May 25, 2023
1 parent 041f82e commit 8590c57
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 0 deletions.
48 changes: 48 additions & 0 deletions ssg/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import print_function

from collections import defaultdict
import os

import ssg.yaml


def load(components_dir):
components = {}
for component_filename in os.listdir(components_dir):
components_filepath = os.path.join(components_dir, component_filename)
component = Component(components_filepath)
components[component.name] = component
return components


def rule_components_mapping(components):
rules_to_components = defaultdict(list)
for component in components.values():
for rule_id in component.rules:
rules_to_components[rule_id].append(component)
return rules_to_components


def package_component_mapping(components):
packages_to_components = {}
for component in components.values():
for package in component.packages:
packages_to_components[package] = component.name
return packages_to_components


def template_component_mapping(components):
template_to_component = {}
for component in components.values():
for template in component.templates:
template_to_component[template] = component.name
return template_to_component


class Component:
def __init__(self, filepath):
yaml_data = ssg.yaml.open_raw(filepath)
self.name = yaml_data["name"]
self.rules = yaml_data["rules"]
self.packages = yaml_data["packages"]
self.templates = yaml_data.get("templates", [])
11 changes: 11 additions & 0 deletions ssg/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import os
from glob import glob

import ssg.build_yaml


def get_rule_dir_yaml(dir_path):
"""
Expand Down Expand Up @@ -165,3 +167,12 @@ def find_rule_dirs_in_paths(base_dirs):
for cur_dir in base_dirs:
for d in find_rule_dirs(cur_dir):
yield d


def find_all_rules(base_dir):
"""
Generator which yields all rule IDs within a given base_dir, recursively
"""
for rule_dir in find_rule_dirs(base_dir):
rule_id = get_rule_dir_id(rule_dir)
yield rule_id
5 changes: 5 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -352,3 +352,8 @@ if (PY_CMAKELINT)
COMMAND env "PYTHONPATH=$ENV{PYTHONPATH}" "cmakelint" --config "${CMAKE_SOURCE_DIR}/.cmakelintrc" ${CMAKELINT_FILES}
)
endif()

add_test(
NAME "components"
COMMAND env "PYTHONPATH=$ENV{PYTHONPATH}" "${PYTHON_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/test_components.py" --build-config-yaml "${CMAKE_BINARY_DIR}/build_config.yml" --product-yaml "${CMAKE_SOURCE_DIR}/products/rhel9/product.yml" --source-dir "${CMAKE_SOURCE_DIR}"
)
136 changes: 136 additions & 0 deletions tests/test_components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import argparse
import os

import ssg.build_yaml
import ssg.components
import ssg.environment
import ssg.rules
import ssg.yaml


def parse_args():
parser = argparse.ArgumentParser(
description="Test components data consistency")
parser.add_argument(
"--build-config-yaml", help="Path to the build config YAML file")
parser.add_argument("--product-yaml", help="Path to the product YAML file")
parser.add_argument("--source-dir", help="Path to the root directory")
return parser.parse_args()


def get_components_by_template(rule, package_to_components, template_to_component):
template = rule.template
if not template:
return []
template_name = template["name"]
template_vars = template["vars"]
components = []
if template_name in template_to_component:
component = template_to_component[template_name]
reason = (
"all rules using template '%s' should be assigned to component "
"'%s'" % (template_name, component))
components.append((component, reason))
elif template_name in ["package_installed", "package_removed"]:
package = template_vars["pkgname"]
component = package_to_components.get(package, package)
reason = (
"rule uses template '%s' with 'pkgname' parameter set to '%s' "
"which is a package that already belongs to component '%s'" %
(template_name, package, component))
components.append((component, reason))
elif template_name in ["service_enabled", "service_disabled"]:
if "packagename" in template_vars:
package = template_vars["packagename"]
else:
package = template_vars["servicename"]
component = package_to_components.get(package, package)
reason = (
"rule uses template '%s' checking service '%s' provided by "
"package '%s' which is a package that already belongs to "
"component '%s'" % (
template_name, template_vars["servicename"],
package, component))
components.append((component, reason))
return components


def test_nonexistent_rules(rules_in_benchmark, rules_with_component):
nonexistent_rules = rules_with_component - rules_in_benchmark
if nonexistent_rules:
print("The following rules aren't part of the benchmark:")
for rule_id in nonexistent_rules:
print("- %s" % (rule_id))
return False
return True


def test_unmapped_rules(rules_in_benchmark, rules_with_component):
unmapped_rules = rules_in_benchmark - rules_with_component
if unmapped_rules:
print("The following rules aren't part of any component:")
for x in unmapped_rules:
print("- " + x)
return False
return True


def iterate_over_all_rules(base_dir, env_yaml):
"""
Generator which yields all rule objects within a given base_dir,
recursively
"""
for rule_dir in ssg.rules.find_rule_dirs(base_dir):
rule_yaml_file_path = ssg.rules.get_rule_dir_yaml(rule_dir)
try:
rule = ssg.build_yaml.Rule.from_yaml(rule_yaml_file_path, env_yaml)
except ssg.yaml.DocumentationNotComplete:
pass
yield rule


def test_templates(
linux_os_guide_dir, env_yaml,
package_to_components, rules_to_components, template_to_component):
result = True
for rule in iterate_over_all_rules(linux_os_guide_dir, env_yaml):
candidates = get_components_by_template(
rule, package_to_components, template_to_component)
rule_components = [c.name for c in rules_to_components[rule.id_]]
for candidate, reason in candidates:
if candidate not in rule_components:
result = False
print(
"Rule '%s' should be assigned to component '%s', "
"because %s." % (rule.id_, candidate, reason))
return result


def main():
result = 0
args = parse_args()
components_dir = os.path.join(args.source_dir, "components")
components = ssg.components.load(components_dir)
rule_to_components = ssg.components.rule_components_mapping(components)
linux_os_guide_dir = os.path.join(args.source_dir, "linux_os", "guide")
rules_with_component = set(rule_to_components.keys())
rules_in_benchmark = set(ssg.rules.find_all_rules(linux_os_guide_dir))
if not test_nonexistent_rules(rules_in_benchmark, rules_with_component):
result = 1
if not test_unmapped_rules(rules_in_benchmark, rules_with_component):
result = 1
env_yaml = ssg.environment.open_environment(
args.build_config_yaml, args.product_yaml)
package_to_component = ssg.components.package_component_mapping(
components)
template_to_component = ssg.components.template_component_mapping(
components)
if not test_templates(
linux_os_guide_dir, env_yaml,
package_to_component, rule_to_components, template_to_component):
result = 1
exit(result)


if __name__ == "__main__":
main()

0 comments on commit 8590c57

Please sign in to comment.