squad 1.89__py3-none-any.whl → 1.91__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of squad might be problematic. Click here for more details.

squad/api/ci.py CHANGED
@@ -73,15 +73,26 @@ def submit_job(request, group_slug, project_slug, version, environment_slug):
73
73
  @csrf_exempt
74
74
  @auth_privileged
75
75
  def watch_job(request, group_slug, project_slug, version, environment_slug):
76
+
77
+ # testjob_id points to the backend's test job
78
+ testjob_id = request.POST.get('testjob_id', None)
79
+ if testjob_id is None:
80
+ return HttpResponseBadRequest("testjob_id is required")
81
+
76
82
  backend_name = request.POST.get('backend')
77
83
  if backend_name is None:
78
84
  return HttpResponseBadRequest("backend field is required")
85
+
79
86
  backend = None
80
87
  try:
81
88
  backend = Backend.objects.get(name=request.POST.get('backend'))
82
89
  except Backend.DoesNotExist:
83
90
  return HttpResponseBadRequest("requested backend does not exist")
84
91
 
92
+ check = backend.get_implementation().check_job_id(testjob_id)
93
+ if check is not True:
94
+ return HttpResponseBadRequest(check)
95
+
85
96
  # project has to exist or request will result with 400
86
97
  project = request.project
87
98
  if backend is None or project is None:
@@ -90,12 +101,6 @@ def watch_job(request, group_slug, project_slug, version, environment_slug):
90
101
  # create Build object
91
102
  build, _ = project.builds.get_or_create(version=version)
92
103
 
93
- # testjob_id points to the backend's test job
94
- testjob_id = request.POST.get('testjob_id', None)
95
-
96
- if testjob_id is None:
97
- return HttpResponseBadRequest("testjob_id is required")
98
-
99
104
  # create TestJob object
