squad 1.92__py3-none-any.whl → 1.93.1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- squad/ci/backend/tuxsuite.py +9 -12
- squad/ci/models.py +1 -3
- squad/core/management/commands/run_log_parser.py +44 -0
- squad/core/models.py +15 -4
- squad/core/tasks/__init__.py +2 -0
- squad/plugins/lib/base_log_parser.py +120 -44
- squad/plugins/linux_log_parser.py +5 -4
- squad/plugins/linux_log_parser_build.py +334 -0
- squad/version.py +1 -1
- {squad-1.92.dist-info → squad-1.93.1.dist-info}/METADATA +1 -1
- {squad-1.92.dist-info → squad-1.93.1.dist-info}/RECORD +15 -13
- {squad-1.92.dist-info → squad-1.93.1.dist-info}/WHEEL +1 -1
- {squad-1.92.dist-info → squad-1.93.1.dist-info}/COPYING +0 -0
- {squad-1.92.dist-info → squad-1.93.1.dist-info}/entry_points.txt +0 -0
- {squad-1.92.dist-info → squad-1.93.1.dist-info}/top_level.txt +0 -0
squad/ci/backend/tuxsuite.py
CHANGED
@@ -10,6 +10,7 @@ from requests.adapters import HTTPAdapter, Retry
|
|
10
10
|
from functools import reduce
|
11
11
|
from urllib.parse import urljoin
|
12
12
|
|
13
|
+
from cryptography.exceptions import InvalidSignature
|
13
14
|
from cryptography.hazmat.primitives.asymmetric import ec
|
14
15
|
from cryptography.hazmat.primitives import (
|
15
16
|
hashes,
|
@@ -481,22 +482,18 @@ class Backend(BaseBackend):
|
|
481
482
|
if public_key is None:
|
482
483
|
raise Exception("missing tuxsuite public key for this project")
|
483
484
|
|
484
|
-
payload = json.loads(request.body)
|
485
485
|
signature = base64.urlsafe_b64decode(signature)
|
486
486
|
key = serialization.load_ssh_public_key(public_key.encode("ascii"))
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
487
|
+
try:
|
488
|
+
key.verify(
|
489
|
+
signature,
|
490
|
+
request.body,
|
491
|
+
ec.ECDSA(hashes.SHA256()),
|
492
|
+
)
|
493
|
+
except InvalidSignature:
|
494
|
+
raise Exception("Failed to verify signature against payload")
|
492
495
|
|
493
496
|
def process_callback(self, json_payload, build, environment, backend):
|
494
|
-
# The payload coming from Tuxsuite is formatted as bytes,
|
495
|
-
# so after the first json.loads(request.body), the result
|
496
|
-
# will still be a string containing the actual json document
|
497
|
-
# We need to call json.loads() once more to get the actual
|
498
|
-
# python dict containing all the information we need
|
499
|
-
json_payload = json.loads(json_payload)
|
500
497
|
if "kind" not in json_payload or "status" not in json_payload:
|
501
498
|
raise Exception("`kind` and `status` are required in the payload")
|
502
499
|
|
squad/ci/models.py
CHANGED
@@ -138,10 +138,8 @@ class Backend(models.Model):
|
|
138
138
|
completed=completed,
|
139
139
|
)
|
140
140
|
test_job.testrun = testrun
|
141
|
-
except InvalidMetadata as exception:
|
141
|
+
except (DuplicatedTestJob, InvalidMetadata) as exception:
|
142
142
|
test_job.failure = str(exception)
|
143
|
-
except DuplicatedTestJob as exception:
|
144
|
-
logger.error('Failed to fetch test_job(%d): "%s"' % (test_job.id, str(exception)))
|
145
143
|
|
146
144
|
if test_job.needs_postprocessing():
|
147
145
|
# Offload postprocessing plugins to a new task
|
@@ -0,0 +1,44 @@
|
|
1
|
+
from django.core.management.base import BaseCommand
|
2
|
+
|
3
|
+
from squad.plugins.linux_log_parser import Plugin as BootTestLogParser
|
4
|
+
from squad.plugins.linux_log_parser_build import Plugin as BuildLogParser
|
5
|
+
|
6
|
+
|
7
|
+
class FakeTestRun:
|
8
|
+
log_file = None
|
9
|
+
id = None
|
10
|
+
|
11
|
+
|
12
|
+
log_parsers = {
|
13
|
+
'linux_log_parser_boot_test': BootTestLogParser(),
|
14
|
+
"linux_log_parser_build": BuildLogParser(),
|
15
|
+
}
|
16
|
+
|
17
|
+
|
18
|
+
class Command(BaseCommand):
|
19
|
+
|
20
|
+
help = """Run a log parser and print the outputs to the stdout."""
|
21
|
+
|
22
|
+
def add_arguments(self, parser):
|
23
|
+
|
24
|
+
parser.add_argument(
|
25
|
+
"LOG_FILE",
|
26
|
+
help="Log file to parser",
|
27
|
+
)
|
28
|
+
|
29
|
+
parser.add_argument(
|
30
|
+
"LOG_PARSER",
|
31
|
+
choices=log_parsers.keys(),
|
32
|
+
help="Which log parser to run"
|
33
|
+
)
|
34
|
+
|
35
|
+
def handle(self, *args, **options):
|
36
|
+
self.options = options
|
37
|
+
|
38
|
+
with open(options["LOG_FILE"], "r") as f:
|
39
|
+
log_file = f.read()
|
40
|
+
|
41
|
+
testrun = FakeTestRun()
|
42
|
+
testrun.log_file = log_file
|
43
|
+
parser = log_parsers[options["LOG_PARSER"]]
|
44
|
+
parser.postprocess_testrun(testrun, squad=False, print=True)
|
squad/core/models.py
CHANGED
@@ -493,6 +493,9 @@ class Build(models.Model):
|
|
493
493
|
ordering = ['datetime']
|
494
494
|
|
495
495
|
def save(self, *args, **kwargs):
|
496
|
+
# Initialize this to timezone.now(), then if a testrun is seen with an
|
497
|
+
# earlier datetime, keep this value up to date with the earliest
|
498
|
+
# testrun.datetime (handled in ReceiveTestRun.__call__).
|
496
499
|
if not self.datetime:
|
497
500
|
self.datetime = timezone.now()
|
498
501
|
with transaction.atomic():
|
@@ -577,12 +580,17 @@ class Build(models.Model):
|
|
577
580
|
List of attachments from all testruns
|
578
581
|
"""
|
579
582
|
if self.__attachments__ is None:
|
583
|
+
test_run_ids = self.test_runs.values_list('id', flat=True)
|
584
|
+
all_attachments = Attachment.objects.filter(test_run_id__in=test_run_ids).values(
|
585
|
+
'test_run_id', 'filename'
|
586
|
+
)
|
587
|
+
|
580
588
|
attachments = {}
|
581
|
-
for
|
582
|
-
attachments[
|
583
|
-
|
584
|
-
attachments[test_run.pk].append(attachment.filename)
|
589
|
+
for attachment in all_attachments:
|
590
|
+
attachments.setdefault(attachment['test_run_id'], []).append(attachment['filename'])
|
591
|
+
|
585
592
|
self.__attachments__ = attachments
|
593
|
+
|
586
594
|
return self.__attachments__
|
587
595
|
|
588
596
|
@property
|
@@ -801,6 +809,9 @@ class TestRun(models.Model):
|
|
801
809
|
unique_together = ('build', 'job_id')
|
802
810
|
|
803
811
|
def save(self, *args, **kwargs):
|
812
|
+
# testrun.datetime will take datetime from the metadata if it exists
|
813
|
+
# (during ReceiveTestRun.__call__). If datetime is not in the metadata,
|
814
|
+
# set it to timezone.now()
|
804
815
|
if not self.datetime:
|
805
816
|
self.datetime = timezone.now()
|
806
817
|
if self.__metadata__:
|
squad/core/tasks/__init__.py
CHANGED
@@ -178,6 +178,8 @@ class ReceiveTestRun(object):
|
|
178
178
|
|
179
179
|
testrun.refresh_from_db()
|
180
180
|
|
181
|
+
# This keeps the datetime of the build in line with the earliest
|
182
|
+
# observed testrun.datetime.
|
181
183
|
if not build.datetime or testrun.datetime < build.datetime:
|
182
184
|
build.datetime = testrun.datetime
|
183
185
|
build.save()
|
@@ -16,17 +16,44 @@ square_brackets_and_contents = r"\[[^\]]+\]"
|
|
16
16
|
|
17
17
|
class BaseLogParser:
|
18
18
|
def compile_regexes(self, regexes):
|
19
|
-
|
20
|
-
|
19
|
+
with_brackets = [r"(%s)" % r[REGEX_BODY] for r in regexes]
|
20
|
+
combined = r"|".join(with_brackets)
|
21
|
+
|
22
|
+
# In the case where there is only one regex, we need to add extra
|
23
|
+
# bracket around it for it to behave the same as the multiple regex
|
24
|
+
# case
|
25
|
+
if len(regexes) == 1:
|
26
|
+
combined = f"({combined})"
|
27
|
+
|
28
|
+
return re.compile(combined, re.S | re.M)
|
21
29
|
|
22
30
|
def remove_numbers_and_time(self, snippet):
|
23
|
-
# [
|
24
|
-
#
|
25
|
-
|
31
|
+
# [ 92.236941] CPU: 1 PID: 191 Comm: kunit_try_catch Tainted: G W 5.15.75-rc1 #1
|
32
|
+
# <4>[ 87.925462] CPU: 0 PID: 135 Comm: (crub_all) Not tainted 6.7.0-next-20240111 #14
|
33
|
+
# Remove '(Not t|T)ainted', to the end of the line.
|
34
|
+
without_tainted = re.sub(r"(Not t|T)ainted.*", "", snippet)
|
35
|
+
|
36
|
+
# x23: ffff9b7275bc6f90 x22: ffff9b7275bcfb50 x21: fff00000cc80ef88
|
37
|
+
# x20: 1ffff00010668fb8 x19: ffff8000800879f0 x18: 00000000805c0b5c
|
38
|
+
# Remove words with hex numbers.
|
39
|
+
# <3>[ 2.491276][ T1] BUG: KCSAN: data-race in console_emit_next_record / console_trylock_spinning
|
40
|
+
# -> <>[ .][ T1] BUG: KCSAN: data-race in console_emit_next_record / console_trylock_spinning
|
41
|
+
without_hex = re.sub(r"\b(?:0x)?[a-fA-F0-9]+\b", "", without_tainted)
|
42
|
+
|
43
|
+
# <>[ 1067.461794][ T132] BUG: KCSAN: data-race in do_page_fault spectre_v4_enable_task_mitigation
|
44
|
+
# -> <>[ .][ T132] BUG: KCSAN: data-race in do_page_fault spectre_v_enable_task_mitigation
|
45
|
+
# But should not remove numbers from functions.
|
46
|
+
without_numbers = re.sub(
|
47
|
+
r"(0x[a-f0-9]+|[<\[][0-9a-f]+?[>\]]|\b\d+\b(?!\s*\())", "", without_hex
|
48
|
+
)
|
26
49
|
|
27
|
-
# [ .][
|
50
|
+
# <>[ .][ T132] BUG: KCSAN: data-race in do_page_fault spectre_v_enable_task_mitigation
|
28
51
|
# -> BUG: KCSAN: data-race in do_page_fault spectre_v_enable_task_mitigation
|
29
|
-
without_time = re.sub(
|
52
|
+
without_time = re.sub(
|
53
|
+
f"^<?>?{square_brackets_and_contents}({square_brackets_and_contents})?",
|
54
|
+
"",
|
55
|
+
without_numbers,
|
56
|
+
) # noqa
|
30
57
|
|
31
58
|
return without_time
|
32
59
|
|
@@ -41,10 +68,7 @@ class BaseLogParser:
|
|
41
68
|
snippet = matches[0]
|
42
69
|
without_numbers_and_time = self.remove_numbers_and_time(snippet)
|
43
70
|
|
44
|
-
|
45
|
-
# for SuiteMetadata in SQUAD is 256 characters. The SHA and "-" take 65
|
46
|
-
# characters: 256-65=191
|
47
|
-
return slugify(without_numbers_and_time)[:191]
|
71
|
+
return slugify(without_numbers_and_time)
|
48
72
|
|
49
73
|
def create_shasum(self, snippet):
|
50
74
|
sha = hashlib.sha256()
|
@@ -52,7 +76,7 @@ class BaseLogParser:
|
|
52
76
|
sha.update(without_numbers_and_time.encode())
|
53
77
|
return sha.hexdigest()
|
54
78
|
|
55
|
-
def create_name_log_dict(self, test_name, lines, test_regex=None):
|
79
|
+
def create_name_log_dict(self, test_name, lines, test_regex=None, create_shas=True):
|
56
80
|
"""
|
57
81
|
Produce a dictionary with the test names as keys and the extracted logs
|
58
82
|
for that test name as values. There will be at least one test name per
|
@@ -64,31 +88,43 @@ class BaseLogParser:
|
|
64
88
|
# have any output for a particular regex, just use the default name
|
65
89
|
# (for example "check-kernel-oops").
|
66
90
|
tests_without_shas_to_create = defaultdict(set)
|
67
|
-
tests_with_shas_to_create =
|
91
|
+
tests_with_shas_to_create = None
|
68
92
|
|
69
93
|
# If there are lines, then create the tests for these.
|
70
94
|
for line in lines:
|
71
95
|
extracted_name = self.create_name(line, test_regex)
|
72
96
|
if extracted_name:
|
73
|
-
|
97
|
+
max_name_length = 256
|
98
|
+
# If adding SHAs, limit the name length to 191 characters,
|
99
|
+
# since the max name length for SuiteMetadata in SQUAD is 256
|
100
|
+
# characters. The SHA and "-" take 65 characters: 256-65=191
|
101
|
+
if create_shas:
|
102
|
+
max_name_length -= 65
|
103
|
+
extended_test_name = f"{test_name}-{extracted_name}"[:max_name_length]
|
74
104
|
else:
|
75
105
|
extended_test_name = test_name
|
76
106
|
tests_without_shas_to_create[extended_test_name].add(line)
|
77
107
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
108
|
+
if create_shas:
|
109
|
+
tests_with_shas_to_create = defaultdict(set)
|
110
|
+
for name, test_lines in tests_without_shas_to_create.items():
|
111
|
+
# Some lines of the matched regex might be the same, and we don't want to create
|
112
|
+
# multiple tests like test1-sha1, test1-sha1, etc, so we'll create a set of sha1sums
|
113
|
+
# then create only new tests for unique sha's
|
82
114
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
115
|
+
for line in test_lines:
|
116
|
+
sha = self.create_shasum(line)
|
117
|
+
name_with_sha = f"{name}-{sha}"
|
118
|
+
tests_with_shas_to_create[name_with_sha].add(line)
|
87
119
|
|
88
120
|
return tests_without_shas_to_create, tests_with_shas_to_create
|
89
121
|
|
90
122
|
def create_squad_tests_from_name_log_dict(
|
91
|
-
self,
|
123
|
+
self,
|
124
|
+
suite_name,
|
125
|
+
testrun,
|
126
|
+
tests_without_shas_to_create,
|
127
|
+
tests_with_shas_to_create=None,
|
92
128
|
):
|
93
129
|
# Import SuiteMetadata from SQUAD only when required so BaseLogParser
|
94
130
|
# does not require a SQUAD to work. This makes it easier to reuse this
|
@@ -96,6 +132,8 @@ class BaseLogParser:
|
|
96
132
|
# patterns.
|
97
133
|
from squad.core.models import SuiteMetadata
|
98
134
|
|
135
|
+
suite, _ = testrun.build.project.suites.get_or_create(slug=suite_name)
|
136
|
+
|
99
137
|
for name, lines in tests_without_shas_to_create.items():
|
100
138
|
metadata, _ = SuiteMetadata.objects.get_or_create(
|
101
139
|
suite=suite.slug, name=name, kind="test"
|
@@ -108,34 +146,72 @@ class BaseLogParser:
|
|
108
146
|
build=testrun.build,
|
109
147
|
environment=testrun.environment,
|
110
148
|
)
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
149
|
+
if tests_with_shas_to_create:
|
150
|
+
for name_with_sha, lines in tests_with_shas_to_create.items():
|
151
|
+
metadata, _ = SuiteMetadata.objects.get_or_create(
|
152
|
+
suite=suite.slug, name=name_with_sha, kind="test"
|
153
|
+
)
|
154
|
+
testrun.tests.create(
|
155
|
+
suite=suite,
|
156
|
+
result=False,
|
157
|
+
log="\n---\n".join(lines),
|
158
|
+
metadata=metadata,
|
159
|
+
build=testrun.build,
|
160
|
+
environment=testrun.environment,
|
161
|
+
)
|
162
|
+
|
163
|
+
def print_squad_tests_from_name_log_dict(
|
164
|
+
self,
|
165
|
+
suite_name,
|
166
|
+
tests_without_shas_to_create,
|
167
|
+
tests_with_shas_to_create=None,
|
168
|
+
):
|
169
|
+
for name, lines in tests_without_shas_to_create.items():
|
170
|
+
print(f"\nName: {suite_name}/{name}")
|
171
|
+
log = "\n".join(lines)
|
172
|
+
print(f"Log:\n{log}")
|
173
|
+
|
174
|
+
if tests_with_shas_to_create:
|
175
|
+
for name_with_sha, lines in tests_with_shas_to_create.items():
|
176
|
+
print(f"\nName: {suite_name}/{name_with_sha}")
|
177
|
+
log = "\n---\n".join(lines)
|
178
|
+
print(f"Log:\n{log}")
|
179
|
+
|
180
|
+
def create_squad_tests(
|
181
|
+
self,
|
182
|
+
testrun,
|
183
|
+
suite_name,
|
184
|
+
test_name,
|
185
|
+
lines,
|
186
|
+
test_regex=None,
|
187
|
+
create_shas=True,
|
188
|
+
print=False,
|
189
|
+
squad=True,
|
190
|
+
):
|
125
191
|
"""
|
126
192
|
There will be at least one test per regex. If there were any match for
|
127
193
|
a given regex, then a new test will be generated using test_name +
|
128
194
|
shasum. This helps comparing kernel logs across different builds
|
129
195
|
"""
|
196
|
+
|
130
197
|
tests_without_shas_to_create, tests_with_shas_to_create = (
|
131
|
-
self.create_name_log_dict(
|
132
|
-
|
133
|
-
|
134
|
-
suite,
|
135
|
-
testrun,
|
136
|
-
tests_without_shas_to_create,
|
137
|
-
tests_with_shas_to_create,
|
198
|
+
self.create_name_log_dict(
|
199
|
+
test_name, lines, test_regex, create_shas=create_shas
|
200
|
+
)
|
138
201
|
)
|
202
|
+
if print:
|
203
|
+
self.print_squad_tests_from_name_log_dict(
|
204
|
+
suite_name,
|
205
|
+
tests_without_shas_to_create,
|
206
|
+
tests_with_shas_to_create,
|
207
|
+
)
|
208
|
+
if squad:
|
209
|
+
self.create_squad_tests_from_name_log_dict(
|
210
|
+
suite_name,
|
211
|
+
testrun,
|
212
|
+
tests_without_shas_to_create,
|
213
|
+
tests_with_shas_to_create,
|
214
|
+
)
|
139
215
|
|
140
216
|
def join_matches(self, matches, regexes):
|
141
217
|
"""
|
@@ -44,8 +44,9 @@ class Plugin(BasePlugin, BaseLogParser):
|
|
44
44
|
kernel_msgs = re.findall(f'({tstamp}{pid}? .*?)$', log, re.S | re.M) # noqa
|
45
45
|
return '\n'.join(kernel_msgs)
|
46
46
|
|
47
|
-
def postprocess_testrun(self, testrun):
|
48
|
-
if
|
47
|
+
def postprocess_testrun(self, testrun, squad=True, print=False):
|
48
|
+
# If running as a SQUAD plugin, only run the boot/test log parser if this is not a build testrun
|
49
|
+
if testrun.log_file is None or (squad and testrun.tests.filter(suite__slug="build").exists()):
|
49
50
|
return
|
50
51
|
|
51
52
|
boot_log, test_log = self.__cutoff_boot_log(testrun.log_file)
|
@@ -56,7 +57,7 @@ class Plugin(BasePlugin, BaseLogParser):
|
|
56
57
|
|
57
58
|
for log_type, log in logs.items():
|
58
59
|
log = self.__kernel_msgs_only(log)
|
59
|
-
|
60
|
+
suite_name = f'log-parser-{log_type}'
|
60
61
|
|
61
62
|
regex = self.compile_regexes(REGEXES)
|
62
63
|
matches = regex.findall(log)
|
@@ -68,4 +69,4 @@ class Plugin(BasePlugin, BaseLogParser):
|
|
68
69
|
test_name_regex = None
|
69
70
|
if regex_pattern:
|
70
71
|
test_name_regex = re.compile(regex_pattern, re.S | re.M)
|
71
|
-
self.create_squad_tests(testrun,
|
72
|
+
self.create_squad_tests(testrun, suite_name, test_name, snippets[regex_id], test_name_regex, squad=squad, print=print)
|
@@ -0,0 +1,334 @@
|
|
1
|
+
import logging
|
2
|
+
import re
|
3
|
+
|
4
|
+
from django.template.defaultfilters import slugify
|
5
|
+
|
6
|
+
from squad.plugins import Plugin as BasePlugin
|
7
|
+
from squad.plugins.lib.base_log_parser import (
|
8
|
+
REGEX_EXTRACT_NAME,
|
9
|
+
REGEX_NAME,
|
10
|
+
BaseLogParser,
|
11
|
+
)
|
12
|
+
|
13
|
+
logger = logging.getLogger()
|
14
|
+
|
15
|
+
file_path = r"^(?:[^\n]*?:(?:\d+:){2}|<[^\n]*?>:)"
|
16
|
+
gcc_clang_compiler_error_warning = rf"{file_path} (?:error|warning): [^\n]+?\n^(?:\.+\n|^(?!\s+(?:CC|Kernel[^\n]*?is ready))\s+?[^\n]+\n|{file_path} note:[^\n]+\n)*"
|
17
|
+
|
18
|
+
MULTILINERS_GCC = [
|
19
|
+
(
|
20
|
+
"gcc-compiler",
|
21
|
+
gcc_clang_compiler_error_warning,
|
22
|
+
r"^[^\n]*(?:error|warning)[^\n]*$",
|
23
|
+
),
|
24
|
+
]
|
25
|
+
|
26
|
+
ONELINERS_GCC = []
|
27
|
+
|
28
|
+
|
29
|
+
MULTILINERS_CLANG = [
|
30
|
+
(
|
31
|
+
"clang-compiler",
|
32
|
+
gcc_clang_compiler_error_warning,
|
33
|
+
r"^[^\n]*(?:error|warning)[^\n]*$",
|
34
|
+
),
|
35
|
+
]
|
36
|
+
|
37
|
+
ONELINERS_CLANG = [
|
38
|
+
(
|
39
|
+
"clang-compiler-single-line",
|
40
|
+
"^clang: (?:error|warning).*?$",
|
41
|
+
r"^[^\n]*(?:error|warning).*?$",
|
42
|
+
),
|
43
|
+
(
|
44
|
+
"clang-compiler-fatal-error",
|
45
|
+
"^fatal error.*?$",
|
46
|
+
r"^fatal error.*?$",
|
47
|
+
),
|
48
|
+
]
|
49
|
+
|
50
|
+
MULTILINERS_GENERAL = [
|
51
|
+
(
|
52
|
+
"general-not-a-git-repo",
|
53
|
+
r"^[^\n]*fatal: not a git repository.*?not set\)\.$",
|
54
|
+
r"^[^\n]*fatal: not a git repository.*?$",
|
55
|
+
),
|
56
|
+
(
|
57
|
+
"general-unexpected-argument",
|
58
|
+
r"^[^\n]*error: Found argument.*?--help$",
|
59
|
+
r"^[^\n]*error: Found argument.*?$",
|
60
|
+
),
|
61
|
+
(
|
62
|
+
"general-broken-32-bit",
|
63
|
+
r"^[^\n]*Warning: you seem to have a broken 32-bit build.*?(?:If[^\n]*?try:(?:\n|\s+.+?$)+)+",
|
64
|
+
r"^[^\n]*Warning:.*?$",
|
65
|
+
),
|
66
|
+
(
|
67
|
+
"general-makefile-overriding",
|
68
|
+
r"^[^\n]*warning: overriding recipe for target.*?ignoring old recipe for target.*?$",
|
69
|
+
r"^[^\n]*warning:.*?$",
|
70
|
+
),
|
71
|
+
(
|
72
|
+
"general-unmet-dependencies",
|
73
|
+
r"^WARNING: unmet direct dependencies detected for.*?$(?:\n +[^\n]+)*",
|
74
|
+
r"^WARNING: unmet direct dependencies detected for.*?$",
|
75
|
+
),
|
76
|
+
(
|
77
|
+
"general-ldd",
|
78
|
+
r"^[^\n]*?lld:[^\n]+?(?:warning|error):.*?$(?:\n^>>>[^\n]+)*",
|
79
|
+
r"^[^\n]*?lld:.*?$",
|
80
|
+
),
|
81
|
+
(
|
82
|
+
"general-ld",
|
83
|
+
r"^[^\n]*?ld:[^\n]+?(?:warning|error):[^\n]*?$(?:\n^[^\n]*?NOTE:[^\n]+)*",
|
84
|
+
r"^[^\n]*?ld:[^\n]+?(?:warning|error):.*?$",
|
85
|
+
),
|
86
|
+
(
|
87
|
+
"general-objcopy",
|
88
|
+
r"^[^\n]*?objcopy:[^\n]+?(?:warning|error):[^\n]*?$(?:\n^[^\n]*?NOTE:[^\n]+)*",
|
89
|
+
r"^[^\n]*?objcopy:[^\n]+?(?:warning|error):.*?$",
|
90
|
+
),
|
91
|
+
(
|
92
|
+
"general-ld-undefined-reference",
|
93
|
+
r"^[^\n]*?ld[^\n]*?$\n^[^\n]+undefined reference.*?$",
|
94
|
+
r"^[^\n]+undefined reference.*?$",
|
95
|
+
),
|
96
|
+
(
|
97
|
+
"general-modpost",
|
98
|
+
r"^[^\n]*?WARNING: modpost:[^\n]*?$(?:\n^To see.*?:$\n^.*?$)?",
|
99
|
+
r"^[^\n]*?WARNING.*?$",
|
100
|
+
),
|
101
|
+
(
|
102
|
+
"general-python-traceback",
|
103
|
+
r"Traceback.*?^[^\s]+Error: .*?$",
|
104
|
+
r"^[^\s]+Error: .*?$",
|
105
|
+
),
|
106
|
+
]
|
107
|
+
|
108
|
+
ONELINERS_GENERAL = [
|
109
|
+
(
|
110
|
+
"general-no-such-file-or-directory",
|
111
|
+
r"^[^\n]+?No such file or directory.*?$",
|
112
|
+
r"^[^\n]+?No such file or directory.*?$",
|
113
|
+
),
|
114
|
+
(
|
115
|
+
"general-no-targets",
|
116
|
+
r"^[^\n]+?No targets.*?$",
|
117
|
+
r"^[^\n]+?No targets.*?$",
|
118
|
+
),
|
119
|
+
(
|
120
|
+
"general-no-rule-to-make-target",
|
121
|
+
r"^[^\n]+?No rule to make target.*?$",
|
122
|
+
r"^[^\n]+?No rule to make target.*?$",
|
123
|
+
),
|
124
|
+
(
|
125
|
+
"general-makefile-config",
|
126
|
+
r"^Makefile.config:\d+:.*?$",
|
127
|
+
r"^Makefile.config:\d+:.*?$",
|
128
|
+
),
|
129
|
+
(
|
130
|
+
"general-not-found",
|
131
|
+
r"^[^\n]*?not found.*?$",
|
132
|
+
r"^[^\n]*?not found.*?$",
|
133
|
+
),
|
134
|
+
(
|
135
|
+
"general-kernel-abi",
|
136
|
+
r"^Warning: Kernel ABI header at.*?$",
|
137
|
+
r"^Warning: Kernel ABI header at.*?$",
|
138
|
+
),
|
139
|
+
(
|
140
|
+
"general-missing",
|
141
|
+
r"^Warning: missing.*?$",
|
142
|
+
r"^Warning: missing.*?$",
|
143
|
+
),
|
144
|
+
(
|
145
|
+
"general-dtc",
|
146
|
+
r"^[^\n]*?Warning \([^\n]*?\).*?$",
|
147
|
+
r"^[^\n]*?Warning.*?$",
|
148
|
+
),
|
149
|
+
(
|
150
|
+
"general-register-allocation",
|
151
|
+
r"^[^\n]*?error: register allocation failed.*?$",
|
152
|
+
r"^[^\n]*?error.*?$",
|
153
|
+
),
|
154
|
+
]
|
155
|
+
|
156
|
+
# Tip: broader regexes should come first
|
157
|
+
REGEXES_GCC = MULTILINERS_GCC + MULTILINERS_GENERAL + ONELINERS_GCC + ONELINERS_GENERAL
|
158
|
+
REGEXES_CLANG = (
|
159
|
+
MULTILINERS_CLANG + MULTILINERS_GENERAL + ONELINERS_CLANG + ONELINERS_GENERAL
|
160
|
+
)
|
161
|
+
|
162
|
+
supported_toolchains = {
|
163
|
+
"gcc": REGEXES_GCC,
|
164
|
+
"clang": REGEXES_CLANG,
|
165
|
+
}
|
166
|
+
|
167
|
+
make_regex = r"^make .*?$"
|
168
|
+
in_file_regex = r"^In file[^\n]*?[:,]$(?:\n^(?:\s+|In file)[^\n]*?[:,]$)*"
|
169
|
+
in_function_regex = r"^[^\n]*?In function.*?:$"
|
170
|
+
entering_dir_regex = r"^make\[(?:\d+)\]: Entering directory.*?$"
|
171
|
+
leaving_dir_regex = r"^make\[(?:\d+)\]: Leaving directory.*?$"
|
172
|
+
|
173
|
+
split_regex_gcc = rf"(.*?)({make_regex}|{in_file_regex}|{in_function_regex}|{entering_dir_regex}|{leaving_dir_regex})"
|
174
|
+
|
175
|
+
|
176
|
+
class Plugin(BasePlugin, BaseLogParser):
|
177
|
+
|
178
|
+
def post_process_test_name(self, text):
|
179
|
+
# Remove "builds/linux" if there
|
180
|
+
text = re.sub(r"builds/linux", "", text)
|
181
|
+
|
182
|
+
# Change "/" and "." to "_" for readability
|
183
|
+
text = re.sub(r"[/\.]", "_", text)
|
184
|
+
|
185
|
+
# Remove numbers and hex
|
186
|
+
text = re.sub(r"(0x[a-f0-9]+|[<\[][0-9a-f]+?[>\]]|\d+)", "", text)
|
187
|
+
|
188
|
+
# Remove "{...}" and "[...]"
|
189
|
+
text = re.sub(r"\{.+?\}", "", text)
|
190
|
+
text = re.sub(r"\[.+?\]", "", text)
|
191
|
+
|
192
|
+
return text
|
193
|
+
|
194
|
+
def create_name(self, snippet, compiled_regex=None):
|
195
|
+
matches = None
|
196
|
+
if compiled_regex:
|
197
|
+
matches = compiled_regex.findall(snippet)
|
198
|
+
if not matches:
|
199
|
+
# Only extract a name if we provide a regex to extract the name and
|
200
|
+
# there is a match
|
201
|
+
return None
|
202
|
+
snippet = matches[0]
|
203
|
+
without_numbers = re.sub(
|
204
|
+
r"(0x[a-f0-9]+|[<\[][0-9a-f]+?[>\]]|\b\d+\b(?!\s*\())", "", snippet
|
205
|
+
)
|
206
|
+
|
207
|
+
name = slugify(self.post_process_test_name(without_numbers))
|
208
|
+
|
209
|
+
return name
|
210
|
+
|
211
|
+
def split_by_regex(self, log, regex):
|
212
|
+
# Split up the log by the keywords we want to capture
|
213
|
+
s_lines_compiled = re.compile(regex, re.DOTALL | re.MULTILINE)
|
214
|
+
split_by_regex_list = s_lines_compiled.split(log)
|
215
|
+
split_by_regex_list = [
|
216
|
+
f for f in split_by_regex_list if f is not None and f != ""
|
217
|
+
]
|
218
|
+
|
219
|
+
return split_by_regex_list
|
220
|
+
|
221
|
+
def process_blocks(
|
222
|
+
self,
|
223
|
+
blocks_to_process,
|
224
|
+
regexes,
|
225
|
+
make_regex=make_regex,
|
226
|
+
entering_dir_regex=entering_dir_regex,
|
227
|
+
leaving_dir_regex=leaving_dir_regex,
|
228
|
+
in_file_regex=in_file_regex,
|
229
|
+
in_function_regex=in_function_regex,
|
230
|
+
):
|
231
|
+
snippets = dict()
|
232
|
+
regex_compiled = self.compile_regexes(regexes)
|
233
|
+
make_regex_compiled = re.compile(make_regex, re.DOTALL | re.MULTILINE)
|
234
|
+
entering_dir_regex_compiled = re.compile(
|
235
|
+
entering_dir_regex, re.DOTALL | re.MULTILINE
|
236
|
+
)
|
237
|
+
leaving_dir_regex_compiled = re.compile(
|
238
|
+
leaving_dir_regex, re.DOTALL | re.MULTILINE
|
239
|
+
)
|
240
|
+
in_file_regex_compiled = re.compile(in_file_regex, re.DOTALL | re.MULTILINE)
|
241
|
+
in_function_regex_compiled = re.compile(
|
242
|
+
in_function_regex, re.DOTALL | re.MULTILINE
|
243
|
+
)
|
244
|
+
|
245
|
+
# For tracking the last piece of information we saw
|
246
|
+
make_command = None
|
247
|
+
entering_dir = None
|
248
|
+
in_file = None
|
249
|
+
in_function = None
|
250
|
+
|
251
|
+
for regex_id in range(len(regexes)):
|
252
|
+
snippets[regex_id] = []
|
253
|
+
for block in blocks_to_process:
|
254
|
+
if make_regex_compiled.match(block):
|
255
|
+
make_command = block
|
256
|
+
entering_dir = None
|
257
|
+
in_file = None
|
258
|
+
in_function = None
|
259
|
+
elif entering_dir_regex_compiled.match(block):
|
260
|
+
entering_dir = block
|
261
|
+
in_file = None
|
262
|
+
in_function = None
|
263
|
+
elif leaving_dir_regex_compiled.match(block):
|
264
|
+
entering_dir = None
|
265
|
+
in_file = None
|
266
|
+
in_function = None
|
267
|
+
elif in_file_regex_compiled.match(block):
|
268
|
+
in_file = block
|
269
|
+
in_function = None
|
270
|
+
elif in_function_regex_compiled.match(block):
|
271
|
+
in_function = block
|
272
|
+
else:
|
273
|
+
matches = regex_compiled.findall(block)
|
274
|
+
sub_snippets = self.join_matches(matches, regexes)
|
275
|
+
prepend = ""
|
276
|
+
if make_command:
|
277
|
+
prepend += make_command + "\n"
|
278
|
+
if entering_dir:
|
279
|
+
prepend += entering_dir + "\n"
|
280
|
+
if in_file:
|
281
|
+
prepend += in_file + "\n"
|
282
|
+
if in_function:
|
283
|
+
prepend += in_function + "\n"
|
284
|
+
for regex_id in range(len(regexes)):
|
285
|
+
for s in sub_snippets[regex_id]:
|
286
|
+
snippets[regex_id].append(prepend + s)
|
287
|
+
|
288
|
+
return snippets
|
289
|
+
|
290
|
+
def postprocess_testrun(self, testrun, squad=True, print=False):
|
291
|
+
"""
|
292
|
+
Check:
|
293
|
+
- There is a log file
|
294
|
+
- If running as SQUAD plugin, the testrun contains the "build"
|
295
|
+
suite - this tells us that the testrun's log is a build log
|
296
|
+
"""
|
297
|
+
if testrun.log_file is None or (
|
298
|
+
squad and not testrun.tests.filter(suite__slug="build").exists()
|
299
|
+
):
|
300
|
+
return
|
301
|
+
|
302
|
+
regexes = None
|
303
|
+
for toolchain, toolchain_regexes in supported_toolchains.items():
|
304
|
+
if f"--toolchain={toolchain}" in testrun.log_file:
|
305
|
+
toolchain_name = toolchain
|
306
|
+
regexes = toolchain_regexes
|
307
|
+
|
308
|
+
# If a supported toolchain was not found in the log
|
309
|
+
if regexes is None:
|
310
|
+
return
|
311
|
+
|
312
|
+
# If running in SQUAD, create the suite
|
313
|
+
suite_name = f"log-parser-build-{toolchain_name}"
|
314
|
+
|
315
|
+
blocks_to_process = self.split_by_regex(testrun.log_file, split_regex_gcc)
|
316
|
+
|
317
|
+
snippets = self.process_blocks(blocks_to_process, regexes)
|
318
|
+
|
319
|
+
for regex_id in range(len(regexes)):
|
320
|
+
test_name = regexes[regex_id][REGEX_NAME]
|
321
|
+
regex_pattern = regexes[regex_id][REGEX_EXTRACT_NAME]
|
322
|
+
test_name_regex = None
|
323
|
+
if regex_pattern:
|
324
|
+
test_name_regex = re.compile(regex_pattern, re.S | re.M)
|
325
|
+
self.create_squad_tests(
|
326
|
+
testrun,
|
327
|
+
suite_name,
|
328
|
+
test_name,
|
329
|
+
snippets[regex_id],
|
330
|
+
test_name_regex,
|
331
|
+
create_shas=False,
|
332
|
+
print=print,
|
333
|
+
squad=squad,
|
334
|
+
)
|
squad/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = '1.
|
1
|
+
__version__ = '1.93.1'
|
@@ -10,7 +10,7 @@ squad/manage.py,sha256=Z-LXT67p0R-IzwJ9fLIAacEZmU0VUjqDOSg7j2ZSxJ4,1437
|
|
10
10
|
squad/settings.py,sha256=0MZ48SV_7CTrLMik2ubWf8-ROQiFju6CKnUC3iR8KAc,14800
|
11
11
|
squad/socialaccount.py,sha256=vySqPwQ3qVVpahuJ-Snln8K--yzRL3bw4Nx27AsB39A,789
|
12
12
|
squad/urls.py,sha256=JiEfVW8YlzLPE52c2aHzdn5kVVKK4o22w8h5KOA6QhQ,2776
|
13
|
-
squad/version.py,sha256=
|
13
|
+
squad/version.py,sha256=N1lgYLLJIViE9VAAq7J5l3sFo7_BdyvoOEdr3j9eNaA,23
|
14
14
|
squad/wsgi.py,sha256=SF8T0cQ0OPVyuYjO5YXBIQzvSXQHV0M2BTmd4gP1rPs,387
|
15
15
|
squad/api/__init__.py,sha256=CJiVakfAlHVN5mIFRVQYZQfuNUhUgWVbsdYTME4tq7U,1349
|
16
16
|
squad/api/apps.py,sha256=Trk72p-iV1uGn0o5mdJn5HARUoHGbfgO49jwXvpkmdQ,141
|
@@ -26,14 +26,14 @@ squad/ci/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
26
|
squad/ci/admin.py,sha256=7yB-6F0cvt0NVvzGOTlZCyGPV_YHarmbKJZTTzataT4,2255
|
27
27
|
squad/ci/apps.py,sha256=6OVnzTdJkxdqEJnKWYE9dZgUcc29_T1LrDw41cK4EQk,139
|
28
28
|
squad/ci/exceptions.py,sha256=a1sccygniTYDSQi7FRn_6doapddFFiMf55AwGUh5Y80,227
|
29
|
-
squad/ci/models.py,sha256=
|
29
|
+
squad/ci/models.py,sha256=wR9FMBdjQgtEP3ga9CY6npFr5fUIeVpnfAhNa2xqM00,15897
|
30
30
|
squad/ci/tasks.py,sha256=P0NYjLuyUViTpO1jZMuRVREbFDCccrMCZDw5E4pt928,3882
|
31
31
|
squad/ci/utils.py,sha256=38zHpw8xkZDSFlkG-2BwSK6AkcddK9OkN9LXuQ3SHR0,97
|
32
32
|
squad/ci/backend/__init__.py,sha256=yhpotXT9F4IdAOXvGQ3-17eOHAFwoaqf9SnMX17ab30,534
|
33
33
|
squad/ci/backend/fake.py,sha256=7Rl-JXnBYThDomOBzBsN9XuVkSjSHTZjtZOURdowZbA,2397
|
34
34
|
squad/ci/backend/lava.py,sha256=WeOJJNxv42geGf3Y6r-I0WnhWinxpSSgZAFAwfkiXGY,34039
|
35
35
|
squad/ci/backend/null.py,sha256=htEd4NbrXLKdPgFfTS0Ixm8PdT6Ghat3BCYi2zjfuv0,5624
|
36
|
-
squad/ci/backend/tuxsuite.py,sha256=
|
36
|
+
squad/ci/backend/tuxsuite.py,sha256=pFcNdcHpFzalHPQhbSY6ryOci_PU3LFsaNjSsgjbqGg,18676
|
37
37
|
squad/ci/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
38
38
|
squad/ci/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
39
39
|
squad/ci/management/commands/create_tuxsuite_boot_tests.py,sha256=JvjNusebLX71eyz9d-kaeCyekYSpzc1eXoeIqWK9ygo,4045
|
@@ -82,7 +82,7 @@ squad/core/comparison.py,sha256=LR3-Unv0CTmakFCDzF_h8fm2peTJzkv79mQWNau1iwI,2442
|
|
82
82
|
squad/core/data.py,sha256=2zw56v7iYRTUc7wlhuUNgwIIMmK2w84hi-amR9J7EPU,2236
|
83
83
|
squad/core/failures.py,sha256=X6lJVghM2fOrd-RfuHeLlezW2pt7owDZ8eX-Kn_Qrt0,918
|
84
84
|
squad/core/history.py,sha256=QRSIoDOw6R6vUWMtsPMknsHGM7FaCAeuCYqASCayHTk,3541
|
85
|
-
squad/core/models.py,sha256=
|
85
|
+
squad/core/models.py,sha256=qSLlxjBwzsZKGoCkPX6T-g48jXg81B1JH3wMXSLLvHQ,61401
|
86
86
|
squad/core/notification.py,sha256=rOpO6F63w7_5l9gQgWBBEk-MFBjp7x_hVzoVIVyDze0,10030
|
87
87
|
squad/core/plugins.py,sha256=FLgyoXXKnPBYEf2MgHup9M017rHuADHivLhgzmx_cJE,6354
|
88
88
|
squad/core/queries.py,sha256=78fhIJZWXIlDryewYAt96beK1VJad66Ufu8cg3dHh4w,7698
|
@@ -105,6 +105,7 @@ squad/core/management/commands/migrate_test_runs.py,sha256=RHV06tb4gWyv_q-ooC821
|
|
105
105
|
squad/core/management/commands/populate_metric_build_and_environment.py,sha256=DJP9_YLRso0RiERBVsB0GP4-GaiRtJb0rAiUQDfFNQk,3166
|
106
106
|
squad/core/management/commands/populate_test_build_and_environment.py,sha256=0yHClC0x_8LSZlvT6Ag0BnipC9Xk-U6lcIaCsqAGEWk,3146
|
107
107
|
squad/core/management/commands/prepdump.py,sha256=WM58leVdJj45KhWPw3DGO7vwnNY70ReXrJRSIIzGXkI,518
|
108
|
+
squad/core/management/commands/run_log_parser.py,sha256=SeksSD1cnbgl8oRsD3wu12p30_FMw090T6ouQyO4ZsI,1113
|
108
109
|
squad/core/management/commands/send-email.py,sha256=wb1o5oKLDyH2ZonnQY-Jw28Y0Mu61OHWP8b1AQGKqbU,1120
|
109
110
|
squad/core/management/commands/update_project_statuses.py,sha256=JleCesbVhYOSXr90ntH7s5u9Isknt7EnlX22VC6yI78,2089
|
110
111
|
squad/core/management/commands/users.py,sha256=qIp87xRMfKWHymsAft5-gnYajm2mgaiHvVn7z86DCT8,9429
|
@@ -278,7 +279,7 @@ squad/core/migrations/0167_add_project_datetime.py,sha256=VUBG-qsAhh2f2NXaHOqfX9
|
|
278
279
|
squad/core/migrations/0168_add_group_settings.py,sha256=5UdylfMMNavTL0KXkjPSiEMhSisGWXbhUXQSzfK29Ck,462
|
279
280
|
squad/core/migrations/0169_userpreferences.py,sha256=FwYv9RWxMWdQ2lXJMgi-Xc6XBB5Kp-_YTAOr9GVq1To,1098
|
280
281
|
squad/core/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
281
|
-
squad/core/tasks/__init__.py,sha256=
|
282
|
+
squad/core/tasks/__init__.py,sha256=wKjyFw0JXiEDY6PEaKx3ureiSNIQFL8lHH4JIOMjlF8,18677
|
282
283
|
squad/core/tasks/exceptions.py,sha256=n4cbmJFBdA6KWsGiTbfN9DyYGbJpk0DjR0UneEYw_W0,931
|
283
284
|
squad/core/tasks/notification.py,sha256=6ZyTbUQZPITPP-4r9MUON7x-NbwvDBG8YeabM6fsjzA,4915
|
284
285
|
squad/core/templates/squad/notification/base.jinja2,sha256=AbtQioEHV5DJBW4Etsu0-DQXd_8tQCnLejzgbDGDW7s,3413
|
@@ -426,17 +427,18 @@ squad/plugins/__init__.py,sha256=9BSzy2jFIoDpWlhD7odPPrLdW4CC3btBhdFCvB651dM,152
|
|
426
427
|
squad/plugins/example.py,sha256=BKpwd315lHRIuNXJPteibpwfnI6C5eXYHYdFYBtVmsI,89
|
427
428
|
squad/plugins/gerrit.py,sha256=CqO2KnFQzu9utr_TQ-sGr1wg3ln0B-bS2-c0_i8T5-c,7009
|
428
429
|
squad/plugins/github.py,sha256=pdtLZw_7xNuzkaFvY_zWi0f2rsMlalXjKm7sz0eADz4,2429
|
429
|
-
squad/plugins/linux_log_parser.py,sha256=
|
430
|
+
squad/plugins/linux_log_parser.py,sha256=HQVreyZLBmLuv-K-MjlN43sQQSkcls4hkUsjJ9_5WfM,3472
|
431
|
+
squad/plugins/linux_log_parser_build.py,sha256=42pTj1_inTsiS_-htElNWw5Cod0bxpF8ZAm1qvYVhes,10481
|
430
432
|
squad/plugins/lib/__init__.py,sha256=jzazbAvp2_ibblAs0cKZrmo9aR2EL3hKLyRDE008r2I,40
|
431
|
-
squad/plugins/lib/base_log_parser.py,sha256=
|
433
|
+
squad/plugins/lib/base_log_parser.py,sha256=Bb3ok6R9_65EYvdWAsm8wcY741duGujTpaDXw1gJ9Yk,9366
|
432
434
|
squad/run/__init__.py,sha256=ssE8GPAGFiK6V0WpZYowav6Zqsd63dfDMMYasNa1sQg,1410
|
433
435
|
squad/run/__main__.py,sha256=DOl8JOi4Yg7DdtwnUeGqtYBJ6P2k-D2psAEuYOjWr8w,66
|
434
436
|
squad/run/listener.py,sha256=jBeOQhPGb4EdIREB1QsCzYuumsfJ-TqJPd3nR-0m59g,200
|
435
437
|
squad/run/scheduler.py,sha256=CDJG3q5C0GuQuxwlMOfWTSSJpDdwbR6rzpbJfuA0xuw,277
|
436
438
|
squad/run/worker.py,sha256=jtML0h5qKDuSbpJ6_rpWP4MT_rsGA7a24AhwGxBquzk,594
|
437
|
-
squad-1.
|
438
|
-
squad-1.
|
439
|
-
squad-1.
|
440
|
-
squad-1.
|
441
|
-
squad-1.
|
442
|
-
squad-1.
|
439
|
+
squad-1.93.1.dist-info/COPYING,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
|
440
|
+
squad-1.93.1.dist-info/METADATA,sha256=BBKV-R_mmv5k6Tujtg0ARX4GSF0LwWdGvbEerx0OHpw,1280
|
441
|
+
squad-1.93.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
442
|
+
squad-1.93.1.dist-info/entry_points.txt,sha256=J_jG3qnkoOHX4RFNGC0f83eJ4BSvK3pqLFkoF3HWfmA,195
|
443
|
+
squad-1.93.1.dist-info/top_level.txt,sha256=_x9uqE1XppiiytmVTl_qNgpnXus6Gsef69HqfliE7WI,6
|
444
|
+
squad-1.93.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|