Skip to content

Commit 24a4148

Browse files
committed
more source fixes
1 parent 481828f commit 24a4148

7 files changed

Lines changed: 38 additions & 39 deletions

File tree

codespeed/admin.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,17 @@ class EnvironmentAdmin(admin.ModelAdmin):
8585

8686
@admin.register(Result)
8787
class ResultAdmin(admin.ModelAdmin):
88-
list_display = ('revision', 'benchmark', 'executable', 'environment',
89-
'value', 'date')
90-
list_filter = ('environment', 'executable', 'date', 'benchmark')
88+
list_display = ('revision', 'benchmark', 'source', 'executable',
89+
'environment', 'value', 'date')
90+
list_filter = ('environment', 'executable', 'date', 'benchmark__source',
91+
'benchmark')
92+
list_select_related = ('revision', 'benchmark', 'executable', 'environment')
9193
raw_id_fields = ('revision', 'benchmark', 'executable', 'environment')
9294

95+
@admin.display(ordering='benchmark__source', description='Source')
96+
def source(self, obj):
97+
return obj.benchmark.source
98+
9399

94100
def recalculate_report(modeladmin, request, queryset):
95101
for report in queryset:

codespeed/management/commands/import_sqlite_dump.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from datetime import datetime
1313

1414
from django.core.management.base import BaseCommand, CommandError
15-
from django.db import connection
15+
from django.db import connection, transaction
1616

1717
# Columns that are stored as 0/1 integers in SQLite but need Python bools
1818
# for PostgreSQL's boolean type.
@@ -66,7 +66,7 @@ def handle(self, *args, **options):
6666

6767
src.row_factory = sqlite3.Row
6868

69-
with connection.cursor() as cur:
69+
with transaction.atomic(), connection.cursor() as cur:
7070
for table in _TABLES:
7171
bool_cols = _BOOL_COLS.get(table, set())
7272
dt_cols = _DT_COLS.get(table, set())

codespeed/models.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -196,17 +196,9 @@ class Benchmark(models.Model):
196196
"Default on comparison page", default=True)
197197

198198
class Meta:
199-
# The same benchmark name can exist in more than one suite
200-
# (e.g. 'nbody' in both the legacy and pyperformance suites);
201-
# source is part of the identity so results don't get merged.
202199
unique_together = (('name', 'source'),)
203200

204201
def ident(self):
205-
"""Stable identifier used in timeline URLs/permalinks.
206-
207-
A bare name (no ``.source`` suffix) is treated as 'legacy' when
208-
parsed back, so old ``?ben=<name>`` permalinks keep working.
209-
"""
210202
return "%s.%s" % (self.name, self.source)
211203

212204
def __str__(self):

codespeed/results.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212

1313
logger = logging.getLogger(__name__)
1414

15+
# failed runs are recorded with a huge value; anything this big is bogus
16+
SENTINEL_THRESHOLD = 1000000
17+
1518

