Skip to content

Commit 02026c0

Browse files
committed
test: guard class-tree serializer against per-node language N+1
Add class-tree N+1 guards (OWL via BRO, SKOS via SKOS-TEST) asserting serialization query fan-out stays sub-linear in node count -- catching a regression of the get_languages per-node naturalLanguage bring. Uses a stopgap counter (GooQueryCountSpy) since the de-fork SPARQL query-counting framework is not yet on the goo branch this app tracks; marked to be replaced by Goo::TestHelpers.assert_sparql_queries. #304
1 parent 1c316d2 commit 02026c0

3 files changed

Lines changed: 112 additions & 0 deletions

File tree

test/models/test_class.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,46 @@ def test_bro_tree_has_children
296296
assert checked > 0, "expected at least one node with a child-count aggregate"
297297
end
298298

299+
# N+1 guard for the class-tree endpoint (OWL). Serializing a tree must resolve
300+
# the submission's languages once, not once per node -- before the get_languages
301+
# `bring?` guard (ncbo/ontologies_linked_data#302) serialization issued ~one
302+
# naturalLanguage query per serialized node. Asserts serialization query
303+
# fan-out stays sub-linear in node count so the N+1 cannot silently return.
304+
def test_bro_tree_serialize_no_n_plus_1
305+
if !LinkedData::Models::Ontology.find("BROTEST123").first
306+
submission_parse("BROTEST123", "SOME BROTEST Bla", "./test/data/ontology_files/BRO_v3.2.owl", 123,
307+
process_rdf: true, index_search: false,
308+
run_metrics: false, reasoning: true)
309+
end
310+
os = LinkedData::Models::Ontology.find("BROTEST123").first.latest_submission(status: [:rdf])
311+
# The hypermedia links serialized per node need submission.ontology.acronym,
312+
# exactly as the controller has it loaded; preload so serialization runs.
313+
os.bring(:ontology) if os.bring?(:ontology)
314+
os.ontology.bring(:acronym) if os.ontology.bring?(:acronym)
315+
316+
statistical_Text_Analysis = "http://bioontology.org/ontologies/BiomedicalResourceOntology.owl#Statistical_Text_Analysis"
317+
display_attrs = [:prefLabel, :hasChildren, :children, :obsolete, :subClassOf]
318+
cls = LinkedData::Models::Class.find(RDF::URI.new(statistical_Text_Analysis)).in(os).include(display_attrs).first
319+
tree_root = cls.tree
320+
321+
node_count = 0
322+
stack = [tree_root]
323+
until stack.empty?
324+
n = stack.pop
325+
node_count += 1
326+
stack.concat(n.children)
327+
end
328+
assert_operator node_count, :>=, 3, "tree too small to be a meaningful N+1 guard"
329+
330+
serialize_queries = count_sparql_queries do
331+
LinkedData::Serializers::JSON.serialize([tree_root], only: display_attrs)
332+
end
333+
334+
assert_operator serialize_queries, :<, node_count,
335+
"serialization issued #{serialize_queries} SPARQL queries for #{node_count} tree nodes -- " \
336+
"looks like a per-node N+1 (see get_languages guard in ncbo/ontologies_linked_data#302)"
337+
end
338+
299339

300340
def test_include_ancestors
301341
if !LinkedData::Models::Ontology.find("BROTEST123").first

test/models/test_ontology_common.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,38 @@
11
require_relative "../test_case"
22
require 'rack'
33

4+
# STOPGAP query counter for N+1 regression guards.
5+
#
6+
# TODO(de-fork): replace with Goo::TestHelpers.assert_sparql_queries once the
7+
# de-fork SPARQL query-counting framework lands on the goo branch this app
8+
# tracks (ncbo/goo `development`). That helper does not exist there yet, so we
9+
# count Goo::SPARQL::Client#query invocations directly. Inert unless a counting
10+
# block armed the thread-local, so it adds nothing in normal runs.
11+
module GooQueryCountSpy
12+
def query(*args, **kwargs, &blk)
13+
c = Thread.current[:test_goo_query_count]
14+
Thread.current[:test_goo_query_count] = c + 1 unless c.nil?
15+
super
16+
end
17+
end
18+
unless Goo::SPARQL::Client.ancestors.include?(GooQueryCountSpy)
19+
Goo::SPARQL::Client.prepend(GooQueryCountSpy)
20+
end
21+
422
module LinkedData
523
class TestOntologyCommon < LinkedData::TestCase
624

25+
# Count Goo SPARQL queries (cache hits + store round-trips) issued by the
26+
# block. Counts query fan-out -- what an N+1 inflates -- independent of
27+
# cache state. See GooQueryCountSpy above.
28+
def count_sparql_queries
29+
Thread.current[:test_goo_query_count] = 0
30+
yield
31+
Thread.current[:test_goo_query_count]
32+
ensure
33+
Thread.current[:test_goo_query_count] = nil
34+
end
35+
736
def create_count_mapping
837
count = LinkedData::Models::MappingCount.where.all.length
938
unless count > 2

test/models/test_skos_submission.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,5 +182,48 @@ def test_skos_class_tree
182182

183183
assert seen_target, 'target class not found within its own tree'
184184
end
185+
186+
# N+1 guard for the class-tree endpoint (SKOS). Same contract as the OWL guard
187+
# in test_class.rb: serializing a tree must resolve the submission's languages
188+
# once, not once per node (get_languages `bring?` guard,
189+
# ncbo/ontologies_linked_data#302). Asserts serialization query fan-out stays
190+
# sub-linear in node count.
191+
def test_skos_tree_serialize_no_n_plus_1
192+
sub = before_suite
193+
sub.bring(:ontology) if sub.bring?(:ontology)
194+
sub.ontology.bring(:acronym) if sub.ontology.bring?(:acronym)
195+
196+
roots = sub.roots
197+
target = nil
198+
roots.each do |r|
199+
LinkedData::Models::Class.in(sub).models([r]).include(children: [:prefLabel]).all
200+
next if r.children.empty?
201+
202+
target = LinkedData::Models::Class.find(r.children.first.id).in(sub).first
203+
break
204+
end
205+
refute_nil target, 'expected a SKOS root with at least one child'
206+
207+
display_attrs = [:prefLabel, :hasChildren, :children, :obsolete, :subClassOf] +
208+
LinkedData::Models::Class.concept_is_in_attributes
209+
tree_root = target.tree
210+
211+
node_count = 0
212+
stack = [tree_root]
213+
until stack.empty?
214+
n = stack.pop
215+
node_count += 1
216+
stack.concat(n.children)
217+
end
218+
assert_operator node_count, :>=, 3, 'tree too small to be a meaningful N+1 guard'
219+
220+
serialize_queries = count_sparql_queries do
221+
LinkedData::Serializers::JSON.serialize([tree_root], only: display_attrs)
222+
end
223+
224+
assert_operator serialize_queries, :<, node_count,
225+
"serialization issued #{serialize_queries} SPARQL queries for #{node_count} tree nodes -- " \
226+
"looks like a per-node N+1 (see get_languages guard in ncbo/ontologies_linked_data#302)"
227+
end
185228
end
186229

0 commit comments

Comments
 (0)