100
105
  test_job = TestJob(
101
106
  backend=backend,
@@ -0,0 +1,52 @@
1
+ import re
2
+ import requests
3
+ import celery
4
+
5
+ from django.conf import settings
6
+ from django.http import HttpResponse, HttpResponseForbidden
7
+ from django.views.decorators.csrf import csrf_exempt
8
+ from django.views.decorators.http import require_http_methods
9
+
10
+ from squad.http import auth_user_from_request
11
+
12
+
13
+ @csrf_exempt
14
+ @require_http_methods(['GET'])
15
+ def metrics(request):
16
+ user = auth_user_from_request(request, request.user)
17
+ if not user.is_authenticated:
18
+ return HttpResponseForbidden()
19
+
20
+ output = ''
21
+ available_queues = None
22
+
23
+ active_queues = celery.current_app.control.inspect().active_queues()
24
+ if active_queues is not None:
25
+ active_workers = set()
26
+ available_queues = set()
27
+ for worker_name, queues in active_queues.items():
28
+ active_workers.add(worker_name)
29
+ available_queues |= set([q['name'] for q in queues])
30
+
31
+ output += '# TYPE workers_count counter\n'
32
+ output += f'workers_count {len(active_workers)}\n'
33
+
34
+ # TODO: check how to get metrics for non-RabbitMQ brokers
35
+ if settings.CELERY_BROKER_URL:
36
+ rabbitmq_url = settings.CELERY_BROKER_URL.replace('amqps://', 'https://').replace('amqp://', 'http://')
37
+ rabbitmq_url = re.sub(r':\d+$', '', rabbitmq_url)
38
+ rabbitmq_url += '/api/queues'
39
+
40
+ response = requests.get(rabbitmq_url)
41
+ queues = response.json()
42
+ available_queues = {r["queue"] for r in settings.CELERY_TASK_ROUTES.values()}
43
+
44
+ for queue in queues:
45
+ if queue['name'] in available_queues:
46
+ metric_name = f'queue_{queue["name"]}_length'
47
+ length = queue['messages_ready']
48
+
49
+ output += f'\n# TYPE {metric_name} counter'
50
+ output += f'\n{metric_name} {length}'
51
+
52
+ return HttpResponse(output, status=200, content_type="text/plain;")
squad/api/urls.py CHANGED
@@ -5,6 +5,7 @@ from rest_framework.schemas import get_schema_view
5
5
  from . import views
6
6
  from . import data
7
7
  from . import ci
8
+ from . import prometheus
8
9
  from . import rest
9
10
 
10
11
 
@@ -26,4 +27,5 @@ urlpatterns = [
26
27
  url(r'^resubmit/([0-9]+)', ci.resubmit_job),
27
28
  url(r'^forceresubmit/([0-9]+)', ci.force_resubmit_job),
28
29
  url(r'^version/', views.version),
30
+ url(r'^prometheus/', prometheus.metrics),
29
31
  ]
squad/api/views.py CHANGED
@@ -105,7 +105,7 @@ def add_test_run(request, group_slug, project_slug, version, environment_slug):
105
105
  if 'attachment' in request.FILES:
106
106
  attachments = {}
107
107
  for f in request.FILES.getlist('attachment'):
108
- attachments[f.name] = read_file_upload(f)
108
+ attachments[f.name] = f
109
109
  test_run_data['attachments'] = attachments
110
110
 
111
111
  receive = ReceiveTestRun(project)
squad/ci/backend/fake.py CHANGED
@@ -72,6 +72,9 @@ class Backend(object):
72
72
  def check_job_definition(self, definition):
73
73
  return True
74
74
 
75
+ def check_job_id(self, job_id):
76
+ return True
77
+
75
78
  def get_job_definition(self, test_job):
76
79
  return "sample job definition"
77
80
 
squad/ci/backend/lava.py CHANGED
@@ -683,7 +683,7 @@ class Backend(BaseBackend):
683
683
  if clone_measurements_to_tests:
684
684
  res_value = result['result']
685
685
  results.update({res_name: res_value})
686
- elif result['name'] == 'auto-login-action' and handle_lava_boot:
686
+ elif 'login-action' in result['name'] and handle_lava_boot:
687
687
  # add artificial 'boot' test result for each test job
688
688
  # by default the boot test is named after the device_type
689
689
  boot = "boot-%s" % test_job.name
@@ -785,6 +785,11 @@ class Backend(BaseBackend):
785
785
  except yaml.YAMLError as e:
786
786
  return str(e)
787
787
 
788
+ def check_job_id(self, job_id):
789
+ if re.match(r"^\d+$", str(job_id)) is not None:
790
+ return True
791
+ return "LAVA job id should be an integer"
792
+
788
793
  def get_job_definition(self, job_id):
789
794
  if self.use_xml_rpc:
790
795
  return self.proxy.scheduler.jobs.definition(job_id)
squad/ci/backend/null.py CHANGED
@@ -143,6 +143,12 @@ class Backend:
143
143
  """
144
144
  raise NotImplementedError
145
145
 
146
+ def check_job_id(self, job_id):
147
+ """
148
+ Returns True if job id matches what the backend expect, else returns the error message
149
+ """
150
+ raise NotImplementedError
151
+
146
152
  def format_message(self, msg):
147
153
  if self.data and hasattr(self.data, "name"):
148
154
  return self.data.name + ': ' + msg
@@ -120,6 +120,13 @@ class Backend(BaseBackend):
120
120
  # The regex below is supposed to find only one match
121
121
  return matches[0]
122
122
 
123
+ def check_job_id(self, job_id):
124
+ try:
125
+ self.parse_job_id(job_id)
126
+ return True
127
+ except FetchIssue as e:
128
+ return str(e)
129
+
123
130
  def generate_job_id(self, result_type, result):
124
131
  """
125
132
  The job id for TuxSuite results is generated using 3 pieces of info:
@@ -144,7 +151,11 @@ class Backend(BaseBackend):
144
151
  url = reduce(urljoin, urlbits)
145
152
 
146
153
  try:
147
- response = Backend.get_session().request("GET", url)
154
+ headers = {}
155
+ if hasattr(self, 'auth_token') and self.auth_token is not None:
156
+ headers = {'Authorization': self.auth_token}
157
+
158
+ response = Backend.get_session().request("GET", url, headers=headers)
148
159
  except Exception as e:
149
160
  raise TemporaryFetchIssue(f"Can't retrieve from {url}: {e}")
150
161
 
@@ -224,6 +235,11 @@ class Backend(BaseBackend):
224
235
  if 'toolchain' in build_metadata_keys and 'kconfig' in build_metadata_keys and metadata['build_name'] in [None, '']:
225
236
  metadata['build_name'] = self.generate_test_name(build_metadata)
226
237
 
238
+ def add_skip_boot_test(self, tests, metadata):
239
+ # Create an artificial boot test and mark it as skip
240
+ boot_test_name = 'boot/' + (metadata.get('build_name') or 'boot')
241
+ tests[boot_test_name] = None
242
+
227
243
  def parse_build_results(self, test_job, job_url, results, settings):
228
244
  required_keys = ['build_status', 'warnings_count', 'download_url', 'retry']
229
245
  self.__check_required_keys__(required_keys, results)
@@ -318,6 +334,11 @@ class Backend(BaseBackend):
318
334
  metadata_keys = settings.get('TEST_METADATA_KEYS', [])
319
335
  metadata = {k: results.get(k) for k in metadata_keys}
320
336
 
337
+ # Change environment name
338
+ if 'test_name' in results and results.get('test_name') is not None:
339
+ test_job.environment = results.get('test_name')
340
+ test_job.save()
341
+
321
342
  # Add extra metadata from metadata file if it exists
322
343
  self.update_metadata_from_file(results=results, metadata=metadata)
323
344
 
@@ -340,6 +361,8 @@ class Backend(BaseBackend):
340
361
  else:
341
362
  test_job.failure = 'sanity test failed'
342
363
 
364
+ self.add_skip_boot_test(tests, metadata)
365
+
343
366
  return status, completed, metadata, tests, metrics, logs
344
367
 
345
368
  # Fetch results even if the job fails, but has results
@@ -348,8 +371,14 @@ class Backend(BaseBackend):
348
371
 
349
372
  elif results['result'] == 'error':
350
373
  test_job.failure = 'tuxsuite infrastructure error'
374
+ self.add_skip_boot_test(tests, metadata)
351
375
  return 'Incomplete', completed, metadata, tests, metrics, logs
352
376
 
377
+ elif results['result'] == 'canceled':
378
+ test_job.failure = 'tuxsuite job canceled'
379
+ self.add_skip_boot_test(tests, metadata)
380
+ return 'Canceled', completed, metadata, tests, metrics, logs
381
+
353
382
  # If boot result is unkown, a retry is needed, otherwise, it either passed or failed
354
383
  if 'unknown' == results['results']['boot']:
355
384
  return None
@@ -384,6 +413,10 @@ class Backend(BaseBackend):
384
413
 
385
414
  def fetch(self, test_job):
386
415
  url = self.job_url(test_job)
416
+
417
+ settings = self.__resolve_settings__(test_job)
418
+ self.auth_token = settings.get('TUXSUITE_TOKEN', None)
419
+
387
420
  if test_job.input:
388
421
  results = self.fetch_from_results_input(test_job)
389
422
  test_job.input = None
@@ -393,11 +426,12 @@ class Backend(BaseBackend):
393
426
  if results.get('state') != 'finished':
394
427
  return None
395
428
 
396
- settings = self.__resolve_settings__(test_job)
397
-
398
429
  result_type = self.parse_job_id(test_job.job_id)[0]
399
430
  parse_results = getattr(self, f'parse_{result_type.lower()}_results')
400
- return parse_results(test_job, url, results, settings)
431
+ parsed = parse_results(test_job, url, results, settings)
432
+
433
+ self.auth_token = None
434
+ return parsed
401
435
 
402
436
  def job_url(self, test_job):
403
437
  result_type, tux_project, tux_uid = self.parse_job_id(test_job.job_id)
@@ -2,6 +2,7 @@ from glob import glob
2
2
  import os
3
3
  import re
4
4
  from django.core.management.base import BaseCommand
5
+ from django.core.files import File
5
6
 
6
7
 
7
8
  from squad.core.models import Build
@@ -123,7 +124,7 @@ class Command(BaseCommand):
123
124
  for f in glob(os.path.join(directory, '*')):
124
125
  name = os.path.basename(f)
125
126
  if name not in ['metrics.json', 'metadata.json', 'tests.json']:
126
- attachments[name] = open(f, 'rb').read()
127
+ attachments[name] = File(open(f, 'rb'))
127
128
 
128
129
  if not self.options['silent']:
129
130
  print("Importing test run: %s" % directory)
squad/core/models.py CHANGED
@@ -909,8 +909,8 @@ class Attachment(models.Model):
909
909
  self.__data__ = b''
910
910
  return self.__data__
911
911
 
912
- def save_file(self, filename, contents):
913
- storage_save(self, self.storage, filename, contents)
912
+ def save_file(self, filename, file):
913
+ storage_save(self, self.storage, filename, file)
914
914
 
915
915
 
916
916
  class SuiteMetadata(models.Model):
@@ -172,9 +172,9 @@ class ReceiveTestRun(object):
172
172
  if log_file is not None:
173
173
  testrun.save_log_file(log_file)
174
174
 
175
- for filename, data in attachments.items():
176
- attachment = testrun.attachments.create(filename=filename, length=len(data))
177
- attachment.save_file(filename, data)
175
+ for filename, file in attachments.items():
176
+ attachment = testrun.attachments.create(filename=filename, length=file.size)
177
+ attachment.save_file(filename, file)
178
178
 
179
179
  testrun.refresh_from_db()
180
180
 
squad/core/utils.py CHANGED
@@ -169,8 +169,11 @@ def log_deletion(request, object, message):
169
169
 
170
170
 
171
171
  def storage_save(obj, storage_field, filename, content):
172
- content_bytes = content or ''
173
- if type(content_bytes) is str:
174
- content_bytes = content_bytes.encode()
175
172
  filename = '%s/%s/%s' % (obj.__class__.__name__.lower(), obj.pk, filename)
176
- storage_field.save(filename, ContentFile(content_bytes))
173
+ if type(content) in [bytes, str]:
174
+ content_bytes = content or ''
175
+ if type(content_bytes) is str:
176
+ content_bytes = content_bytes.encode()
177
+ storage_field.save(filename, ContentFile(content_bytes))
178
+ else:
179
+ storage_field.save(filename, content)
@@ -6,6 +6,14 @@
6
6
 
7
7
  {% include "squad/project-nav.jinja2" %}
8
8
 
9
+
10
+ {% if project.description%}
11
+ <h2>{{ _('Description') }}</h2>
12
+ <div class='description-{{project.id}}'>
13
+ {{project.description}}
14
+ </div>
15
+ {% endif %}
16
+
9
17
  {% if last_build %}
10
18
  <div>
11
19
  <h2>
squad/frontend/urls.py CHANGED
@@ -52,6 +52,7 @@ urlpatterns = [
52
52
  url(r'^(%s)/(%s)/build/([^/]+)/attachments/testrun/([^/]+)/([^/]+)$' % group_and_project, views.build_attachment, name='build_attachments'),
53
53
  url(r'^(%s)/(%s)/build/([^/]+)/testrun/([^/]+)/suite/([^/]+)/tests/$' % group_and_project, views.test_run_suite_tests, name='testrun_suite_tests'),
54
54
  url(r'^(%s)/(%s)/build/([^/]+)/testrun/([^/]+)/suite/([^/]+)/test/([^/]+)/history/$' % group_and_project, tests.test_history, name='test_history'),
55
+ url(r'^(%s)/(%s)/build/([^/]+)/testrun/([^/]+)/suite/([^/]+)/test/([^/]+)/$' % group_and_project, views.test_run_suite_test_details, name='testrun_suite_test_details'),
55
56
  url(r'^(%s)/(%s)/build/([^/]+)/testrun/([^/]+)/suite/([^/]+)/test/([^/]+)/details/$' % group_and_project, views.test_run_suite_test_details, name='testrun_suite_test_details'),
56
57
  url(r'^(%s)/(%s)/build/([^/]+)/testrun/([^/]+)/suite/([^/]+)/metrics/$' % group_and_project, views.test_run_suite_metrics, name='testrun_suite_metrics'),
57
58
  url(r'^(%s)/(%s)/build/([^/]+)/testrun/([^/]+)/suite/([^/]+)/test/([^/]+)/log$' % group_and_project, views.test_details_log, name='test_details_log'),
@@ -0,0 +1 @@
1
+ from squad.plugins import Plugin # noqa
@@ -0,0 +1,157 @@
1
+ import hashlib
2
+ import re
3
+ from collections import defaultdict
4
+
5
+ from django.template.defaultfilters import slugify
6
+
7
+ REGEX_NAME = 0
8
+ REGEX_BODY = 1
9
+ REGEX_EXTRACT_NAME = 2
10
+
11
+ tstamp = r"\[[ \d]+\.[ \d]+\]"
12
+ pid = r"(?:\s*?\[\s*?[CT]\d+\s*?\])"
13
+ not_newline_or_plus = r"[^\+\n]"
14
+ square_brackets_and_contents = r"\[[^\]]+\]"
15
+
16
+
17
+ class BaseLogParser:
18
+ def compile_regexes(self, regexes):
19
+ combined = [r"(%s)" % r[REGEX_BODY] for r in regexes]
20
+ return re.compile(r"|".join(combined), re.S | re.M)
21
+
22
+ def remove_numbers_and_time(self, snippet):
23
+ # [ 1067.461794][ T132] BUG: KCSAN: data-race in do_page_fault spectre_v4_enable_task_mitigation
24
+ # -> [ .][ T] BUG: KCSAN: data-race in do_page_fault spectre_v_enable_task_mitigation
25
+ without_numbers = re.sub(r"(0x[a-f0-9]+|[<\[][0-9a-f]+?[>\]]|\d+)", "", snippet)
26
+
27
+ # [ .][ T] BUG: KCSAN: data-race in do_page_fault spectre_v_enable_task_mitigation
28
+ # -> BUG: KCSAN: data-race in do_page_fault spectre_v_enable_task_mitigation
29
+ without_time = re.sub(f"^{square_brackets_and_contents}({square_brackets_and_contents})?", "", without_numbers) # noqa
30
+
31
+ return without_time
32
+
33
+ def create_name(self, snippet, compiled_regex=None):
34
+ matches = None
35
+ if compiled_regex:
36
+ matches = compiled_regex.findall(snippet)
37
+ if not matches:
38
+ # Only extract a name if we provide a regex to extract the name and
39
+ # there is a match
40
+ return None
41
+ snippet = matches[0]
42
+ without_numbers_and_time = self.remove_numbers_and_time(snippet)
43
+
44
+ # Limit the name length to 191 characters, since the max name length
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]
48
+
49
+ def create_shasum(self, snippet):
50
+ sha = hashlib.sha256()
51
+ without_numbers_and_time = self.remove_numbers_and_time(snippet)
52
+ sha.update(without_numbers_and_time.encode())
53
+ return sha.hexdigest()
54
+
55
+ def create_name_log_dict(self, test_name, lines, test_regex=None):
56
+ """
57
+ Produce a dictionary with the test names as keys and the extracted logs
58
+ for that test name as values. There will be at least one test name per
59
+ regex. If there were any matches for a given regex, then a new test
60
+ will be generated using test_name + shasum.
61
+ """
62
+ # Run the REGEX_EXTRACT_NAME regex over the log lines to sort them by
63
+ # extracted name. If no name is extracted or the log parser did not
64
+ # have any output for a particular regex, just use the default name
65
+ # (for example "check-kernel-oops").
66
+ tests_without_shas_to_create = defaultdict(set)
67
+ tests_with_shas_to_create = defaultdict(set)
68
+
69
+ # If there are lines, then create the tests for these.
70
+ for line in lines:
71
+ extracted_name = self.create_name(line, test_regex)
72
+ if extracted_name:
73
+ extended_test_name = f"{test_name}-{extracted_name}"
74
+ else:
75
+ extended_test_name = test_name
76
+ tests_without_shas_to_create[extended_test_name].add(line)
77
+
78
+ for name, test_lines in tests_without_shas_to_create.items():
79
+ # Some lines of the matched regex might be the same, and we don't want to create
80
+ # multiple tests like test1-sha1, test1-sha1, etc, so we'll create a set of sha1sums
81
+ # then create only new tests for unique sha's
82
+
83
+ for line in test_lines:
84
+ sha = self.create_shasum(line)
85
+ name_with_sha = f"{name}-{sha}"
86
+ tests_with_shas_to_create[name_with_sha].add(line)
87
+
88
+ return tests_without_shas_to_create, tests_with_shas_to_create
89
+
90
+ def create_squad_tests_from_name_log_dict(
91
+ self, suite, testrun, tests_without_shas_to_create, tests_with_shas_to_create
92
+ ):
93
+ # Import SuiteMetadata from SQUAD only when required so BaseLogParser
94
+ # does not require a SQUAD to work. This makes it easier to reuse this
95
+ # class outside of SQUAD for testing and developing log parser
96
+ # patterns.
97
+ from squad.core.models import SuiteMetadata
98
+
99
+ for name, lines in tests_without_shas_to_create.items():
100
+ metadata, _ = SuiteMetadata.objects.get_or_create(
101
+ suite=suite.slug, name=name, kind="test"
102
+ )
103
+ testrun.tests.create(
104
+ suite=suite,
105
+ result=(len(lines) == 0),
106
+ log="\n".join(lines),
107
+ metadata=metadata,
108
+ build=testrun.build,
109
+ environment=testrun.environment,
110
+ )
111
+ for name_with_sha, lines in tests_with_shas_to_create.items():
112
+ metadata, _ = SuiteMetadata.objects.get_or_create(
113
+ suite=suite.slug, name=name_with_sha, kind="test"
114
+ )
115
+ testrun.tests.create(
116
+ suite=suite,
117
+ result=False,
118
+ log="\n---\n".join(lines),
119
+ metadata=metadata,
120
+ build=testrun.build,
121
+ environment=testrun.environment,
122
+ )
123
+
124
+ def create_squad_tests(self, testrun, suite, test_name, lines, test_regex=None):
125
+ """
126
+ There will be at least one test per regex. If there were any match for
127
+ a given regex, then a new test will be generated using test_name +
128
+ shasum. This helps comparing kernel logs across different builds
129
+ """
130
+ tests_without_shas_to_create, tests_with_shas_to_create = (
131
+ self.create_name_log_dict(test_name, lines, test_regex)
132
+ )
133
+ self.create_squad_tests_from_name_log_dict(
134
+ suite,
135
+ testrun,
136
+ tests_without_shas_to_create,
137
+ tests_with_shas_to_create,
138
+ )
139
+
140
+ def join_matches(self, matches, regexes):
141
+ """
142
+ group regex in python are returned as a list of tuples which each
143
+ group match in one of the positions in the tuple. Example:
144
+ regex = r'(a)|(b)|(c)'
145
+ matches = [
146
+ ('match a', '', ''),
147
+ ('', 'match b', ''),
148
+ ('match a', '', ''),
149
+ ('', '', 'match c')
150
+ ]
151
+ """
152
+ snippets = {regex_id: [] for regex_id in range(len(regexes))}
153
+ for match in matches:
154
+ for regex_id in range(len(regexes)):
155
+ if len(match[regex_id]) > 0:
156
+ snippets[regex_id].append(match[regex_id])
157
+ return snippets
@@ -1,42 +1,33 @@
1
- import hashlib
2
1
  import logging
3
2
  import re
4
- from collections import defaultdict
5
3
  from squad.plugins import Plugin as BasePlugin
6
- from squad.core.models import SuiteMetadata
7
- from django.template.defaultfilters import slugify
8
-
4
+ from squad.plugins.lib.base_log_parser import BaseLogParser, REGEX_NAME, REGEX_EXTRACT_NAME, tstamp, pid, not_newline_or_plus
9
5
 
10
6
  logger = logging.getLogger()
11
7
 
12
- REGEX_NAME = 0
13
- REGEX_BODY = 1
14
- REGEX_EXTRACT_NAME = 2
15
-
16
8
  MULTILINERS = [
17
- ('check-kernel-exception', r'-+\[? cut here \]?-+.*?-+\[? end trace \w* \]?-+', r"\d][^\+\n]*"),
18
- ('check-kernel-kasan', r'=+\n\[[\s\.\d]+\]\s+BUG: KASAN:.*?=+', r"BUG: KASAN:[^\+\n]*"),
19
- ('check-kernel-kfence', r'=+\n\[[\s\.\d]+\]\s+BUG: KFENCE:.*?=+', r"BUG: KFENCE:[^\+\n]*"),
9
+ ('exception', f'-+\[? cut here \]?-+.*?{tstamp}{pid}?\s+-+\[? end trace \w* \]?-+', f"\n{tstamp}{not_newline_or_plus}*"), # noqa
10
+ ('kasan', f'{tstamp}{pid}?\s+=+\n{tstamp}{pid}?\s+BUG: KASAN:.*?\n*?{tstamp}{pid}?\s+=+', f"BUG: KASAN:{not_newline_or_plus}*"), # noqa
11
+ ('kcsan', f'{tstamp}{pid}?\s+=+\n{tstamp}{pid}?\s+BUG: KCSAN:.*?=+', f"BUG: KCSAN:{not_newline_or_plus}*"), # noqa
12
+ ('kfence', f'{tstamp}{pid}?\s+=+\n{tstamp}{pid}?\s+BUG: KFENCE:.*?{tstamp}{pid}?\s+=+', f"BUG: KFENCE:{not_newline_or_plus}*"), # noqa
13
+ ('panic-multiline', f'{tstamp}{pid}?\s+Kernel panic - [^\n]+\n.*?-+\[? end Kernel panic - [^\n]+ \]?-*', f"Kernel {not_newline_or_plus}*"), # noqa
14
+ ('internal-error-oops', f'{tstamp}{pid}?\s+Internal error: Oops.*?-+\[? end trace \w+ \]?-+', f"Oops{not_newline_or_plus}*"), # noqa
20
15
  ]
21
16
 
22
17
  ONELINERS = [
23
- ('check-kernel-oops', r'^[^\n]+Oops(?: -|:).*?$', r"Oops[^\+\n]*"),
24
- ('check-kernel-fault', r'^[^\n]+Unhandled fault.*?$', r"Unhandled [^\+\n]*"),
25
- ('check-kernel-warning', r'^[^\n]+WARNING:.*?$', r"WARNING: [^\+\n]*"),
26
- ('check-kernel-bug', r'^[^\n]+(?: kernel BUG at|BUG:).*?$', r"BUG[^\+\n]*"),
27
- ('check-kernel-invalid-opcode', r'^[^\n]+invalid opcode:.*?$', r"invalid opcode: [^\+\n]*"),
28
- ('check-kernel-panic', r'Kernel panic - not syncing.*?$', r"Kernel [^\+\n]*"),
18
+ ('oops', r'^[^\n]+Oops(?: -|:).*?$', f"Oops{not_newline_or_plus}*"), # noqa
19
+ ('fault', r'^[^\n]+Unhandled fault.*?$', f"Unhandled {not_newline_or_plus}*"), # noqa
20
+ ('warning', r'^[^\n]+WARNING:.*?$', f"WARNING:{not_newline_or_plus}*"), # noqa
21
+ ('bug', r'^[^\n]+(?: kernel BUG at|BUG:).*?$', f"BUG{not_newline_or_plus}*"), # noqa
22
+ ('invalid-opcode', r'^[^\n]+invalid opcode:.*?$', f"invalid opcode:{not_newline_or_plus}*"), # noqa
23
+ ('panic', r'Kernel panic - not syncing.*?$', f"Kernel {not_newline_or_plus}*"), # noqa
29
24
  ]
30
25
 
31
26
  # Tip: broader regexes should come first
32
27
  REGEXES = MULTILINERS + ONELINERS
33
28
 
34
29
 
35
- class Plugin(BasePlugin):
36
- def __compile_regexes(self, regexes):
37
- combined = [r'(%s)' % r[REGEX_BODY] for r in regexes]
38
- return re.compile(r'|'.join(combined), re.S | re.M)
39
-
30
+ class Plugin(BasePlugin, BaseLogParser):
40
31
  def __cutoff_boot_log(self, log):
41
32
  # Attempt to split the log in " login:"
42
33
  logs = log.split(' login:', 1)
@@ -50,112 +41,9 @@ class Plugin(BasePlugin):
50
41
  return boot_log, test_log
51
42
 
52
43
  def __kernel_msgs_only(self, log):
53
- kernel_msgs = re.findall(r'(\[[ \d]+\.[ \d]+\] .*?)$', log, re.S | re.M)
44
+ kernel_msgs = re.findall(f'({tstamp}{pid}? .*?)$', log, re.S | re.M) # noqa
54
45
  return '\n'.join(kernel_msgs)
55
46
 
56
- def __join_matches(self, matches, regexes):
57
- """
58
- group regex in python are returned as a list of tuples which each
59
- group match in one of the positions in the tuple. Example:
60
- regex = r'(a)|(b)|(c)'
61
- matches = [
62
- ('match a', '', ''),
63
- ('', 'match b', ''),
64
- ('match a', '', ''),
65
- ('', '', 'match c')
66
- ]
67
- """
68
- snippets = {regex_id: [] for regex_id in range(len(regexes))}
69
- for match in matches:
70
- for regex_id in range(len(regexes)):
71
- if len(match[regex_id]) > 0:
72
- snippets[regex_id].append(match[regex_id])
73
- return snippets
74
-
75
- def __create_tests(self, testrun, suite, test_name, lines, test_regex=None):
76
- """
77
- There will be at least one test per regex. If there were any match for a given
78
- regex, then a new test will be generated using test_name + shasum. This helps
79
- comparing kernel logs accross different builds
80
- """
81
- # Run the REGEX_EXTRACT_NAME regex over the log lines to sort them by
82
- # extracted name. If no name is extracted or the log parser did not
83
- # have any output for a particular regex, just use the default name
84
- # (for example "check-kernel-oops").
85
- tests_to_create = defaultdict(set)
86
- shas = defaultdict(set)
87
-
88
- # If there are no lines, use the default name and create a passing
89
- # test. For example "check-kernel-oops"
90
- if not lines:
91
- tests_to_create[test_name] = []
92
-
93
- # If there are lines, then create the tests for these.
94
- for line in lines:
95
- extracted_name = self.__create_name(line, test_regex)
96
- if extracted_name:
97
- extended_test_name = f"{test_name}-{extracted_name}"
98
- else:
99
- extended_test_name = test_name
100
- tests_to_create[extended_test_name].add(line)
101
-
102
- for name, lines in tests_to_create.items():
103
- metadata, _ = SuiteMetadata.objects.get_or_create(suite=suite.slug, name=name, kind='test')
104
- testrun.tests.create(
105
- suite=suite,
106
- result=(len(lines) == 0),
107
- log='\n'.join(lines),
108
- metadata=metadata,
109
- build=testrun.build,
110
- environment=testrun.environment,
111
- )
112
-
113
- # Some lines of the matched regex might be the same, and we don't want to create
114
- # multiple tests like test1-sha1, test1-sha1, etc, so we'll create a set of sha1sums
115
- # then create only new tests for unique sha's
116
-
117
- for line in lines:
118
- sha = self.__create_shasum(line)
119
- name_with_sha = f"{name}-{sha}"
120
- shas[name_with_sha].add(line)
121
-
122
- for name_with_sha, lines in shas.items():
123
- metadata, _ = SuiteMetadata.objects.get_or_create(suite=suite.slug, name=name_with_sha, kind='test')
124
- testrun.tests.create(
125
- suite=suite,
126
- result=False,
127
- log='\n---\n'.join(lines),
128
- metadata=metadata,
129
- build=testrun.build,
130
- environment=testrun.environment,
131
- )
132
-
133
- def __remove_numbers_and_time(self, snippet):
134
- without_numbers = re.sub(r"(0x[a-f0-9]+|[<\[][0-9a-f]+?[>\]]|\d+)", "", snippet)
135
- without_time = re.sub(r"^\[[^\]]+\]", "", without_numbers)
136
-
137
- return without_time
138
-
139
- def __create_name(self, snippet, regex=None):
140
- matches = None
141
- if regex:
142
- matches = regex.findall(snippet)
143
- if not matches:
144
- return None
145
- snippet = matches[0]
146
- without_numbers_and_time = self.__remove_numbers_and_time(snippet)
147
-
148
- # Limit the name length to 191 characters, since the max name length
149
- # for SuiteMetadata in SQUAD is 256 characters. The SHA and "-" take 65
150
- # characters: 256-65=191
151
- return slugify(without_numbers_and_time)[:191]
152
-
153
- def __create_shasum(self, snippet):
154
- sha = hashlib.sha256()
155
- without_numbers_and_time = self.__remove_numbers_and_time(snippet)
156
- sha.update(without_numbers_and_time.encode())
157
- return sha.hexdigest()
158
-
159
47
  def postprocess_testrun(self, testrun):
160
48
  if testrun.log_file is None:
161
49
  return
@@ -170,9 +58,9 @@ class Plugin(BasePlugin):
170
58
  log = self.__kernel_msgs_only(log)
171
59
  suite, _ = testrun.build.project.suites.get_or_create(slug=f'log-parser-{log_type}')
172
60
 
173
- regex = self.__compile_regexes(REGEXES)
61
+ regex = self.compile_regexes(REGEXES)
174
62
  matches = regex.findall(log)
175
- snippets = self.__join_matches(matches, REGEXES)
63
+ snippets = self.join_matches(matches, REGEXES)
176
64
 
177
65
  for regex_id in range(len(REGEXES)):
178
66
  test_name = REGEXES[regex_id][REGEX_NAME]
@@ -180,4 +68,4 @@ class Plugin(BasePlugin):
180
68
  test_name_regex = None
181
69
  if regex_pattern:
182
70
  test_name_regex = re.compile(regex_pattern, re.S | re.M)
183
- self.__create_tests(testrun, suite, test_name, snippets[regex_id], test_name_regex)
71
+ self.create_squad_tests(testrun, suite, test_name, snippets[regex_id], test_name_regex)
squad/settings.py CHANGED
@@ -37,17 +37,21 @@ if not os.access(DATA_DIR, os.W_OK):
37
37
  # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
38
38
 
39
39
  # SECURITY WARNING: keep the secret key used in production secret!
40
- secret_key_file = os.getenv('SECRET_KEY_FILE', None)
41
- if secret_key_file is None:
42
- secret_key_file = os.path.join(DATA_DIR, 'secret.dat')
43
-
44
- if not os.path.exists(secret_key_file):
45
- from squad.core.utils import random_key
46
- fd = os.open(secret_key_file, os.O_WRONLY | os.O_CREAT, 0o600)
47
- with os.fdopen(fd, 'w') as f:
48
- f.write(random_key(64))
49
-
50
- SECRET_KEY = open(secret_key_file).read()
40
+ secret_key = os.getenv('SECRET_KEY', None)
41
+ if secret_key:
42
+ SECRET_KEY = secret_key
43
+ else:
44
+ secret_key_file = os.getenv('SECRET_KEY_FILE', None)
45
+ if secret_key_file is None:
46
+ secret_key_file = os.path.join(DATA_DIR, 'secret.dat')
47
+
48
+ if not os.path.exists(secret_key_file):
49
+ from squad.core.utils import random_key
50
+ fd = os.open(secret_key_file, os.O_WRONLY | os.O_CREAT, 0o600)
51
+ with os.fdopen(fd, 'w') as f:
52
+ f.write(random_key(64))
53
+
54
+ SECRET_KEY = open(secret_key_file).read()
51
55
 
52
56
  DEBUG = os.getenv('ENV') not in ['production', 'staging']
53
57
 
@@ -418,6 +422,7 @@ CRISPY_TEMPLATE_PACK = 'bootstrap3'
418
422
 
419
423
  # Sentry support
420
424
  SENTRY_DSN = os.getenv('SENTRY_DSN')
425
+ SENTRY_TRACES_SAMPLE_RATE = os.getenv('SENTRY_TRACES_SAMPLE_RATE', '0')
421
426
  if SENTRY_DSN:
422
427
  try:
423
428
  import sentry_sdk
@@ -428,6 +433,7 @@ if SENTRY_DSN:
428
433
  dsn=SENTRY_DSN,
429
434
  integrations=[DjangoIntegration(), CeleryIntegration()],
430
435
  release='%s@%s' % (os.getenv('ENV', 'squad'), squad_version),
436
+ traces_sample_rate=float(SENTRY_TRACES_SAMPLE_RATE),
431
437
  )
