Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions python/tests/test_workspace_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,110 @@ def test_forged_cell_link_and_oversized_tag_sets_fail_closed(self):
for raw in ('limit=true','limit=1.5','limit=-1','limit=1&limit=2','limit=%D9%A1'):
with self.subTest(raw=raw):self.assertEqual(self.client.get('/api/annotation-tags?'+raw).status_code,400)

def test_protocol_local_tag_filter_matches_inherited_and_direct_tags_across_views_and_export(self):
base = '/api/protocols/' + self.service.protocol_id
original = copy.deepcopy(self.service.query_result(self.service.protocol_id))
self.edit('cell', self.cell, ['selected'])
self.edit('epoch', self.second, ['other'])
summary = self.client.get(base + '?tag=selected').get_json()
self.assertEqual(summary['counts']['epochs'], 1)
self.assertEqual([cell['cell_uuid'] for cell in summary['cells']], [self.cell])
page = self.client.get(base + '/epochs?tag=selected').get_json()
self.assertEqual([row['epoch_uuid'] for row in page['epochs']], [self.first])
tree = self.client.get(base + '/tree?tag=selected&splits=cell')
self.assertEqual(tree.status_code, 200, tree.get_json())
self.assertEqual(tree.get_json()['count'], 1)
self.assertEqual(tree.get_json()['children'][0]['epoch_uuids'], [self.first])
fields = self.client.get(base + '/tree-fields?tag=selected')
self.assertEqual(fields.status_code, 200, fields.get_json())
paged = self.client.post('/api/tree-pages', json={
'protocol_uuid': self.service.protocol_id, 'filters': {'tag': 'selected'},
'splits': 'cell'}, headers=self.headers).get_json()
self.assertEqual(paged['total_epochs'], 1)
leaf = self.client.post('/api/tree-pages', json={
'protocol_uuid': self.service.protocol_id, 'filters': {'tag': 'selected'},
'splits': 'cell', 'path': paged['branches'][0]['path'],
'revision': paged['revision']}, headers=self.headers).get_json()
self.assertEqual([row['epoch_uuid'] for row in leaf['epochs']], [self.first])
self.assertEqual(self.client.get(base + '/epochs?tagged=true').get_json()['total'], 2)
self.assertEqual(self.client.get(base + '/epochs?tag=Selected').get_json()['total'], 0)
exported = self.client.post(base + '/exports', json={
'query_revision': summary['query_revision'], 'filters': {'tag': 'selected'}}, headers=self.headers)
self.assertEqual(exported.status_code, 201, exported.get_json())
response = self.client.get(exported.get_json()['download_url'])
package = json.loads(response.data); response.close()
self.assertEqual([row['epoch_uuid'] for row in package['epochs']], [self.first])
self.assertEqual(package['recipe']['options']['filters'], {'tag': 'selected'})
self.assertEqual(self.service.query_result(self.service.protocol_id), original)

def test_protocol_tag_predicate_all_any_and_negation_keep_tree_and_exports_consistent(self):
base = '/api/protocols/' + self.service.protocol_id
original = copy.deepcopy(self.service.query_result(self.service.protocol_id))
self.edit('cell', self.cell, ['good'])
self.edit('epoch', self.first, ['inspect'])
self.edit('epoch', self.second, ['reject'])
def rule(tag, field='annotations/effective/tags'):
return {'field': field, 'operator': 'contains', 'value': tag}
cases = [({'all': [rule('good'), rule('inspect'), {'not': rule('reject')}]}, [self.first]),
({'any': [rule('good'), rule('reject')]}, [self.first, self.second]),
({'all': [rule('good', 'annotations/epoch/tags')]}, []),
({'all': [{'not': rule('reject')}]}, [self.first])]
for predicate, expected in cases:
with self.subTest(predicate=predicate):
filters = {'tag_predicate': json.dumps(predicate)}
response = self.client.get(base, query_string=filters)
self.assertEqual(response.status_code, 200, response.get_json())
summary = response.get_json()
self.assertEqual(summary['counts']['epochs'], len(expected))
page = self.client.get(base + '/epochs', query_string=filters).get_json()
self.assertEqual([row['epoch_uuid'] for row in page['epochs']], expected)
tree = self.client.get(base + '/tree', query_string={**filters, 'splits': 'cell'}).get_json()
self.assertEqual(tree['count'], len(expected))
paged = self.client.post('/api/tree-pages', json={
'protocol_uuid': self.service.protocol_id, 'filters': filters,
'splits': 'cell'}, headers=self.headers).get_json()
self.assertEqual(paged['total_epochs'], len(expected))
if expected:
exported = self.client.post(base + '/exports', json={
'query_revision': summary['query_revision'], 'filters': filters}, headers=self.headers)
self.assertEqual(exported.status_code, 201, exported.get_json())
download = self.client.get(exported.get_json()['download_url'])
package = json.loads(download.data); download.close()
self.assertEqual([row['epoch_uuid'] for row in package['epochs']], expected)
self.assertEqual(self.service.query_result(self.service.protocol_id), original)