1619
def validate_result(item):
1720
"""
@@ -38,12 +41,18 @@ def validate_result(item):
3841
elif key in item and item[key] == "":
3942
return 'Value for key "' + key + '" empty in request', error
4043

41-
# source is optional but, when given, must be a known suite. It is part
42-
# of the Benchmark identity, so an unvalidated value would silently
43-
# create a bogus benchmark row via get_or_create.
4444
if 'source' in item and item['source'] not in dict(Benchmark.S_TYPES):
4545
return 'Invalid source "%s"' % item['source'], error
4646

47+
try:
48+
result_value = float(item['result_value'])
49+
except (TypeError, ValueError):
50+
return 'Value for key "result_value" is not a number', error
51+
if result_value >= SENTINEL_THRESHOLD:
52+
return ('Result value %s rejected: at or above the failed-run '
53+
'sentinel threshold (%s)' % (
54+
item['result_value'], SENTINEL_THRESHOLD), error)
55+
4756
# Check that the Environment exists
4857
try:
4958
e = Environment.objects.get(name=item['environment'])
@@ -64,8 +73,6 @@ def save_result(data, update_repo=True):
6473
p, created = Project.objects.get_or_create(name=data["project"])
6574
branch, created = Branch.objects.get_or_create(name=data["branch"],
6675
project=p)
67-
# source is part of the benchmark identity: the same name in a different
68-
# suite is a distinct benchmark, so results are never merged across suites.
6976
source = data.get("source", "legacy")
7077
b, created = Benchmark.objects.get_or_create(
7178
name=data["benchmark"], source=source)

codespeed/tests/test_views.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,7 @@ def test_source_defaults_to_legacy(self):
197197
self.assertEqual(b.source, 'legacy')
198198

199199
def test_same_name_different_source_are_distinct(self):
200-
"""The same name in a different suite is a separate Benchmark, so
201-
results are not merged across suites."""
200+
"""The same name in a different suite is a separate Benchmark"""
202201
self.client.post(self.path, self.data)
203202
modified_data = copy.deepcopy(self.data)
204203
modified_data['source'] = 'pyperformance'
@@ -207,7 +206,6 @@ def test_same_name_different_source_are_distinct(self):
207206
legacy = Benchmark.objects.get(name='float', source='legacy')
208207
pyperf = Benchmark.objects.get(name='float', source='pyperformance')
209208
self.assertNotEqual(legacy.pk, pyperf.pk)
210-
# each benchmark owns its own result, nothing merged onto the other
211209
self.assertEqual(legacy.results.count(), 1)
212210
self.assertEqual(pyperf.results.count(), 1)
213211

@@ -219,6 +217,14 @@ def test_invalid_source_rejected(self):
219217
self.assertEqual(response.status_code, 400)
220218
self.assertFalse(Benchmark.objects.filter(name='float').exists())
221219

220+
def test_sentinel_value_rejected(self):
221+
"""A failed-run sentinel value is rejected"""
222+
modified_data = copy.deepcopy(self.data)
223+
modified_data['result_value'] = 1000000
224+
response = self.client.post(self.path, modified_data)
225+
self.assertEqual(response.status_code, 400)
226+
self.assertFalse(Result.objects.filter(benchmark__name='float').exists())
227+
222228

223229
@override_settings(ALLOW_ANONYMOUS_POST=True)
224230
class TestAddJSONResults(TestCase):

codespeed/views.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -388,9 +388,9 @@ def comparison(request):
388388
units = unit_qs[0].units
389389
lessisbetter = (unit_qs[0].lessisbetter and
390390
' (less is better)' or ' (more is better)')
391-
bench_units[unit] = [
392-
[b.id for b in unit_qs], lessisbetter, units
393-
]
391+
# a units_title (e.g. 'Time') can span sources: accumulate, don't overwrite
392+
entry = bench_units.setdefault(unit, [[], lessisbetter, units])
393+
entry[0].extend(b.id for b in unit_qs)
394394
checkedbenchmarks = []
395395
if 'ben' in data:
396396
checkedbenchmarks = _parse_ben_param(data['ben'])
@@ -717,8 +717,7 @@ def timeline(request):
717717
baseline = getbaselineexecutables()
718718
defaultbaseline = None
719719
if len(baseline) > 1:
720-
# must match the option keys built in getbaselineexecutables()
721-
# ("<exe.id>:<rev.id>"), which gettimelinedata splits on ":"
720+
# must match the "<exe.id>:<rev.id>" option keys from getbaselineexecutables()
722721
defaultbaseline = str(baseline[1]['executable'].id) + ":"
723722
defaultbaseline += str(baseline[1]['revision'].id)
724723
if "base" in data and data['base'] != "undefined":
@@ -739,8 +738,6 @@ def timeline(request):
739738
lastrevisions.append(revs_int)
740739
defaultlast = revs_int
741740

742-
# order by source so the timeline sidebar can {% regroup %} into
743-
# per-suite accordion sections
744741
benchmarks = Benchmark.objects.all().order_by('source', 'name')
745742

746743
defaultbenchmark = "grid"
@@ -792,8 +789,6 @@ def timeline(request):
792789
for proj in Project.objects.filter(track=True):
793790
executables[proj] = Executable.objects.filter(project=proj)
794791
use_median_bands = hasattr(settings, 'USE_MEDIAN_BANDS') and settings.USE_MEDIAN_BANDS
795-
# The radio buttons carry 'name.source' idents, so the JS default must
796-
# match that form (the 'grid'/'show_none' sentinels are passed through).
797792
if isinstance(defaultbenchmark, Benchmark):
798793
defaultbenchmark_value = defaultbenchmark.ident()
799794
else:
@@ -937,8 +932,7 @@ def changes(request):
937932
baseline = getbaselineexecutables()
938933
defaultbaseline = "none"
939934
if len(baseline) > 1:
940-
# must match the "<exe.id>:<rev.id>" option keys from
941-
# getbaselineexecutables()
935+
# must match the "<exe.id>:<rev.id>" option keys from getbaselineexecutables()
942936
defaultbaseline = str(baseline[1]['executable'].id) + ":"
943937
defaultbaseline += str(baseline[1]['revision'].id)
944938
if "base" in data and data['base'] != "undefined":

codespeed/views_data.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,7 @@
1111

1212

1313
def parse_benchmark_ident(ben):
14-
"""Split a timeline ``ben`` value into (name, source).
15-
16-
Accepts ``<name>.<source>`` (e.g. 'nbody.pyperformance') and, for
17-
backwards compatibility, a bare ``<name>`` which defaults to the
18-
'legacy' source. Benchmark names may themselves contain dots, so only
19-
a trailing segment that is a known source slug is treated as the source.
20-
"""
14+
"""Split a '<name>.<source>' (or bare '<name>') into (name, source='legacy')."""
2115
name, _, suffix = ben.rpartition('.')
2216
if name and suffix in dict(Benchmark.S_TYPES):
2317
return name, suffix

0 commit comments

Comments
 (0)