432
438
  except ImportError:
433
439
  pass
squad/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '1.89'
1
+ __version__ = '1.91'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: squad
3
- Version: 1.89
3
+ Version: 1.91
4
4
  Summary: Software Quality Dashboard
5
5
  Home-page: https://github.com/Linaro/squad
6
6
  Author: Antonio Terceiro
@@ -12,26 +12,26 @@ Requires-Dist: aiohttp
12
12
  Requires-Dist: celery
13
13
  Requires-Dist: cryptography
14
14
  Requires-Dist: coreapi
15
- Requires-Dist: django-crispy-forms ==1.14.0
16
- Requires-Dist: Django >=3
15
+ Requires-Dist: django-crispy-forms==1.14.0
16
+ Requires-Dist: Django>=3
17
17
  Requires-Dist: django-allauth
18
18
  Requires-Dist: django-bootstrap3
19
19
  Requires-Dist: django-celery-results
20
20
  Requires-Dist: django-cors-headers
21
21
  Requires-Dist: django-debug-toolbar
22
- Requires-Dist: django-simple-history >3.0
23
- Requires-Dist: django-filter >=2.0
24
- Requires-Dist: djangorestframework >=3.9.2
25
- Requires-Dist: djangorestframework-filters >=1.0.0.dev0
22
+ Requires-Dist: django-simple-history>3.0
23
+ Requires-Dist: django-filter>=2.0
24
+ Requires-Dist: djangorestframework>=3.9.2
25
+ Requires-Dist: djangorestframework-filters>=1.0.0.dev0
26
26
  Requires-Dist: drf-extensions
