earl-report 0.3.6 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: db739ef98aaf6d8ce76e793057d5cdabdfaee7a9
4
- data.tar.gz: b5a224f7c5af327cfbea8d7d947a374b512f6a6a
3
+ metadata.gz: 255dbe2a1d649a13c1f35e23d6c3dbcd2105a3e3
4
+ data.tar.gz: 37c3002143ac3de614a84664311c4dc57e888469
5
5
  SHA512:
6
- metadata.gz: 5556e589606ff22f62440de4e5981b87c495a3446ce38ace08e1c4879b020b648fa058cc68c5a30816154682b86cde5f7e95d89947f1546ff3cc76ae999a69f0
7
- data.tar.gz: 315cb136dd11906ee1ceaa693b400fb10acd0062c82d8815f19a8e7cff41ffee9fe364e89443a74d381d1e4c0f9360c9eaf49dcb6ac060848926d4d478ae46a5
6
+ metadata.gz: ad90655184326e3d6cc2a1c754d7bdc707fe41939b69da97727e1b0066b64f76cd8353341636da863bc60340d4107963f222039e52dd86ecbd2f2c09b0c77966
7
+ data.tar.gz: 47a6100932b129080d1c8182522937fa6616d30a817ee5e29b8fea4a1ede40bdcd80a0b568353bbc22548948fd6c30ee823594bcf0672301c7f3be4be00f8356
data/README.md CHANGED
@@ -48,40 +48,59 @@ of the following form:
48
48
  If not found, the IRI identified by `doap:developer`
49
49
  will be dereferenced and is presumed to provide a [FOAF]() profile of the developer.
50
50
 
51
+ Assertions are added to each test entry based on that test being referenced from the assertion.
52
+
51
53
  ## Manifest query
52
- The test manifest is used to generate `earl:TestCase` entries for each test
53
- described in the test manifest. It will also summarize each test, including
54
- any input and result files associated with the tests. The built-in query
55
- is based on the [standard RDF WG format](). Alternative manifest formats
56
- can be used by specifying a customized manifest query. The default query
57
- is the following:
58
-
59
- PREFIX dc: <http://purl.org/dc/terms/>
54
+ The test manifest is used to find test entries and a manifest. The built-in
55
+ query is based on the [standard RDF WG format](). Alternative manifest formats
56
+ can be used by specifying a customized manifest query, but may require a custom
57
+ [Haml]() template for report generation. The default query is the following:
58
+
60
59
  PREFIX mf: <http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#>
61
60
  PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
62
- PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
63
61
 
64
- SELECT ?lh ?uri ?type ?title ?description ?testAction ?testResult ?manUri ?manTitle ?manDescription
62
+ SELECT ?uri ?testAction ?manUri
65
63
  WHERE {
66
- ?uri a ?type;
67
- mf:name ?title;
68
- mf:action ?testAction .
69
- OPTIONAL { ?uri rdfs:comment ?description . }
70
- OPTIONAL { ?uri mf:result ?testResult . }
64
+ ?uri mf:action ?testAction .
71
65
  OPTIONAL {
72
66
  ?manUri a mf:Manifest; mf:entries ?lh .
73
67
  ?lh rdf:first ?uri .
74
- OPTIONAL { ?manUri mf:name ?manTitle . }
75
- OPTIONAL { ?manUri rdfs:comment ?manDescription . }
76
68
  }
77
69
  }
78
70
 
79
- If any result has a non-null `?lh`, it is taken as the list head and used
80
- to maintain the list order within `earl:tests`.
81
-
82
71
  ## Report generation template
83
72
  The report template is in [ReSpec][] form using [Haml]() to generate individual report elements.
84
73
 