def test_protocol_tag_predicate_rejects_non_tag_fields_and_invalid_ast(self):
base = '/api/protocols/' + self.service.protocol_id
predicates = ['{', 'null', json.dumps({'field': 'parameters/example', 'operator': 'eq', 'value': 1}),
json.dumps({'field': 'annotations/effective/tags', 'operator': 'contains', 'value': 3}),
json.dumps({'field': 'annotations/effective/tags', 'operator': 'unknown', 'value': 'x'}),
json.dumps({'all': [], 'extra': True}), json.dumps({'not': {'all': []}, 'extra': True})]
for predicate in predicates:
with self.subTest(predicate=predicate):
filters = {'tag_predicate': predicate}
self.assertEqual(self.client.get(base + '/epochs', query_string=filters).status_code, 400)
self.assertEqual(self.client.post('/api/tree-pages', json={
'protocol_uuid': self.service.protocol_id, 'filters': filters}, headers=self.headers).status_code, 400)

def test_tag_filter_tree_fields_refresh_after_annotation_change_and_reject_malformed_filters(self):
base = '/api/protocols/' + self.service.protocol_id
self.edit('epoch', self.first, ['selected'])
before = self.service._tree_fields(self.service.protocol_id, {'tag': 'selected'})
self.edit('epoch', self.first, remove=['selected'], revision=1)
self.edit('epoch', self.second, ['selected'])
after = self.service._tree_fields(self.service.protocol_id, {'tag': 'selected'})
self.assertNotEqual(before[1], after[1])
self.assertEqual([row['epoch_uuid'] for row in self.service.filtered_rows(
self.service.protocol_id, {'tag': 'selected'})], [self.second])
for query in ('tagged=false', 'tagged=1', 'tag=', 'tag=%20selected', 'tag=selected%0A', 'tag=selected&tag=other', 'tagged=true&tagged=false'):
with self.subTest(query=query):
self.assertEqual(self.client.get(base + '/epochs?' + query).status_code, 400)
for filters in ({'tag': True}, {'tagged': True}, {'tag': ['selected']}, {'tagged': ''}):
with self.subTest(filters=filters):
response = self.client.post('/api/tree-pages', json={
'protocol_uuid': self.service.protocol_id, 'filters': filters}, headers=self.headers)
self.assertEqual(response.status_code, 400)

def test_protocol_export_freezes_annotations_and_stale_tags_reject_old_revision(self):
base='/api/protocols/'+self.service.protocol_id
previous=self.client.get(base).get_json()['query_revision']
Expand Down
21 changes: 21 additions & 0 deletions python/tests/test_workspace_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,27 @@ def test_export_saves_verified_dynamic_tree_order(self):
self.assertEqual(saved['recipe']['options']['split_order'], 'cell, parameters/frequencyCutoff, block')
self.assertEqual({row['uuid'] for row in saved['recipe']['epochs']}, set(self.service.ids))

def test_focused_epoch_export_freezes_exact_uuid_with_repeated_numbers(self):
key = self.service.ids[1] # Both fixture rows are Epoch 1.
response = self.client.post(self.base + '/exports', json={
'query_revision': self.revision(), 'filters': {'epoch_uuid': key},
'review_policy': 'include_unreviewed'}, headers=self.headers)
self.assertEqual(response.status_code, 201, response.get_json())
saved = self.store.get_dataset_revision(response.get_json()['dataset_uuid'])
self.assertEqual([row['uuid'] for row in saved['recipe']['epochs']], [key])
self.assertEqual(response.get_json()['epoch_count'], 1)