27
27
  Requires-Dist: future
28
28
  Requires-Dist: gunicorn
29
- Requires-Dist: importlib-metadata >3
30
- Requires-Dist: Jinja2 ==3.0.3
29
+ Requires-Dist: importlib-metadata>3
30
+ Requires-Dist: Jinja2==3.0.3
31
31
  Requires-Dist: Markdown
32
- Requires-Dist: msgpack >=0.5.0
32
+ Requires-Dist: msgpack>=0.5.0
33
33
  Requires-Dist: python-dateutil
34
- Requires-Dist: PyYAML >=5.1
34
+ Requires-Dist: PyYAML>=5.1
35
35
  Requires-Dist: PyJWT
36
36
  Requires-Dist: pyzmq
37
37
  Requires-Dist: requests
@@ -40,6 +40,7 @@ Requires-Dist: sqlparse
40
40
  Requires-Dist: svgwrite
41
41
  Requires-Dist: whitenoise
42
42
  Provides-Extra: postgres
43
- Requires-Dist: psycopg2 ; extra == 'postgres'
43
+ Requires-Dist: psycopg2-binary; extra == "postgres"
44
44
 
45
45
  Software Quality Dashboard
46
+
@@ -7,20 +7,21 @@ squad/http.py,sha256=KuIKtpf3yOvf5fwc0T2MR0ul1l4AKxq3b0CLdk6KBhM,3667
7
7
  squad/jinja2.py,sha256=OKX-lzNz6qtTZL56HWv4UBMPuBl4WQXv0qFJztGp9zs,2541
