File: test_product_stability.py

package info (click to toggle)
scap-security-guide 0.1.76-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 110,644 kB
  • sloc: xml: 241,883; sh: 73,777; python: 32,527; makefile: 27
file content (171 lines) | stat: -rwxr-xr-x 6,033 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
#!/usr/bin/python3

from __future__ import print_function

import argparse
import glob
import os.path
import sys

import ssg.products
import ssg.yaml

from tests.common import stability


IGNORED_PROPERTIES = (
    "product_dir",
)


def describe_modification(intro, changeset):
    if not changeset:
        return ""

    msg = intro
    for what, (initial, final) in changeset.items():
        msg += " - {what} from '{initial}' -> '{final}'\n".format(
                what=what, initial=initial, final=final)
    return msg


def describe_change(difference, name):
    msg = ""

    msg += stability.describe_changeset(
        "Following properties were added to the {name} product:\n".format(name=name),
        difference.added,
    )
    msg += stability.describe_changeset(
        "Following properties were removed from the {name} product:\n".format(name=name),
        difference.removed,
    )
    msg += describe_modification(
        "Following properties got different values in the {name} product:\n".format(name=name),
        difference.modified,
    )
    return msg.rstrip()


def compare_dictionaries(reference, sample):
    reference_keys = set(reference.keys())
    sample_keys = set(sample.keys())

    result = stability.Difference()
    result.added = list(sample_keys.difference(reference_keys))
    result.removed = list(reference_keys.difference(sample_keys))
    for key, value in reference.items():
        if sample.get(key, value) != value:
            result.modified[key] = (value, sample[key])
    for item in IGNORED_PROPERTIES:
        result.remove_item_from_comparison(item)
    return result


def get_references_filenames(ref_root):
    return glob.glob(os.path.join(ref_root, "*.yml"))


def corresponding_product_built(build_dir, product_id):
    return os.path.isdir(os.path.join(build_dir, product_id))


def get_matching_compiled_product_filename(build_dir, product_id):
    ref_path_components = reference_fname.split(os.path.sep)
    matching_filename = os.path.join(build_dir, product_id, "product.yml")
    if os.path.isfile(matching_filename):
        return matching_filename


def get_reference_vs_built_difference(ref_product, built_product):
    ref_dict = dict()
    ref_dict.update(ref_product)
    built_dict = dict()
    built_dict.update(built_product)
    difference = compare_dictionaries(ref_dict, built_dict)
    return difference


def inform_and_append_fix_based_on_reference_compiled_product(ref, build_root):
    ref_product = ssg.products.Product(ref)
    product_id = ref_product["product"]
    if not corresponding_product_built(build_root, product_id):
        return True

    compiled_path = os.path.join(build_root, product_id, "product.yml")
    compiled_product = ssg.products.Product(compiled_path)
    difference = get_reference_vs_built_difference(ref_product, compiled_product)
    all_ok = difference.empty
    return all_ok


def compare_compiled_products_with_reference_data(test_data_root, build_root):
    reference_files = get_references_filenames(test_data_root)
    if not reference_files:
        raise RuntimeError("Unable to find any reference compiled products in {test_root}"
                           .format(test_root=test_data_root))
    all_ok = True
    for ref in reference_files:
        all_ok &= inform_and_append_fix_based_on_reference_compiled_product(
                ref, build_root)

    if not all_ok:
        products_dir = os.path.join(os.path.dirname(__file__), "..", "products")
        msg = (
            "If changes to mentioned products are intentional, "
            "execute this script with PYTHONPATH set to the project root "
            "to regenerate reference products: \n"
            "{script_name} --update-reference-data {build_root} {test_data_root}\n"
            "If those changes are unwanted, take a look at product properties "
            "that likely cause these changes."
            .format(script_name=__file__, build_root=os.path.normpath(products_dir),
                    test_data_root=test_data_root))
        print(msg, file=sys.stderr)
    return all_ok


def write_product_without_keys(product, path, exclude_keys=frozenset()):
    product_data = dict()
    product_data.update(product)
    for key in exclude_keys:
        product_data.pop(key, None)
    with open(path, "w") as f:
        ssg.yaml.ordered_dump(product_data, f)


def update_reference_data(test_data_root, products_dir):
    properties_dir = os.path.join(products_dir, os.path.pardir, "product_properties")
    product_yamls = glob.glob(os.path.join(products_dir, "*", "product.yml"))
    for fname in product_yamls:
        product = ssg.products.Product(fname)
        product.read_properties_from_directory(properties_dir)
        out_fname = os.path.join(test_data_root, product["product"] + ".yml")
        write_product_without_keys(product, out_fname, IGNORED_PROPERTIES)
    print("Updated test data of {num_products} products".format(num_products=len(product_yamls)))


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument(
            "build_root", help="A directory that contains product-named subdirectories "
            "with product.yml files. Typically the build root, or products source root "
            "if the script is executed in the update mode")
    parser.add_argument("test_data_root", help="Root of reference data containing "
                        "reference files named <product>.yml")
    parser.add_argument(
        "--update-reference-data", action="store_true", default=False,
        help="If supplied with this option, "
        "and the build root is the product root, update all of the test data")
    args = parser.parse_args()

    if args.update_reference_data:
        update_reference_data(args.test_data_root, args.build_root)
        sys.exit(0)

    all_ok = compare_compiled_products_with_reference_data(args.test_data_root, args.build_root)
    return_code = int(not all_ok)
    sys.exit(return_code)


if __name__ == "__main__":
    main()