def test_focused_export_rejects_excluded_and_out_of_view_without_artifacts(self):
initial_exports = set((Path(self.temp.name) / 'exports').rglob('*'))
key = self.service.ids[0]
self.client.post(self.base + '/curation', json=self.curation_body({'included': False}, [key]), headers=self.headers)
for filters in ({'epoch_uuid': key}, {'epoch_uuid': self.service.ids[1], 'cell_uuid': self.service.cell_ids[0]}):
response = self.client.post(self.base + '/exports', json={
'query_revision': self.revision(), 'filters': filters}, headers=self.headers)
self.assertEqual(response.status_code, 400, response.get_json())
self.assertFalse(self.datasets.rows)
self.assertEqual(set((Path(self.temp.name) / 'exports').rglob('*')), initial_exports)

def test_selection_export_defaults_to_optional_review_and_links_exact_members(self):
counts = self.client.get(self.base).get_json()['counts']
self.assertEqual((counts['exportable'], counts['approved_exportable']), (2, 0))
Expand Down
14 changes: 14 additions & 0 deletions python/tests/test_workspace_candidate_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ def export(self,candidate,format='reference-json',**options):
return self.client.post('/api/explore/revisions/'+candidate['revision_uuid']+'/exports',json={
'format':format,'expected_recipe_sha256':candidate['recipe']['full_recipe_sha256'],**options},headers=self.headers)

def test_focused_search_epoch_uuid_intersects_query_and_exports_one_row(self):
key = self.service.ids[1] # Both rows have the same block epoch number.
candidate = self.save({'all': [
{'field': 'protocol', 'operator': 'eq', 'value': 'example'},
{'field': 'epoch', 'operator': 'eq', 'value': key}]})
self.assertEqual(candidate['recipe']['epoch_count'], 1)
response = self.export(candidate, 'wheeler-sqlite')
self.assertEqual(response.status_code, 201, response.get_json())
saved = self.store.get_dataset_revision(response.get_json()['dataset_uuid'])
self.assertEqual([row['uuid'] for row in saved['recipe']['epochs']], [key])
self.assertEqual(response.get_json()['epoch_count'], 1)
self.assertFalse(self.case.curation.rows)
self.assertFalse(self.case.protocol_bindings.rows)