8
8
  squad/mail.py,sha256=xH5wuIpD7u1fTN9vNOcbzByojleaffsKwp-9i3BeOD0,390
9
9
  squad/manage.py,sha256=Z-LXT67p0R-IzwJ9fLIAacEZmU0VUjqDOSg7j2ZSxJ4,1437
10
- squad/settings.py,sha256=CRmnXFDrfdspzXGUIlffRcKMQizJ4Wzyyg1cIRu-h-M,14535
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=DHemnt4WEI99t3epDIiaS6j6mPKaFVroxzlr9RNdMCU,21
13
+ squad/version.py,sha256=S8qXLXebPToWZIRCvA5VIWk_5c_MVZR49-YdwQ0ypzc,21
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
17
- squad/api/ci.py,sha256=ymG-eMKXpJgrVUiZcqJW-dYZQKvm1LkdR3TUMe4OSoM,6943
17
+ squad/api/ci.py,sha256=QjGIhSpm8gmIjH4Nd2NAWtJItSVleg3QOLxBU_p9h1E,7082
18
18
  squad/api/data.py,sha256=obKDV0-neEvj5lPF9VED2gy_hpfhGtLJABYvSY38ing,2379
19
19
  squad/api/filters.py,sha256=Zvp8DCJmiNquFWqvfVseEAAMYYPiT95RUjqKdzcqSnw,6917
