File: load.py

package info (click to toggle)
firefox 147.0.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 4,683,320 kB
  • sloc: cpp: 7,607,359; javascript: 6,533,295; ansic: 3,775,223; python: 1,415,500; xml: 634,561; asm: 438,949; java: 186,241; sh: 62,752; makefile: 18,079; objc: 13,092; perl: 12,808; yacc: 4,583; cs: 3,846; pascal: 3,448; lex: 1,720; ruby: 1,003; php: 436; lisp: 258; awk: 247; sql: 66; sed: 54; csh: 10; exp: 6
file content (150 lines) | stat: -rw-r--r-- 4,951 bytes parent folder | download | duplicates (2)
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
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.

import sys
from os import path

import yaml

__all__ = ["annotations_filename", "read_annotations"]

annotations_filename = path.normpath(
    path.join(path.dirname(__file__), "..", "CrashAnnotations.yaml")
)


def sort_annotations(annotations):
    """Return annotations in ascending alphabetical order ignoring case"""

    return sorted(annotations.items(), key=lambda annotation: str.lower(annotation[0]))


# Convert CamelCase to snake_case. Also supports CAPCamelCase.
def camel_to_snake(s):
    if s.islower():
        return s
    lowers = [c.islower() for c in s] + [False]
    words = []
    last = 0
    for i in range(1, len(s)):
        if not lowers[i] and (lowers[i - 1] or lowers[i + 1]):
            words.append(s[last:i])
            last = i
    words.append(s[last:])
    return "_".join(words).lower()


class AnnotationValidator:
    def __init__(self, name):
        self._name = name
        self.passed = True

    def validate(self, data):
        """
        Ensure that the annotation has all the required fields, and elaborate
        default values.
        """
        if "description" not in data:
            self._error("does not have a description")
        annotation_type = data.get("type")
        if annotation_type is None:
            self._error("does not have a type")

        valid_types = ["string", "boolean", "u32", "u64", "usize", "object"]
        if annotation_type and annotation_type not in valid_types:
            self._error(f"has an unknown type: {annotation_type}")
            annotation_type = None

        annotation_scope = data.setdefault("scope", "client")
        valid_scopes = ["client", "report", "ping", "ping-only"]
        if annotation_scope not in valid_scopes:
            self._error(f"has an unknown scope: {annotation_scope}")
            annotation_scope = None

        is_ping = annotation_scope and annotation_scope in ["ping", "ping-only"]

        if annotation_scope and "glean" in data and not is_ping:
            self._error("has a glean metric specification but does not have ping scope")

        if annotation_type and is_ping:
            self._glean(annotation_type, data.setdefault("glean", {}))

    def _error(self, message):
        print(
            f"{annotations_filename}: Annotation {self._name} {message}.",
            file=sys.stderr,
        )
        self.passed = False

    def _glean(self, annotation_type, glean):
        if not isinstance(glean, dict):
            self._error("has invalid glean metric specification (expected a map)")
            return

        glean_metric_name = glean.setdefault("metric", "crash")
        # If only a category is given, derive the metric name from the annotation name.
        if "." not in glean_metric_name:
            glean_metric_name = glean["metric"] = (
                f"{glean_metric_name}.{camel_to_snake(self._name)}"
            )

        glean_default_type = (
            annotation_type if annotation_type in ["string", "boolean"] else None
        )
        glean_type = glean.setdefault("type", glean_default_type)
        if glean_type is None:
            self._error("must have a glean metric type specified")

        glean_types = [
            "boolean",
            "datetime",
            "timespan",
            "string",
            "string_list",
            "quantity",
            "object",
        ]
        if glean_type and glean_type not in glean_types:
            self._error(f"has an invalid glean metric type ({glean_type})")
            glean_type = None

        metric_required_fields = {
            "datetime": ["time_unit"],
            "timespan": ["time_unit"],
            "quantity": ["unit"],
            "string_list": ["delimiter"],
            "object": ["structure"],
        }

        required_fields = metric_required_fields.get(glean_type, [])
        for field in required_fields:
            if field not in glean:
                self._error(f"requires a `{field}` field for glean {glean_type} metric")


def read_annotations():
    """Read the annotations from the YAML file.
    If an error is encountered quit the program."""

    try:
        with open(annotations_filename) as annotations_file:
            annotations = sort_annotations(yaml.safe_load(annotations_file))
    except (OSError, ValueError) as e:
        sys.exit("Error parsing " + annotations_filename + ":\n" + str(e) + "\n")

    valid = True
    for name, data in annotations:
        validator = AnnotationValidator(name)
        validator.validate(data)
        valid &= validator.passed

    if not valid:
        sys.exit(1)

    return annotations


def main(output):
    yaml.safe_dump(read_annotations(), stream=output)
    return {annotations_filename}