def test_sqlite_exact_candidate_ignores_unrelated_protocol_masks_and_keeps_history(self):
key=self.service.ids[0]
self.store.update(self.service.protocol_id,[key],{'included':False,'tags_add':['protocol-only']},
Expand Down
6 changes: 5 additions & 1 deletion python/tests/test_workspace_catalog_optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ def legacy_predicate_catalog(catalog, values):
class CatalogOptimizationTests(unittest.TestCase):
def test_catalog_and_values_match_preoptimization_fixture_exactly(self):
catalog, values = tree.catalog(*fixture())
digest = hashlib.sha256(json.dumps([catalog, values], sort_keys=True, allow_nan=False).encode()).hexdigest()
# Epoch UUID is a new identity field; existing scientific fields retain the historical oracle.
self.assertEqual({key: current['epoch'] for key, current in values.items()}, {key: key for key in values})
legacy_catalog = {**catalog, 'fields': [field for field in catalog['fields'] if field['id'] != 'epoch']}
legacy_values = {key: {field: value for field, value in current.items() if field != 'epoch'} for key, current in values.items()}
digest = hashlib.sha256(json.dumps([legacy_catalog, legacy_values], sort_keys=True, allow_nan=False).encode()).hexdigest()
# Captured from the unoptimized implementation before this change.
self.assertEqual(digest, '97d7c1e6971e4a563f4caaafb4f0d3d241404a45a23a2279847bc8fe17377df1')
fields = {item['id']: item for item in catalog['fields']}
Expand Down
4 changes: 3 additions & 1 deletion python/tests/test_workspace_disk_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ def test_predicate_types_missing_and_scoped_not(self):
catalog,values=tree.catalog(self.rows,self.details,sources=self.sources)
self.assertEqual(index.predicate_catalog(),predicates.predicate_catalog(catalog,values))
tests=[{'field':'parameters/a~1b~0','operator':op,**({'value':value} if op not in predicates.UNARY else {})} for op,value in [('eq',1),('eq',True),('eq',[0,675]),('ne',1),('contains',1),('exists',None),('missing',None),('is_null',None)]]
tests += [{'field':'epoch','operator':'eq','value':'epoch-1'}]
self.assertEqual(index.match({'field':'epoch','operator':'eq','value':'epoch-1'})[1], ['epoch-1'])
tests += [{'not':{'field':'parameters/sometimes','operator':'eq','value':2}}, {'all':[]},{'any':[]},{'all':[{'field':'parameters/frequencyCutoff','operator':'gte','value':100},{'any':[{'field':'parameters/sometimes','operator':'missing'},{'field':'parameters/sometimes','operator':'is_null'}]}]}]
for predicate in tests:
expected=predicates.evaluate(predicate,catalog,values)
Expand Down Expand Up @@ -96,7 +98,7 @@ def test_empty_and_persisted_summaries(self):
self.assertIsNotNone(opened._catalog_cache)
self.assertIsNotNone(opened._predicate_cache)
mutated=opened.catalog();mutated['fields'].clear()
self.assertEqual(len(opened.catalog()['fields']),8)
self.assertEqual(len(opened.catalog()['fields']),len(tree.BASE_FIELDS))
def test_truncated_choices_and_numeric_identity_exact(self):
rows,details,sources=fixture(160)
sequence=[1,1.0,True,-0.0,0.0,0,None,2**60,2**60+1,float(2**60)]+list(range(100))+[1.0,49,99]*16
Expand Down
10 changes: 7 additions & 3 deletions python/workspace_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,12 @@ def create_app(project_dir, retinanalysis_dir, *, service=None, store=None, expl
app.extensions["curation_store"] = store

def filters(allowed=()):
if set(request.args) - {"cell_uuid", "cell_type", "group_label", "offset", "limit", "splits"} - set(allowed):
if any(len(request.args.getlist(key)) != 1 for key in ('tag', 'tagged', 'tag_predicate') if key in request.args):
raise ValueError('Tag filters must be specified once')
if set(request.args) - {"epoch_uuid", "cell_uuid", "cell_type", "group_label", "tag", "tagged", "tag_predicate", "offset", "limit", "splits"} - set(allowed):
raise ValueError("Unknown query filter; no unrestricted fallback was applied")
return {key: request.args[key] for key in ("cell_uuid", "cell_type", "group_label")
if request.args.get(key)}
return {key: request.args[key] for key in ("epoch_uuid", "cell_uuid", "cell_type", "group_label", "tag", "tagged", "tag_predicate")
if key in request.args}

def state(protocol_uuid):
result = service.query_result(protocol_uuid)
Expand Down Expand Up @@ -1045,6 +1047,8 @@ def export(protocol_uuid):
field_catalog = {field["id"]: field for field in service.tree_fields(protocol_uuid, splits=split_order)["fields"]}
rows = service.filtered_rows(protocol_uuid, query_filters)
included = [r["epoch_uuid"] for r in rows if curation[r["epoch_uuid"]]["included"]]
if 'epoch_uuid' in query_filters and included != [str(uuid.UUID(query_filters['epoch_uuid']))]:
raise ValueError('The focused epoch is excluded or no longer matches this protocol view')
approved = [key for key, value in curation.items() if value["review_state"] == "approved"]
name = str(body.get("name") or definition["name"]).strip()[:120]
recipe = prepare_export(snapshot, included, destination=export_format,
Expand Down
4 changes: 2 additions & 2 deletions python/workspace_disk_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import workspace_tree as tree
import workspace_predicates as predicates

FORMAT = 1
FORMAT = 2 # Epoch identity is now an indexed predicate field.
CHUNK_SIZE = 256
CACHE_SIZE = 64

Expand Down Expand Up @@ -432,7 +432,7 @@ def _suggest(fields,index,ids,family):
for field in fields:
field.update(suggested_rank=None,suggestion_reason='')
count=field['recorded_distinct_count'];high=count>24 or count>max(5,len(ids)*.25);field['high_cardinality']=high
if not field['varying'] or high or field.get('grouping_role')=='technical' or field['id'] in {'block','block time','group'}: continue
if not field['varying'] or high or field.get('grouping_role')=='technical' or field['id'] in {'epoch','block','block time','group'}: continue
if field['id'].rsplit('/',1)[-1].lower() in {'seed','uuid','id'}: continue
candidates.append(field)
candidates.sort(key=lambda f:(priority.get(f['id'],20 if f['category']=='Parameters' else 30),not f.get('varies_within_cell',False),f['missing_count'],f['recorded_distinct_count'],f['id']))
Expand Down
Loading
Loading