20
+ squad/api/prometheus.py,sha256=0usJgOz14g1a71sdfjM-cOC8IGXkpE-5-TqpvJj-Oyk,1840
20
21
  squad/api/rest.py,sha256=ZtbK0c1BLPPnsX79XlKFVYONM_VJ0vacWZ2JsdCd4l0,77342
21
- squad/api/urls.py,sha256=rmsdaL1uOCVSZ5x1redup9RliICmijaBjRK5ObsTkG8,1343
22
+ squad/api/urls.py,sha256=c-o27_RP0ynOtxuyRKUl274fFMWWrzoii31Mr2saxSQ,1414
22
23
  squad/api/utils.py,sha256=Sa8QFId3_oSqD2UOoY3Kuh54LLDLPNMq2sub5ktd6Fs,1160
23
- squad/api/views.py,sha256=kuFlbiyZiD0i9jwwmkL3Y22LwJ3bx2oJs28d1g2DPA0,3898
24
+ squad/api/views.py,sha256=WH4c10e7iRmuL5tWDxG4zEFHzvF5hxDpEVvybfvbc_E,3880
24
25
  squad/ci/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
26
  squad/ci/admin.py,sha256=7yB-6F0cvt0NVvzGOTlZCyGPV_YHarmbKJZTTzataT4,2255
26
27
  squad/ci/apps.py,sha256=6OVnzTdJkxdqEJnKWYE9dZgUcc29_T1LrDw41cK4EQk,139
@@ -29,10 +30,10 @@ squad/ci/models.py,sha256=Fm-4b3SDgMh9HXzqjOd4iZDRMJ1D9AnZ2cg7i2OR248,16018
29
30
  squad/ci/tasks.py,sha256=P0NYjLuyUViTpO1jZMuRVREbFDCccrMCZDw5E4pt928,3882