74
+ ## Changes from previous versions
75
+ Version 0.3 and prior re-constructed the test manifest used to create the body of the report, which caused information not described within the query to be lost. Starting with 0.4, all manifests and assertions are read into a single graph, and each test references a list of assertions against it using a list referenced by `earl:assertions`. Additionally, all included manifests are included in a top-level entity referenced via `mf:entries`. For example:
76
+
77
+ <> a earl:Software, doap:Project;
78
+ mf:entries (<http://example/manifest.ttl>);
79
+ earl:assertions (<spec/test-files/report-complete.ttl>) .
80
+
81
+ <http://example/manifest.ttl> a mf:Manifest, earl:Report;
82
+ mf:name "Example Test Cases";
83
+ rdfs:comment "Description for Example Test Cases";
84
+ mf:entries (<http://example/manifest.ttl#testeval00>) .
85
+
86
+ <http://example/manifest.ttl#testeval00> a earl:TestCriterion, earl:TestCase;
87
+ mf:name "subm-test-00";
88
+ rdfs:comment "Blank subject";
89
+ mf:action <http://example/test-00.ttl>;
90
+ mf:result <http://example/test-00.out>;
91
+ earl:assertions ([
92
+ a earl:Assertion;
93
+ earl:assertedBy <http://greggkellogg.net/foaf#me>;
94
+ earl:mode earl:automatic;
95
+ earl:result [
96
+ a earl:TestResult;
97
+ dc:date "2012-11-06T19:23:29-08:00"^^xsd:dateTime;
98
+ earl:outcome earl:passed
99
+ ];
100
+ earl:subject <http://rubygems.org/gems/rdf-turtle>;
101
+ earl:test <http://example/manifest.ttl#testeval00>
102
+ ]) .
103
+
85
104
  ## Usage
86
105
  The `earl-report` command may be used to directly create a report from zero or more input files, which are themselves [EARL][] report.
87
106
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.6
1
+ 0.4.0
data/bin/earl-report CHANGED
@@ -9,7 +9,7 @@ require 'yaml'
9
9
  OPT_ARGS = [
10
10
  ["--base", GetoptLong::REQUIRED_ARGUMENT,"Base URI to use when loading test manifest"],
11
11
  ["--bibRef", GetoptLong::REQUIRED_ARGUMENT,"ReSpec BibRef of specification being reported upon"],
12
- ["--format", "-f", GetoptLong::REQUIRED_ARGUMENT,"Format of output, one of 'ttl', 'json', or 'html'"],
12
+ ["--format", "-f", GetoptLong::REQUIRED_ARGUMENT,"Format of output, one of 'ttl', 'json', or 'html'. May also be a different RDF format"],
13
13
  ["--json", GetoptLong::NO_ARGUMENT, "Input is a JSON-LD formatted result"],
14
14
  ["--manifest", GetoptLong::REQUIRED_ARGUMENT,"Test manifest(s)"],
15
15
  ["--name", GetoptLong::REQUIRED_ARGUMENT,"Name of specification"],
@@ -30,6 +30,8 @@ def usage
30
30
  Options are initialized by reading optional run-control file '.earl' in the local directory,
31
31
  if it exists.
32
32
 
33
+ Writing with a format other than ttl, json, or html will also write any loaded manifests
34
+
33
35
  Usage: #{$0} [options] test-result ...
34
36
  }.gsub(/^ /, '')
35
37
  width = OPT_ARGS.map do |o|
data/lib/earl_report.rb CHANGED
@@ -12,26 +12,19 @@ class EarlReport
12
12
 
13
13
  attr_reader :graph
14
14
 
15
- # Return information about each test, and for the first test in the
16
- # manifest, about the manifest itself
15
+ # Return information about each test.
16
+ # Tests all have an mf:action property.
17
+ # The Manifest lists all actions in list from mf:entries
17
18
  MANIFEST_QUERY = %(
18
- PREFIX dc: <http://purl.org/dc/terms/>
19
19
  PREFIX mf: <http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#>
20
20
  PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
21
- PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
22
21
 
23
- SELECT ?lh ?uri ?type ?title ?description ?testAction ?testResult ?manUri ?manTitle ?manDescription
22
+ SELECT ?uri ?testAction ?manUri
24
23
  WHERE {
25
- ?uri a ?type;
26
- mf:name ?title;
27
- mf:action ?testAction .
28
- OPTIONAL { ?uri rdfs:comment|dc:description ?description . }
29
- OPTIONAL { ?uri mf:result ?testResult . }
24
+ ?uri mf:action ?testAction .
30
25
  OPTIONAL {
31
26
  ?manUri a mf:Manifest; mf:entries ?lh .
32
27
  ?lh rdf:first ?uri .
33
- OPTIONAL { ?manUri rdfs:label|mf:name ?manTitle . }
34
- OPTIONAL { ?manUri rdfs:comment|dc:description ?manDescription . }
35
28
  }
36
29
  }
37
30
  ).freeze
@@ -39,7 +32,7 @@ class EarlReport
39
32
  TEST_SUBJECT_QUERY = %(
40
33
  PREFIX doap: <http://usefulinc.com/ns/doap#>
41
34
  PREFIX foaf: <http://xmlns.com/foaf/0.1/>
42
-
35
+
43
36
  SELECT DISTINCT ?uri ?name ?doapDesc ?homepage ?language ?developer ?devName ?devType ?devHomepage
44
37
  WHERE {
45
38
  ?uri a doap:Project; doap:name ?name; doap:developer ?developer .
@@ -50,6 +43,7 @@ class EarlReport
50
43
  OPTIONAL { ?developer foaf:name ?devName .}
51
44
  OPTIONAL { ?developer foaf:homepage ?devHomepage .}
52
45
  }
46
+ ORDER BY ?name
53
47
  ).freeze
54
48
 
55
49
  DOAP_QUERY = %(
@@ -68,7 +62,7 @@ class EarlReport
68
62
  ASSERTION_QUERY = %(
69
63
  PREFIX earl: <http://www.w3.org/ns/earl#>
70
64
 
71
- SELECT ?by ?mode ?outcome ?subject ?test
65
+ SELECT ?test ?subject ?by ?mode ?outcome
72
66
  WHERE {
73
67
  ?a a earl:Assertion;
74
68
  earl:assertedBy ?by;
@@ -82,40 +76,69 @@ class EarlReport
82
76
  ORDER BY ?subject
83
77
  ).freeze
84
78
 
85
- TEST_CONTEXT = {
86
- "@vocab" => "http://www.w3.org/ns/earl#",
87
- "foaf:homepage" => {"@type" => "@id"},
88
- dc: "http://purl.org/dc/terms/",
89
- doap: "http://usefulinc.com/ns/doap#",
90
- earl: "http://www.w3.org/ns/earl#",
91
- mf: "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#",
92
- foaf: "http://xmlns.com/foaf/0.1/",
93
- rdfs: "http://www.w3.org/2000/01/rdf-schema#",
94
- assertedBy: {"@type" => "@id"},
95
- assertions: {"@type" => "@id", "@container" => "@list"},
96
- bibRef: {"@id" => "dc:bibliographicCitation"},
97
- created: {"@id" => "doap:created", "@type" => "xsd:date"},
98
- description: {"@id" => "dc:description", "@language" => "en"},
99
- developer: {"@id" => "doap:developer", "@type" => "@id", "@container" => "@set"},
100
- doapDesc: {"@id" => "doap:description", "@language" => "en"},
101
- generatedBy: {"@type" => "@id"},
102
- homepage: {"@id" => "doap:homepage", "@type" => "@id"},
103
- label: {"@id" => "rdfs:label", "@language" => "en"},
104
- language: {"@id" => "doap:programming-language"},
105
- license: {"@id" => "doap:license", "@type" => "@id"},
106
- mode: {"@type" => "@id"},
107
- name: {"@id" => "doap:name"},
108
- outcome: {"@type" => "@id"},
109
- release: {"@id" => "doap:release", "@type" => "@id"},
110
- shortdesc: {"@id" => "doap:shortdesc", "@language" => "en"},
111
- subject: {"@type" => "@id"},
112
- test: {"@type" => "@id"},
113
- testAction: {"@id" => "mf:action", "@type" => "@id"},
114
- testResult: {"@id" => "mf:result", "@type" => "@id"},
115
- entries: {"@id" => "mf:entries", "@type" => "@id", "@container" => "@list"},
116
- testSubjects: {"@type" => "@id", "@container" => "@list"},
117
- title: {"@id" => "dc:title"},
118
- xsd: {"@id" => "http://www.w3.org/2001/XMLSchema#"}
79
+ TEST_FRAME = {
80
+ "@context" => {
81
+ "@vocab" => "http://www.w3.org/ns/earl#",
82
+ "foaf:homepage" => {"@type" => "@id"},
83
+ "dc" => "http://purl.org/dc/terms/",
84
+ "doap" => "http://usefulinc.com/ns/doap#",
85
+ "earl" => "http://www.w3.org/ns/earl#",
86
+ "mf" => "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#",
87
+ "foaf" => "http://xmlns.com/foaf/0.1/",
88
+ "rdfs" => "http://www.w3.org/2000/01/rdf-schema#",
89
+ "assertedBy" => {"@type" => "@id"},
90
+ "assertions" => {"@type" => "@id", "@container" => "@set"},
91
+ "bibRef" => {"@id" => "dc:bibliographicCitation"},
92
+ "created" => {"@id" => "doap:created", "@type" => "xsd:date"},
93
+ "description" => {"@id" => "rdfs:comment", "@language" => "en"},
94
+ "developer" => {"@id" => "doap:developer", "@type" => "@id", "@container" => "@set"},
95
+ "doapDesc" => {"@id" => "doap:description", "@language" => "en"},
96
+ "generatedBy" => {"@type" => "@id"},
97
+ "homepage" => {"@id" => "doap:homepage", "@type" => "@id"},
98
+ "language" => {"@id" => "doap:programming-language"},
99
+ "license" => {"@id" => "doap:license", "@type" => "@id"},
100
+ "mode" => {"@type" => "@id"},
101
+ "name" => {"@id" => "doap:name"},
102
+ "outcome" => {"@type" => "@id"},
103
+ "release" => {"@id" => "doap:release", "@type" => "@id"},
104
+ "revision" => {"@id" => "doap:revision"},
105
+ "shortdesc" => {"@id" => "doap:shortdesc", "@language" => "en"},
106
+ "subject" => {"@type" => "@id"},
107
+ "test" => {"@type" => "@id"},
108
+ "testAction" => {"@id" => "mf:action", "@type" => "@id"},
109
+ "testResult" => {"@id" => "mf:result", "@type" => "@id"},
110
+ "title" => {"@id" => "mf:name"},
111
+ "entries" => {"@id" => "mf:entries", "@type" => "@id", "@container" => "@list"},
112
+ "testSubjects" => {"@type" => "@id", "@container" => "@set"},
113
+ "xsd" => {"@id" => "http://www.w3.org/2001/XMLSchema#"}
114
+ },
115
+ "assertions" => {},
116
+ "bibRef" => {},
117
+ "generatedBy" => {
118
+ "@embed" => "@always",
119
+ "developer" => {},
120
+ "release" => {
121
+ "@embed" => "@always"
122
+ }
123
+ },
124
+ "testSubjects" => {
125
+ "@embed" => "@always",
126
+ "@type" => "earl:TestSubject",
127
+ "developer" => {"@embed" => "@always"},
128
+ "homepage" => {"@embed" => false}
129
+ },
130
+ "entries" => [{
131
+ "@type" => "mf:Manifest",
132
+ "entries" => [{
133
+ "@type" => "earl:TestCase",
134
+ "assertions" => {
135
+ "@type" => "earl:Assertion",
136
+ "assertedBy" => {"@embed" => false},
137
+ "result" => {"@type" => "earl:TestResult"},
138
+ "subject" => {"@embed" => false}
139
+ }
140
+ }]
141
+ }]
119
142
  }.freeze
120
143
 
121
144
  # Convenience vocabularies
@@ -144,8 +167,11 @@ class EarlReport
144
167
  raise "Require at least one input file" if files.empty?
145
168
  @files = files
146
169
  @prefixes = {}
170
+
171
+ # If provided :json, it is used for generating all other output forms
147
172
  if @options[:json]
148
173
  @json_hash = ::JSON.parse(File.read(files.first))
174
+ JSON::LD::Reader.open(files.first) {|r| @graph = RDF::Graph.new << r}
149
175
  return
150
176
  end
151
177
 
@@ -157,14 +183,31 @@ class EarlReport
157
183
  Array(@options[:manifest]).each do |man|
158
184
  g = RDF::Graph.load(man, man_opts)
159
185
  status " loaded #{g.count} triples from #{man}"
160
- @graph << g
186
+ graph << g
187
+ end
188
+
189
+ # Hash test cases by URI
190
+ tests = SPARQL.execute(@options[:query], graph)
191
+ .to_a
192
+ .inject({}) {|memo, soln| memo[soln[:uri]] = soln; memo}
193
+
194
+ if tests.empty?
195
+ raise "no tests found querying manifest.\n" +
196
+ "Results are found using the following query, this can be overridden using the --query option:\n" +
197
+ "#{@options[:query]}"
161
198
  end
162
199
 
163
- # Read test assertion files
200
+ # Manifests in graph
201
+ man_uris = tests.values.map {|v| v[:manUri]}.uniq.compact
202
+ test_resources = tests.values.map {|v| v[:uri]}.uniq.compact
203
+ subjects = {}
204
+
205
+ assertion_graph = RDF::Graph.new
206
+ # Read test assertion files into assertion graph
164
207
  files.flatten.each do |file|
165
208
  status "read #{file}"
166
209
  file_graph = RDF::Graph.load(file)
167
- if file_graph.first_object(:predicate => RDF::URI('http://www.w3.org/ns/earl#testSubjects'))
210
+ if file_graph.first_object(predicate: RDF::URI('http://www.w3.org/ns/earl#testSubjects'))
168
211
  status " skip #{file}, which seems to be a previous rollup earl report"
169
212
  @files -= [file]
170
213
  else
@@ -196,30 +239,163 @@ class EarlReport
196
239
  end
197
240
 
198
241
  # Load developers referenced from Test Subjects
199
- solutions.each do |solution|
200
- # Load FOAF definitions
201
- if solution[:developer] && !solution[:devName] # not loaded
202
- status "read description for developer #{solution[:developer].inspect}"
203
- begin
204
- foaf_graph = RDF::Graph.load(solution[:developer])
205
- status " loaded #{foaf_graph.count} triples"
206
- file_graph << foaf_graph.to_a
207
- rescue
208
- warn "\nfailed to load FOAF from #{solution[:developer]}: #{$!}"
209
- end
210
- elsif !solution[:developer]
211
- warn "\nNo developer identified for #{solution[:developer]}"
242
+ if !solutions.first[:developer]
243
+ warn "\nNo developer identified for #{solutions.first[:uri]}"
244
+ elsif !solutions.first[:devName]
245
+ status "read description for developer #{solutions.first[:developer].inspect}"
246
+ begin
247
+ foaf_graph = RDF::Graph.load(solutions.first[:developer])
248
+ status " loaded #{foaf_graph.count} triples"
249
+ file_graph << foaf_graph.to_a
250
+ # Reload solutions
251
+ solutions = SPARQL.execute(TEST_SUBJECT_QUERY, file_graph)
252
+ rescue
253
+ warn "\nfailed to load FOAF from #{solutions.first[:developer]}: #{$!}"
212
254
  end
213
255
  end
214
256
 
215
- # Look for test assertions matching test definitions
216
- @graph << file_graph
257
+ solutions.each do |solution|
258
+ # Kepp track of subjects
259
+ subjects[solution[:uri]] = RDF::URI(file)
260
+
261
+ # Add TestSubject information to main graph
262
+ name = solution[:name].to_s if solution[:name]
263
+ language = solution[:language].to_s if solution[:language]
264
+ doapDesc = solution[:doapDesc] if solution[:doapDesc]
265
+ doapDesc.language ||= :en
266
+ devName = solution[:devName].to_s if solution[:devName]
267
+ graph << RDF::Statement(solution[:uri], RDF.type, RDF::DOAP.Project)
268
+ graph << RDF::Statement(solution[:uri], RDF.type, EARL.TestSubject)
269
+ graph << RDF::Statement(solution[:uri], RDF.type, EARL.Software)
270
+ graph << RDF::Statement(solution[:uri], RDF::DOAP.name, name)
271
+ graph << RDF::Statement(solution[:uri], RDF::DOAP.developer, solution[:developer])
272
+ graph << RDF::Statement(solution[:uri], RDF::DOAP.homepage, solution[:homepage]) if solution[:homepage]
273
+ graph << RDF::Statement(solution[:uri], RDF::DOAP.description, doapDesc) if doapDesc
274
+ graph << RDF::Statement(solution[:uri], RDF::DOAP[:"programming-language"], language) if solution[:language]
275
+ graph << RDF::Statement(solution[:developer], RDF.type, solution[:devType]) if solution[:devType]
276
+ graph << RDF::Statement(solution[:developer], RDF::FOAF.name, devName) if devName
277
+ graph << RDF::Statement(solution[:developer], RDF::FOAF.homepage, solution[:devHomepage]) if solution[:devHomepage]
278
+ end
279
+
280
+ assertion_graph << file_graph
217
281
  end
218
282
  end
283
+
284
+ # Make sure that each assertion matches a test and add reference from test to assertion
285
+ found_solutions = {}
286
+
287
+ # Initialize test assertions with an entry for each test subject
288
+ test_assertion_lists = {}
289
+ test_assertion_lists = tests.keys.inject({}) do |memo, test|
290
+ memo.merge(test => Array.new(subjects.length))
291
+ end
292
+
293
+ status "query assertions"
294
+ SPARQL.execute(ASSERTION_QUERY, assertion_graph).each do |solution|
295
+ subject = solution[:subject]
296
+ unless tests[solution[:test]]
297
+ $stderr.puts "Skipping result for #{solution[:test]} for #{subject}, which is not defined in manifests"
298
+ next
299
+ end
300
+ unless subjects[subject]
301
+ $stderr.puts "No test result subject found for #{subject}: #{solution.inspect}"
302
+ next
303
+ end
304
+ found_solutions[subject] = true
305
+
306
+ # Add this solution at the appropriate index within that list
307
+ ndx = subjects.keys.find_index(subject)
308
+ ary = test_assertion_lists[solution[:test]] ||= []
309
+
310
+ ary[ndx] = a = RDF::Node.new
311
+ graph << RDF::Statement(a, RDF.type, EARL.Assertion)
312
+ graph << RDF::Statement(a, EARL.subject, subject)
313
+ graph << RDF::Statement(a, EARL.test, solution[:test])
314
+ graph << RDF::Statement(a, EARL.assertedBy, solution[:by])
315
+ graph << RDF::Statement(a, EARL.mode, solution[:mode]) if solution[:mode]
316
+ r = RDF::Node.new
317
+ graph << RDF::Statement(a, EARL.result, r)
318
+ graph << RDF::Statement(r, RDF.type, EARL.TestResult)
319
+ graph << RDF::Statement(r, EARL.outcome, solution[:outcome])
320
+ end
321
+
322
+ # Add ordered assertions for each test
323
+ test_assertion_lists.each do |test, ary|
324
+ # Fill any missing entries with an untested outcome
325
+ ary.each_with_index do |a, ndx|
326
+ unless a
327
+ ary[ndx] = a = RDF::Node.new
328
+ graph << RDF::Statement(a, RDF.type, EARL.Assertion)
329
+ graph << RDF::Statement(a, EARL.subject, subjects.keys[ndx])
330
+ graph << RDF::Statement(a, EARL.test, test)
331
+ r = RDF::Node.new
332
+ graph << RDF::Statement(a, EARL.result, r)
333
+ graph << RDF::Statement(r, RDF.type, EARL.TestResult)
334
+ graph << RDF::Statement(r, EARL.outcome, EARL.untested)
335
+ end
336
+
337
+ # This counts on order being preserved in default repository so we can avoid using an rdf:List
338
+ graph << RDF::Statement(test, EARL.assertions, a)
339
+ end
340
+ end
341
+
342
+ # See if any subject did not report results, which may indicate a formatting error in the EARL source
343
+ subjects.reject {|s| found_solutions[s]}.each do |sub|
344
+ $stderr.puts "No results found for #{sub} using #{ASSERTION_QUERY}"
345
+ end
346
+
347
+ # Add report wrapper to graph
348
+ ttl = %(
349
+ @prefix dc: <http://purl.org/dc/terms/> .
350
+ @prefix doap: <http://usefulinc.com/ns/doap#> .
351
+ @prefix earl: <http://www.w3.org/ns/earl#> .
352
+ @prefix mf: <http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#> .
353
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
354
+
355
+ <> a earl:Software, doap:Project;
356
+ doap:name #{quoted(@options.fetch(:name, 'Unknown'))};
357
+ dc:bibliographicCitation "#{@options.fetch(:bibRef, 'Unknown reference')}";
358
+ earl:generatedBy <http://rubygems.org/gems/earl-report>;
359
+ earl:assertions #{subjects.values.map {|f| f.to_ntriples}.join(",\n ")};
360
+ earl:testSubjects #{subjects.keys.map {|f| f.to_ntriples}.join(",\n ")};
361
+ mf:entries (#{man_uris.map {|f| f.to_ntriples}.join("\n ")}) .
362
+
363
+ <http://rubygems.org/gems/earl-report> a earl:Software, doap:Project;
364
+ doap:name "earl-report";
365
+ doap:shortdesc "Earl Report summary generator"@en;
366
+ doap:description "EarlReport generates HTML+RDFa rollups of multiple EARL reports"@en;
367
+ doap:homepage <https://github.com/gkellogg/earl-report>;
368
+ doap:programming-language "Ruby";
369
+ doap:license <http://unlicense.org>;
370
+ doap:release <https://github.com/gkellogg/earl-report/tree/#{VERSION}>;
371
+ doap:developer <http://greggkellogg.net/foaf#me> .
372
+
373
+ <https://github.com/gkellogg/earl-report/tree/#{VERSION}> a doap:Version;
374
+ doap:name "earl-report-#{VERSION}";
375
+ doap:created "#{File.mtime(File.expand_path('../../VERSION', __FILE__)).strftime('%Y-%m-%d')}"^^xsd:date;
376
+ doap:revision "#{VERSION}" .
377
+ ).gsub(/^ /, '')
378
+ RDF::Turtle::Reader.new(ttl) {|r| graph << r}
379
+
380
+ # Each manifest is an earl:Report
381
+ man_uris.each do |u|
382
+ graph << RDF::Statement.new(u, RDF.type, EARL.Report)
383
+ end
384
+
385
+ # Each subject is an earl:TestSubject
386
+ subjects.keys.each do |u|
387
+ graph << RDF::Statement.new(u, RDF.type, EARL.TestSubject)
388
+ end
389
+
390
+ # Each assertion test is a earl:TestCriterion and earl:TestCase
391
+ test_resources.each do |u|
392
+ graph << RDF::Statement.new(u, RDF.type, EARL.TestCriterion)
393
+ graph << RDF::Statement.new(u, RDF.type, EARL.TestCase)
394
+ end
219
395
  end
220
-
396
+
221
397
  ##
222
- # Dump the collesced output graph
398
+ # Dump the coalesced output graph
223
399
  #
224
400
  # If no `io` option is provided, the output is returned as a string
225
401
  #
@@ -229,7 +405,7 @@ class EarlReport
229
405
  # Optional `IO` to output results
230
406
  # @return [String] serialized graph, if `io` is nil
231
407
  def generate(options = {})
232
- options = {:format => :html}.merge(options)
408
+ options = {format: :html}.merge(options)
233
409
 
234
410
  io = options[:io]
235
411
 
@@ -246,7 +422,7 @@ class EarlReport
246
422
  earl_turtle(options)
247
423
  else
248
424
  io = StringIO.new
249
- earl_turtle(options.merge(:io => io))
425
+ earl_turtle(options.merge(io: io))
250
426
  io.rewind
251
427
  io.read
252
428
  end
@@ -259,15 +435,12 @@ class EarlReport
259
435
  end
260
436
 
261
437
  # Generate HTML report
262
- html = Haml::Engine.new(template, :format => :xhtml).render(self, :tests => json_hash)
438
+ html = Haml::Engine.new(template, format: :xhtml).render(self, tests: json_hash)
263
439
  io.write(html) if io
264
440
  html
265
441
  else
266
- if io
267
- RDF::Writer.for(options[:format]).new(io) {|w| w << graph}
268
- else
269
- graph.dump(options[:format])
270
- end
442
+ writer = RDF::Writer.for(options[:format])
443
+ writer.dump(@graph, io, options.merge(standard_prefixes: true))
271
444
  end
272
445
  end
273
446
 
@@ -279,209 +452,16 @@ class EarlReport
279
452
  def json_hash
280
453
  @json_hash ||= begin
281
454
  # Customized JSON-LD output
282
- {
283
- "@context" => TEST_CONTEXT,
284
- "@id" => "",
285
- "@type" => %w(earl:Software doap:Project),
286
- 'name' => @options.fetch(:name, "Unknown"),
287
- 'bibRef' => @options.fetch(:bibRef, "Unknown reference"),
288
- 'generatedBy' => {
289
- "@id" => "http://rubygems.org/gems/earl-report",
290
- "@type" => "doap:Project",
291
- "name" => "earl-report",
292
- "shortdesc" => "Earl Report summary generator",
293
- "doapDesc" => "EarlReport generates HTML+RDFa rollups of multiple EARL reports",
294
- "homepage" => "https://github.com/gkellogg/earl-report",
295
- "language" => "Ruby",
296
- "license" => "http://unlicense.org",
297
- "release" => {
298
- "@id" => "https://github.com/gkellogg/earl-report/tree/#{VERSION}",
299
- "@type" => "doap:Version",
300
- "name" => "earl-report-#{VERSION}",
301
- "created" => File.mtime(File.expand_path('../../VERSION', __FILE__)).strftime('%Y-%m-%d'),
302
- "revision" => VERSION.to_s
303
- },
304
- "developer" => {
305
- "@type" => "foaf:Person",
306
- "@id" => "http://greggkellogg.net/foaf#me",
307
- "foaf:name" => "Gregg Kellogg",
308
- "foaf:homepage" => "http://greggkellogg.net/"
309
- }
310
- },
311
- "assertions" => @files,
312
- 'testSubjects' => json_test_subject_info,
313
- 'entries' => json_result_info
314
- }
315
- end
316
- end
317
-
318
- ##
319
- # Return array of test subject information
320
- # @return [Array]
321
- def json_test_subject_info
322
- # Get the set of subjects
323
- @subject_info ||= begin
324
- ts_info = {}
325
- SPARQL.execute(TEST_SUBJECT_QUERY, @graph).each do |solution|
326
- #status "solution #{solution.to_hash.inspect}"
327
- info = ts_info[solution[:uri].to_s] ||= {}
328
- %w(name doapDesc homepage language).each do |prop|
329
- info[prop] = solution[prop.to_sym].to_s if solution[prop.to_sym]
330
- end
331
- if solution[:devName]
332
- dev_type = solution[:devType].to_s =~ /Organization/ ? "foaf:Organization" : "foaf:Person"
333
- dev = {'@type' => dev_type}
334
- dev['@id'] = solution[:developer].to_s if solution[:developer].uri?
335
- dev['foaf:name'] = solution[:devName].to_s if solution[:devName]
336
- dev['foaf:homepage'] = solution[:devHomepage].to_s if solution[:devHomepage]
337
- (info['developer'] ||= []) << dev
338
- end
339
- info['developer'] = info['developer'].uniq if info['developer']
455
+ r = JSON::LD::API.fromRDF(graph) do |expanded|
456
+ JSON::LD::API.frame(expanded, TEST_FRAME, expanded: true)
340
457
  end
341
-
342
- # Map ids and values to array entries
343
- ts_info.keys.sort.map do |id|
344
- info = ts_info[id]
345
- subject = {"@id" => id, "@type" => %w(earl:TestSubject doap:Project)}
346
- %w(name developer doapDesc homepage language).each do |prop|
347
- subject[prop] = info[prop] if info[prop]
348
- end
349
- subject
458
+ unless r.is_a?(Hash) && r.has_key?('@graph') && Array(r["@graph"]).length == 1
459
+ raise "Expected JSON result to have a single entry"
350
460
  end
461
+ {"@context" => r["@context"]}.merge(r["@graph"].first)
351
462
  end
352
463
  end
353
464
 
354
- ##
355
- # Return result information for each test.
356
- # This counts on hash maintaining insertion order
357
- #
358
- # @return [Array<Hash>] List of manifests
359
- def json_result_info
360
- manifests = []
361
- test_cases = {}
362
- subjects = json_test_subject_info.map {|s| s['@id']}
363
-
364
- # Hash test cases by URI
365
- solutions = SPARQL.execute(@options[:query], @graph)
366
- .to_a
367
- .inject({}) {|memo, soln| memo[soln[:uri]] = soln; memo}
368
-
369
- if solutions.empty?
370
- raise "no results found querying manifest.\n" +
371
- "Results are found using the following query, this can be overridden using the --query option:\n" +
372
- "#{@options[:query]}"
373
- end
374
-
375
- qst = Time.now
376
- # If test cases are in a list, maintain order
377
- solutions.values.select {|s| s[:manUri]}.each do |man_soln|
378
- # Get tests for this manifest in list order
379
- solution_list = RDF::List.new(man_soln[:lh], @graph)
380
-
381
- # Set up basic manifest information
382
- man_info = manifests.detect {|m| m['@id'] == man_soln[:manUri].to_s}
383
- unless man_info
384
- status "manifest: #{man_soln[:manUri]}"
385
- man_info = {
386
- '@id' => man_soln[:manUri].to_s,
387
- "@type" => %w{earl:Report mf:Manifest},
388
- 'entries' => []
389
- }
390
- man_info['title'] = man_soln[:manTitle].to_s if man_soln[:manTitle]
391
- man_info['description'] = man_soln[:manDescription].to_s if man_soln[:manDescription]
392
- manifests << man_info
393
- end
394
-
395
- # Collect each TestCase
396
- solution_list.each do |uri|
397
- solution = solutions[uri]
398
-
399
- unless solution
400
- $stderr.puts "Expected to find solution for #{uri}\n"
401
- "Results are found using the following query, this can be overridden using the --query option:\n" +
402
- "#{@options[:query]}"
403
- next
404
- end
405
-
406
- # Create entry for this test case, if it doesn't already exist
407
- tc = man_info['entries'].detect {|t| t['@id'] == uri}
408
- unless tc
409
- tc = {
410
- '@id' => uri.to_s,
411
- '@type' => %w(earl:TestCriterion earl:TestCase),
412
- 'title' => solution[:title].to_s,
413
- 'testAction' => solution[:testAction].to_s,
414
- 'assertions' => []
415
- }
416
- tc['@type'] << solution[:type].to_s if solution[:type]
417
- tc['description'] = solution[:description].to_s if solution[:description]
418
- tc['testResult'] = solution[:testResult].to_s if solution[:testResult]
419
-
420
- # Pre-initialize results for each subject to untested
421
- subjects.each do |siri|
422
- tc['assertions'] << {
423
- '@type' => 'earl:Assertion',
424
- 'test' => uri.to_s,
425
- 'subject' => siri,
426
- 'mode' => 'earl:notAvailable',
427
- 'result' => {
428
- '@type' => 'earl:TestResult',
429
- 'outcome' => 'earl:untested'
430
- }
431
- }
432
- end
433
-
434
- test_cases[uri.to_s] = tc
435
- man_info['entries'] << tc
436
- end
437
- end
438
-
439
- raise "No test cases found" if man_info['entries'].empty?
440
- status "Test cases:\n #{man_info['entries'].map {|tc| tc.fetch('@id')}.join("\n ")}"
441
- end
442
- qnd = Time.now
443
- status "\nassertion query: #{(qnd - qst)/1000} secs"
444
-
445
- raise "No manifests found" if manifests.empty?
446
- status "Manifests:\n #{manifests.map {|m| m.fetch('@id')}.join("\n ")}"
447
-
448
- # Iterate through assertions and add to appropriate test case
449
- found_solutions = {}
450
- SPARQL.execute(ASSERTION_QUERY, @graph).each do |solution|
451
- tc = test_cases[solution[:test].to_s]
452
- unless tc
453
- $stderr.puts "Skipping result for #{solution[:test]}, which is not defined in manifests"
454
- next
455
- end
456
- unless solution[:outcome]
457
- $stderr.puts "No result found for #{solution[:test]}: #{solution.inspect}"
458
- next
459
- end
460
- unless solution[:outcome]
461
- $stderr.puts "No test subject found for #{solution[:test]}: #{solution.inspect}"
462
- next
463
- end
464
- subject = solution[:subject].to_s
465
- found_solutions[subject] = true
466
- result_index = subjects.index(subject)
467
- unless result_index
468
- $stderr.puts "No test result subject found for #{subject}: #{solution.inspect}"
469
- next
470
- end
471
- ta_hash = tc['assertions'][result_index]
472
- ta_hash['assertedBy'] = solution[:by].to_s
473
- ta_hash['mode'] = "earl:#{solution[:mode].to_s.split('#').last || 'notAvailable'}"
474
- ta_hash['result']['outcome'] = "earl:#{solution[:outcome].to_s.split('#').last}"
475
- end
476
-
477
- # See if any subject did not report results, which indicates a formatting error in the EARL source
478
- subjects.reject {|s| found_solutions[s]}.each do |sub|
479
- $stderr.puts "No results found for #{sub} using #{ASSERTION_QUERY}"
480
- end
481
-
482
- manifests.sort_by {|m| m['title'].to_s}
483
- end
484
-
485
465
  ##
486
466
  # Output consoloated EARL report as Turtle
487
467
  # @param [Hash{Symbol => Object}] options
@@ -489,165 +469,45 @@ class EarlReport
489
469
  # @return [String]
490
470
  def earl_turtle(options)
491
471
  io = options[:io]
492
- # Write preamble
493
- {
494
- :dc => RDF::DC,
495
- :doap => RDF::DOAP,
496
- :earl => EARL,
497
- :foaf => RDF::FOAF,
498
- :mf => MF,
499
- :owl => RDF::OWL,
500
- :rdf => RDF,
501
- :rdfs => RDF::RDFS,
502
- :xhv => RDF::XHV,
503
- :xsd => RDF::XSD
504
- }.each do |prefix, vocab|
505
- io.puts("@prefix #{prefix}: <#{vocab.to_uri}> .")
506
- end
507
- io.puts
508
472
 
509
- %w(name bibRef generatedBy assertions testSubjects).each do |field|
510
- raise "Expected EARL json to have #{field.inspect} entry" unless json_hash[field]
511
- end
473
+ top_level = graph.first_subject(predicate: EARL.generatedBy)
512
474
 
513
- # Write earl:Software for the report
514
- man_defs = json_hash['entries'].map {|defn| as_resource(defn.fetch('@id'))}.join("\n ")
515
- io.puts %{
516
- #{as_resource(json_hash.fetch('@id'))} a #{Array(json_hash.fetch('@type')).join(', ')};
517
- doap:name #{quoted(json_hash.fetch('name', 'Unknown'))};
518
- dc:bibliographicCitation "#{json_hash.fetch('bibRef', 'Unknown reference')}";
519
- earl:generatedBy #{as_resource json_hash.fetch('generatedBy').fetch('@id')};
520
- earl:assertions
521
- #{json_hash.fetch('assertions').map {|a| as_resource(a)}.join(",\n ")};
522
- earl:testSubjects (
523
- #{json_hash.fetch('testSubjects').map {|a| as_resource(a.fetch('@id'))}.join("\n ")});
524
- mf:entries (\n #{man_defs}) .
525
- }.gsub(/^ /, '')
526
-
527
- # Write generating software information
528
- io.puts %{
529
- <http://rubygems.org/gems/earl-report> a earl:Software, doap:Project;
530
- doap:name "earl-report";
531
- doap:shortdesc "Earl Report summary generator"@en;
532
- doap:description "EarlReport generates HTML+RDFa rollups of multiple EARL reports"@en;
533
- doap:homepage <https://github.com/gkellogg/earl-report>;
534
- doap:programming-language "Ruby";
535
- doap:license <http://unlicense.org>;
536
- doap:release <https://github.com/gkellogg/earl-report/tree/#{VERSION}>;
537
- doap:developer <http://greggkellogg.net/foaf#me> .
538
-
539
- }.gsub(/^ /, '')
540
-
541
- # Output Manifest definitions
542
- # along with test cases and assertions
543
- test_cases = []
544
- io.puts %(\n# Manifests)
545
- json_hash['entries'].each do |man|
546
- io.puts %(#{as_resource(man.fetch('@id'))} a earl:Report, mf:Manifest;)
547
- io.puts %( dc:title #{quoted(man['title'])};) if man['title']
548
- io.puts %( mf:name #{quoted(man['title'])};) if man['title']
549
- io.puts %( rdfs:comment #{quoted(man['description'])};) if man['description']
550
-
551
- # Test Cases
552
- test_defs = man.fetch('entries').map {|defn| as_resource(defn.fetch('@id'))}.join("\n ")
553
- io.puts %( mf:entries (\n #{test_defs}) .\n\n)
554
-
555
- test_cases += man['entries']
556
- end
475
+ # Write starting with the entire graph to get preamble
476
+ writer = RDF::Turtle::Writer.new(io, standard_prefixes: true)
477
+ writer << graph
557
478
 
558
- # Write out each earl:TestSubject
559
- io.puts %(#\n# Subject Definitions\n#)
560
- json_hash.fetch('testSubjects').each do |ts_desc|
561
- io.write(test_subject_turtle(ts_desc))
562
- end
479
+ writer.send(:preprocess)
480
+ writer.send(:start_document)
563
481
 
564
- # Write out each earl:TestCase
565
- io.puts %(#\n# Test Case Definitions\n#)
566
- json_hash.fetch('entries').each do |manifest|
567
- manifest.fetch('entries').each do |test_case|
568
- io.write(tc_turtle(test_case))
569
- end
570
- end
571
- end
572
-
573
- ##
574
- # Write out Test Subject definition for each earl:TestSubject
575
- # @param [Hash] desc
576
- # @return [String]
577
- def test_subject_turtle(desc)
578
- %w(@id @type name).each do |field|
579
- raise "Expected test description to have #{field.inspect} entry" unless desc[field]
580
- end
482
+ # Write top-level object referencing manifests and subjects
483
+ writer.send(:statement, top_level)
581
484
 
582
- res = %(<#{desc.fetch('@id')}> a #{desc.fetch('@type').join(', ')};\n)
583
- res += %( doap:name #{quoted(desc.fetch('name'))};\n)
584
- res += %( doap:description #{quoted(desc['doapDesc'])}@en;\n) if desc['doapDesc']
585
- res += %( doap:programming-language #{quoted(desc['language'])};\n) if desc['language']
586
- res += %( .\n\n)
485
+ # Write each manifest
486
+ io.puts "\n# Manifests"
487
+ RDF::List.new(graph.first_object(subject: top_level, predicate: MF[:entries]), graph).each do |manifest|
488
+ writer.send(:statement, manifest)
587
489
 
588
- [desc['developer']].flatten.compact.each do |developer|
589
- if developer['@id']
590
- %w(@id @type foaf:name).each do |field|
591
- raise "Expected FOAF description to have #{field.inspect} entry" unless developer[field]
592
- end
593
- res += %(<#{desc.fetch('@id')}> doap:developer <#{developer.fetch('@id')}> .\n\n)
594
- res += %(<#{developer.fetch('@id')}> a #{Array(developer.fetch('@type')).join(', ')};\n)
595
- res += %( foaf:homepage <#{developer['foaf:homepage']}>;\n) if developer['foaf:homepage']
596
- res += %( foaf:name #{quoted(developer.fetch('foaf:name'))} .\n\n)
597
- else
598
- %w(foaf:name).each do |field|
599
- raise "Expected FOAF description to have #{field.inspect} entry" unless developer[field]
600
- end
601
- res += %(<#{desc.fetch('@id')}> doap:developer\n)
602
- res += %( [ a #{developer.fetch('@type', 'foaf:Person')};\n)
603
- res += %( foaf:homepage <#{developer['foaf:homepage']}>;\n) if developer['foaf:homepage']
604
- res += %( foaf:name #{quoted(developer.fetch('foaf:name'))} ] .\n\n)
490
+ # Write each test case
491
+ RDF::List.new(graph.first_object(subject: manifest, predicate: MF[:entries]), graph).each do |tc|
492
+ writer.send(:statement, tc)
605
493
  end
606
494
  end
607
- res + "\n"
608
- end
609
-
610
- ##
611
- # Write out each Test Case definition
612
- # @prarm[Hash] desc
613
- # @return [String]
614
- def tc_turtle(desc)
615
- %w(@id @type title testAction assertions).each do |field|
616
- raise "Expected test case description to have #{field.inspect} entry" unless desc[field]
617
- end
618
- types = Array(desc['@type']).map do |t|
619
- t.include?("://") ? "<#{t}>" : t
620
- end
621
- res = %{#{as_resource desc.fetch('@id')} a #{types.join(', ')};\n}
622
- res += %{ dc:title #{quoted(desc.fetch('title'))};\n}
623
- res += %{ dc:description #{quoted(desc['description'])}@en;\n} if desc['description']
624
- res += %{ mf:result #{as_resource desc['testResult']};\n} if desc['testResult']
625
- res += %{ mf:action #{as_resource desc.fetch('testAction')};\n}
626
- res += %{ earl:assertions (\n}
627
- desc['assertions'].each do |as_desc|
628
- res += as_turtle(as_desc)
629
- end
630
- res += %{ ) .\n\n}
631
- end
632
495
 
633
- ##
634
- # Write out each Assertion definition
635
- # @prarm[Hash] desc
636
- # @return [String]
637
- def as_turtle(desc)
638
- %w(test subject result).each do |field|
639
- raise "Expected assertion description to have #{field.inspect} entry" unless desc[field]
496
+ # Write test subjects
497
+ io.puts "\n# Test Subjects"
498
+ graph.query(subject: top_level, predicate: EARL.testSubjects).each do |s|
499
+ writer.send(:statement, s.object)
500
+
501
+ # Write each developer
502
+ graph.query(subject: s.object, predicate: RDF::DOAP.developer).each do |d|
503
+ writer.send(:statement, d.object)
504
+ end
640
505
  end
641
- res = %( [ a earl:Assertion;\n)
642
- res += %( earl:assertedBy #{as_resource desc['assertedBy']};\n) if desc['assertedBy']
643
- res += %( earl:test #{as_resource desc.fetch('test')};\n)
644
- res += %( earl:subject #{as_resource desc.fetch('subject')};\n)
645
- res += %( earl:mode #{desc['mode']};\n) if desc['mode']
646
- res += %( earl:result [ a earl:TestResult; earl:outcome #{desc.fetch('result').fetch('outcome')} ]]\n)
647
- end
648
506
 
649
- def as_resource(resource)
650
- resource[0,2] == '_:' ? resource : "<#{resource}>"
507
+ # Write generator
508
+ io.puts "\n# Report Generation Software"
509
+ writer.send(:statement, RDF::URI("http://rubygems.org/gems/earl-report"))
510
+ writer.send(:statement, RDF::URI("https://github.com/gkellogg/earl-report/tree/#{VERSION}"))
651
511
  end
652
512
 
653
513
  def quoted(string)