30
31
  squad/ci/utils.py,sha256=38zHpw8xkZDSFlkG-2BwSK6AkcddK9OkN9LXuQ3SHR0,97
31
32
  squad/ci/backend/__init__.py,sha256=yhpotXT9F4IdAOXvGQ3-17eOHAFwoaqf9SnMX17ab30,534
32
- squad/ci/backend/fake.py,sha256=9sPKndsGd5GDNPp35v-zfJWXZCbge-yXH3RBQGgTlPk,2340
33
- squad/ci/backend/lava.py,sha256=E4QE0XtAiqArzzx3YSv7_2qYUBs4aSw8JOz0AV0z9W8,33877
34
- squad/ci/backend/null.py,sha256=0CVylWELIZw3JyzCROB4XXAjgQUi15YjQz5caRfTNBo,5434
35
- squad/ci/backend/tuxsuite.py,sha256=-A4p5HpUWnIC-61os0vdJOfAGoO81szoLkSgzyaUt6c,17901
33
+ squad/ci/backend/fake.py,sha256=7Rl-JXnBYThDomOBzBsN9XuVkSjSHTZjtZOURdowZbA,2397
34
+ squad/ci/backend/lava.py,sha256=WeOJJNxv42geGf3Y6r-I0WnhWinxpSSgZAFAwfkiXGY,34039
35
+ squad/ci/backend/null.py,sha256=htEd4NbrXLKdPgFfTS0Ixm8PdT6Ghat3BCYi2zjfuv0,5624
36
+ squad/ci/backend/tuxsuite.py,sha256=HTYLyJvtraHnkMKOjYix66bq1QV4m8bamNBahV5SZZw,19129
36
37
  squad/ci/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
38
  squad/ci/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
39
  squad/ci/management/commands/create_tuxsuite_boot_tests.py,sha256=JvjNusebLX71eyz9d-kaeCyekYSpzc1eXoeIqWK9ygo,4045
@@ -81,12 +82,12 @@ squad/core/comparison.py,sha256=LR3-Unv0CTmakFCDzF_h8fm2peTJzkv79mQWNau1iwI,2442
81
82
  squad/core/data.py,sha256=2zw56v7iYRTUc7wlhuUNgwIIMmK2w84hi-amR9J7EPU,2236
82
83
  squad/core/failures.py,sha256=X6lJVghM2fOrd-RfuHeLlezW2pt7owDZ8eX-Kn_Qrt0,918
83
84
  squad/core/history.py,sha256=QRSIoDOw6R6vUWMtsPMknsHGM7FaCAeuCYqASCayHTk,3541
84
- squad/core/models.py,sha256=jAQpsCo4uNPF2f93LI-_Wfpu5TjUhHFf6LfwXA8grU0,60859
85
+ squad/core/models.py,sha256=sXQmgPtl54IZT7rDmJEU3QK6JSPbi0hTUGRsjwL6PIo,60851
85
86
  squad/core/notification.py,sha256=rOpO6F63w7_5l9gQgWBBEk-MFBjp7x_hVzoVIVyDze0,10030
86
87
  squad/core/plugins.py,sha256=FLgyoXXKnPBYEf2MgHup9M017rHuADHivLhgzmx_cJE,6354
87
88
  squad/core/queries.py,sha256=78fhIJZWXIlDryewYAt96beK1VJad66Ufu8cg3dHh4w,7698
88
89
  squad/core/statistics.py,sha256=xyTHuhdBjcJ4AozZESjTzSD3dBmmCDgLpbg5XpeyO_M,1056
89
- squad/core/utils.py,sha256=jinGlLEnr8nL84OzBvm0VFGt6EzWWR0r4dOabDrbXfM,4908
90
+ squad/core/utils.py,sha256=HwCq8SsKJHbBUtF4DZt1iWCuWhqZaHRBn--Yh0O_RH4,5018
90
91
  squad/core/locale/django.pot,sha256=XycSJyEaEpozGBS9zu7QTNQbffZC0D9eSJ-AwXaVZx4,2282
91
92
  squad/core/locale/es_MX/LC_MESSAGES/django.po,sha256=bwvTWHK2KOT6zFqbIYh61_xYqRnMaQECZsMsOvNdMNw,3071
92
93
  squad/core/locale/pl/LC_MESSAGES/django.po,sha256=mI-Vo8OKWCcx4PrsoB6GiPY3lYU55tSqh0sO6fUeK2Y,3111
@@ -98,7 +99,7 @@ squad/core/management/commands/compute_build_summaries.py,sha256=dz6-3vXtFNGYOzl
98
99
  squad/core/management/commands/compute_project_statuses.py,sha256=qcm71zEP_A-XhNWrDHM55TJSgKUk_oWjewuZEu2B2KM,3134
99
100
  squad/core/management/commands/fill_test_metadata.py,sha256=EG2mqKtThY5D7nnGalM3q0XOPEVDiDnFLV7sw7YSz1U,1326
100
101
  squad/core/management/commands/fix_squadplugin_data.py,sha256=cbjPL_-AvazBsmXKd5x6LpaoP-3MGpa3uoUUxljVzdw,5072
101
- squad/core/management/commands/import_data.py,sha256=cuQchMuyMj3UhXvpx-pbMmqdDIwUKxyBp26IiCZGamY,4522
102
+ squad/core/management/commands/import_data.py,sha256=KgSTNtrQQiqzqjJdvKDHbU6IExPsdTbdMJ-yqfZY4Y4,4556
102
103
  squad/core/management/commands/import_data.rst,sha256=79tAcJ6hOVRVzW2iheQuO6o2RHZKbbFtsHM-IEr6490,1444
103
104
  squad/core/management/commands/migrate_test_runs.py,sha256=RHV06tb4gWyv_q-ooC821_QGZi0WGwxjIYaUGTboqfI,4214
104
105
  squad/core/management/commands/populate_metric_build_and_environment.py,sha256=DJP9_YLRso0RiERBVsB0GP4-GaiRtJb0rAiUQDfFNQk,3166
@@ -277,7 +278,7 @@ squad/core/migrations/0167_add_project_datetime.py,sha256=VUBG-qsAhh2f2NXaHOqfX9
277
278
  squad/core/migrations/0168_add_group_settings.py,sha256=5UdylfMMNavTL0KXkjPSiEMhSisGWXbhUXQSzfK29Ck,462
278
279
  squad/core/migrations/0169_userpreferences.py,sha256=FwYv9RWxMWdQ2lXJMgi-Xc6XBB5Kp-_YTAOr9GVq1To,1098
279
280
  squad/core/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
280
- squad/core/tasks/__init__.py,sha256=QoQPjAJhN8Evqb4uaUYdxqGojRTvc572abQH9N1o9y4,18567
281
+ squad/core/tasks/__init__.py,sha256=pYbEkFzNaat7iQQretRiJQPPF4Sq-5-hBykJYnBM04g,18567
281
282
  squad/core/tasks/exceptions.py,sha256=n4cbmJFBdA6KWsGiTbfN9DyYGbJpk0DjR0UneEYw_W0,931
282
283
  squad/core/tasks/notification.py,sha256=6ZyTbUQZPITPP-4r9MUON7x-NbwvDBG8YeabM6fsjzA,4915
283
284
  squad/core/templates/squad/notification/base.jinja2,sha256=AbtQioEHV5DJBW4Etsu0-DQXd_8tQCnLejzgbDGDW7s,3413
@@ -305,7 +306,7 @@ squad/frontend/project_settings.py,sha256=TtWz8h8Goeb3pccLy9jLUibeHqyqkdK8phL7_V
305
306
  squad/frontend/queries.py,sha256=NxQF2woAf9A4Wk_ozHzZXOGmr2as-j7hqfvmsfJ-ojc,967
306
307
  squad/frontend/setup.py,sha256=NF9VunY1HJGB2HsHJss-go7EGmqr__JASddxiBCvmeQ,169
307
308
  squad/frontend/tests.py,sha256=PidrjaToK_Cks0s9Mc4i3Vh4UXOWoXTZlpnxQ2wWjHY,8740
308
- squad/frontend/urls.py,sha256=biWauxwXR5j9kOfrSUqkv1Iqz-elB2aNViS9_UFoLzQ,4882
309
+ squad/frontend/urls.py,sha256=-rxbsUlMyxldzoVKiVAOMAREqU8SOipy4CqBTlULuMQ,5055
309
310
  squad/frontend/user_settings.py,sha256=U_i59iuylg98uH98K4ezPa2NY56idslBhn7MS6FguHQ,4976
310
311
  squad/frontend/utils.py,sha256=DeH58CJUI1dovpQrj3a-DcxNzM0cxsnBDOF0mrC4Qws,1364
311
312
  squad/frontend/views.py,sha256=4ld9n8pOS35sUKPifgCY4rLHL_Dmybfo_Jg0_Bo5sxs,27031
@@ -387,7 +388,7 @@ squad/frontend/templates/squad/knownissues.jinja2,sha256=RdQZ2AKgV97953eIuP-4IwN
387
388
  squad/frontend/templates/squad/login.jinja2,sha256=NPp20MpmgoGxWOschCUxcZMJKdnkVhUmy8kpCgogPs0,2775
388
389
  squad/frontend/templates/squad/metrics.jinja2,sha256=7oFBkTiGi3k1UtfR5x0RS961u6rsRCe_YcEXklA0iLA,3277
389
390
  squad/frontend/templates/squad/project-nav.jinja2,sha256=AHN7r5TMvJ-NwEo_u3vlJg34J1njsuII32SgQuTfiwA,1526
390
- squad/frontend/templates/squad/project.jinja2,sha256=JUmiYPlAo5hlUd-M32QmyjWmqe49NILySVFQ2a39Ftc,737
391
+ squad/frontend/templates/squad/project.jinja2,sha256=k2orc5C6Fxp_74utQAN1sa6XPOFMF2sC2D2y671pgSg,887
391
392
  squad/frontend/templates/squad/test_history.jinja2,sha256=g_pHD4yQdfXK1D8-tTAPG4yzoqbDVUYm6ml2hANffp8,5869
392
393
  squad/frontend/templates/squad/test_run.jinja2,sha256=smxFEC7XnHu28Wj7iC2WQrGjpuPiqsxASpflbyYGG_A,1176
393
394
  squad/frontend/templates/squad/test_run_suite_metrics.jinja2,sha256=WGjlObw7ZTGoomTmON0O2QRHHdmEBOYf9xMSTWP83F4,1780
@@ -425,15 +426,17 @@ squad/plugins/__init__.py,sha256=9BSzy2jFIoDpWlhD7odPPrLdW4CC3btBhdFCvB651dM,152
425
426
  squad/plugins/example.py,sha256=BKpwd315lHRIuNXJPteibpwfnI6C5eXYHYdFYBtVmsI,89
426
427
  squad/plugins/gerrit.py,sha256=CqO2KnFQzu9utr_TQ-sGr1wg3ln0B-bS2-c0_i8T5-c,7009
427
428
  squad/plugins/github.py,sha256=pdtLZw_7xNuzkaFvY_zWi0f2rsMlalXjKm7sz0eADz4,2429
428
- squad/plugins/linux_log_parser.py,sha256=8GgIM4skM06uZX1d-15nZ0D4lxg5jOp9XYRvMtUtjA0,7195
429
+ squad/plugins/linux_log_parser.py,sha256=WrDbyfupEcP1-E4ke9wjHiddio8sD5BFuEtF4AH0aXA,3274
430
+ squad/plugins/lib/__init__.py,sha256=jzazbAvp2_ibblAs0cKZrmo9aR2EL3hKLyRDE008r2I,40
431
+ squad/plugins/lib/base_log_parser.py,sha256=OW6JkZ3PM5RiDkt9UZ7OFFpUIArCxFUaqovynzwBL1Y,6573
429
432
  squad/run/__init__.py,sha256=ssE8GPAGFiK6V0WpZYowav6Zqsd63dfDMMYasNa1sQg,1410
430
433
  squad/run/__main__.py,sha256=DOl8JOi4Yg7DdtwnUeGqtYBJ6P2k-D2psAEuYOjWr8w,66
431
434
  squad/run/listener.py,sha256=jBeOQhPGb4EdIREB1QsCzYuumsfJ-TqJPd3nR-0m59g,200
432
435
  squad/run/scheduler.py,sha256=CDJG3q5C0GuQuxwlMOfWTSSJpDdwbR6rzpbJfuA0xuw,277
433
436
  squad/run/worker.py,sha256=jtML0h5qKDuSbpJ6_rpWP4MT_rsGA7a24AhwGxBquzk,594
434
- squad-1.89.dist-info/COPYING,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
435
- squad-1.89.dist-info/METADATA,sha256=Kb687CzcwB57kqLaoR1vJ9H6ESKXgDx5UToU4yOcZFg,1281
436
- squad-1.89.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
437
- squad-1.89.dist-info/entry_points.txt,sha256=apCDQydHZtvqV334ql6NhTJUAJeZRdtAm0TVcbbAi5Q,194
438
- squad-1.89.dist-info/top_level.txt,sha256=_x9uqE1XppiiytmVTl_qNgpnXus6Gsef69HqfliE7WI,6
439
- squad-1.89.dist-info/RECORD,,
437
+ squad-1.91.dist-info/COPYING,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
438
+ squad-1.91.dist-info/METADATA,sha256=U074123n6U3T9v0BY5GTaFCynFxFjL4HA7GEoLQjXX4,1278
439
+ squad-1.91.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
440
+ squad-1.91.dist-info/entry_points.txt,sha256=J_jG3qnkoOHX4RFNGC0f83eJ4BSvK3pqLFkoF3HWfmA,195
441
+ squad-1.91.dist-info/top_level.txt,sha256=_x9uqE1XppiiytmVTl_qNgpnXus6Gsef69HqfliE7WI,6
442
+ squad-1.91.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (71.1.0)
2
+ Generator: bdist_wheel (0.44.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -4,3 +4,4 @@ squad-admin = squad.manage:main
4
4
  squad-listener = squad.run.listener:main
5
5
  squad-scheduler = squad.run.scheduler:main
6
6
  squad-worker = squad.run.worker:main
7
+
File without changes