earl-report 0.4.9 → 0.6.1

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
  SHA256:
3
- metadata.gz: e76543b490f30df2542076f3beb58514e02346c964e716aed470ea1f1dd1dcac
4
- data.tar.gz: e03701faad62e4ea150911a1bdca0a875839276ddc28604e1066a5a532ac0509
3
+ metadata.gz: 77b18a547a9e1721a2c32e83abc8230dfb5c8a4a7170874cda1b4cf41d50d4d8
4
+ data.tar.gz: 04654044c3dfca443518097550ef07bd68b74203ca9dfc239a49c1a62a5c7556
5
5
  SHA512:
6
- metadata.gz: 7a5f474b6b1fe4d1f1c259d93048b2a2299dd56ff88fd82d215f8f55cf6f51c20706ccac8189bf6d250d16a0a1c5f614d90c30f6f0212c2beaab65ddf13a03b2
7
- data.tar.gz: fa242658e76b29269cf820834f83df03b6bef2d3b30e018d420ace79dd0b2b0afdde97708fb8183a3afbbce397f9339a7920795cbe697b2eeeef15035d9aaf3d
6
+ metadata.gz: 743c19a11e2b87f8819d841df0d37b9a6693b8eac15546d87e025d2c14f3464604cacc78adadc6d08103326070975334cb77e50a7da7b1824baf1babc4dbd33f
7
+ data.tar.gz: 23524a7e72dfd286504c422b05defa55e1ae00343a348926180d5d252a3ca041bc6a948cd929eca6e4183bdc2f8c1a0ff3b39861c34417ead1360bce7cd77525
data/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
  Ruby gem to consolidate multiple EARL report and generate a rollup conformance report.
3
3
 
4
4
  [![Gem Version](https://badge.fury.io/rb/earl-report.png)](http://badge.fury.io/rb/earl-report)
5
- [![Build Status](https://travis-ci.org/gkellogg/earl-report.png?branch=master)](http://travis-ci.org/gkellogg/earl-report)
6
- [![Coverage Status](https://coveralls.io/repos/gkellogg/earl-report/badge.svg)](https://coveralls.io/r/gkellogg/earl-report)
5
+ [![Build Status](https://github.com/gkellogg/earl-report/workflows/CI/badge.svg?branch=develop)](https://github.com/gkellogg/earl-report/actions?query=workflow%3ACI)
6
+ [![Coverage Status](https://coveralls.io/repos/gkellogg/earl-report/badge.svg?branch=develop)](https://coveralls.io/r/gkellogg/earl-report?branch=develop)
7
7
 
8
8
  ## Description
9
9
  Reads a test manifest in the
@@ -17,8 +17,8 @@ may be specified in an any compatible RDF serialization. The report is composed
17
17
  in the following form:
18
18
 
19
19
  [ a earl:Assertion;
20
- earl:assertedBy <http://greggkellogg.net/foaf#me>;
21
- earl:subject <http://rubygems.org/gems/rdf-turtle>;
20
+ earl:assertedBy <https://greggkellogg.net/foaf#me>;
21
+ earl:subject <https://rubygems.org/gems/rdf-turtle>;
22
22
  earl:test <http://dvcs.w3.org/hg/rdf/raw-file/default/rdf-turtle/tests-ttl/manifest.ttl#turtle-syntax-file-01>;
23
23
  earl:result [
24
24
  a earl:TestResult;
@@ -29,11 +29,16 @@ in the following form:
29
29
  Additionally, `earl:subject` is expected to reference a [DOAP]() description
30
30
  of the reported software, in the following form:
31
31
 
32
- <http://rubygems.org/gems/rdf-turtle> a doap:Project, earl:TestSubject, earl:Software ;
32
+ <https://rubygems.org/gems/rdf-turtle> a doap:Project, earl:TestSubject, earl:Software ;
33
33
  doap:name "RDF::Turtle" ;
34
- doap:developer <http://greggkellogg.net/foaf#me> ;
34
+ doap:developer <https://greggkellogg.net/foaf#me> ;
35
35
  doap:homepage <http://ruby-rdf.github.com/rdf-turtle> ;
36
36
  doap:description "RDF::Turtle is an Turtle reader/writer for the RDF.rb library suite."@en ;
37
+ doap:release [
38
+ doap:name "RDF::Turtle 3.1.0" ;
39
+ doap:created "2015-09-27"^^xsd:date ;
40
+ doap:revision "3.1.0"
41
+ ] ;
37
42
  doap:programming-language "Ruby" .
38
43
 
39
44
  The [DOAP]() description may be included in the [EARL]() report. If not found,
@@ -44,7 +49,7 @@ The `doap:developer` is expected to reference a [FOAF]() profile for the agent
44
49
  (user or organization) responsible for the test subject. It is expected to be
45
50
  of the following form:
46
51
 
47
- <http://greggkellogg.net/foaf#me> foaf:name "Gregg Kellogg" .
52
+ <https://greggkellogg.net/foaf#me> foaf:name "Gregg Kellogg" .
48
53
 
49
54
  If not found, the IRI identified by `doap:developer`
50
55
  will be dereferenced and is presumed to provide a [FOAF]() profile of the developer.
@@ -91,14 +96,14 @@ Version 0.3 and prior re-constructed the test manifest used to create the body o
91
96
  mf:result <http://example/test-00.out>;
92
97
  earl:assertions ([
93
98
  a earl:Assertion;
94
- earl:assertedBy <http://greggkellogg.net/foaf#me>;
99
+ earl:assertedBy <https://greggkellogg.net/foaf#me>;
95
100
  earl:mode earl:automatic;
96
101
  earl:result [
97
102
  a earl:TestResult;
98
103
  dc:date "2012-11-06T19:23:29-08:00"^^xsd:dateTime;
99
104
  earl:outcome earl:passed
100
105
  ];
101
- earl:subject <http://rubygems.org/gems/rdf-turtle>;
106
+ earl:subject <https://rubygems.org/gems/rdf-turtle>;
102
107
  earl:test <http://example/manifest.ttl#testeval00>
103
108
  ]) .
104
109
 
@@ -127,7 +132,7 @@ Generally, creating a `json` format first is more efficient. Subsequent invocati
127
132
  When run, `earl-report` attempts to open the file `.earl` in the current directory. This file is in [YAML][] format with entries for each option. Use the `--rc` option to automatically generate it.
128
133
 
129
134
  ## Author
130
- * [Gregg Kellogg](http://github.com/gkellogg) - <http://greggkellogg.net/>
135
+ * [Gregg Kellogg](https://github.com/gkellogg) - <https://greggkellogg.net/>
131
136
 
132
137
  ## License
133
138
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.9
1
+ 0.6.1
data/bin/earl-report CHANGED
@@ -15,6 +15,7 @@ OPT_ARGS = [
15
15
  ["--output", "-o", GetoptLong::REQUIRED_ARGUMENT,"Output report to file"],
16
16
  ["--query", GetoptLong::REQUIRED_ARGUMENT,"Query, or file containing query for extracting information from Test manifest"],
17
17
  ["--rc", GetoptLong::NO_ARGUMENT, "Write options to run-control file"],
18
+ ["--strict", GetoptLong::NO_ARGUMENT, "Fails if any skipped result is found"],
18
19
  ["--template", GetoptLong::OPTIONAL_ARGUMENT,"Specify or return default report template"],
19
20
  ["--verbose", GetoptLong::NO_ARGUMENT, "Detail on execution"],
20
21
  ["--help", "-?", GetoptLong::NO_ARGUMENT, "This message"]
@@ -65,6 +66,7 @@ opts.each do |opt, arg|
65
66
  when '--name' then options[:name] = arg
66
67
  when '--output' then options[:io] = File.open(arg, "w")
67
68
  when '--rc' then options[:rc] = true
69
+ when '--strict' then options[:strict] = true
68
70
  when '--template' then options[:template] = (File.open(arg, "r") unless arg.empty?)
69
71
  when '--verbose' then options[:verbose] = true
70
72
  when '--help' then usage
@@ -94,6 +96,6 @@ if options.has_key?(:template) && options[:template].nil?
94
96
  elsif ARGV.empty?
95
97
  usage
96
98
  else
97
- earl = EarlReport.new(*ARGV, options)
98
- earl.generate(options)
99
+ earl = EarlReport.new(*ARGV, **options)
100
+ earl.generate(**options)
99
101
  end
data/lib/earl_report.rb CHANGED
@@ -11,6 +11,7 @@ class EarlReport
11
11
  autoload :VERSION, 'earl_report/version'
12
12
 
13
13
  attr_reader :graph
14
+ attr_reader :verbose
14
15
 
15
16
  # Return information about each test.
16
17
  # Tests all have an mf:action property.
@@ -33,13 +34,14 @@ class EarlReport
33
34
  PREFIX doap: <http://usefulinc.com/ns/doap#>
34
35
  PREFIX foaf: <http://xmlns.com/foaf/0.1/>
35
36
 
36
- SELECT DISTINCT ?uri ?name ?doapDesc ?revision ?homepage ?language ?developer ?devName ?devType ?devHomepage
37
+ SELECT DISTINCT ?uri ?name ?doapDesc ?release ?revision ?homepage ?language ?developer ?devName ?devType ?devHomepage
37
38
  WHERE {
38
39
  ?uri a doap:Project; doap:name ?name; doap:developer ?developer .
39
40
  OPTIONAL { ?uri doap:homepage ?homepage . }
40
41
  OPTIONAL { ?uri doap:description ?doapDesc . }
41
42
  OPTIONAL { ?uri doap:programming-language ?language . }
42
- OPTIONAL { ?uri doap:release [ doap:revision ?revision] .}
43
+ OPTIONAL { ?uri doap:release ?release . }
44
+ OPTIONAL { ?release doap:revision ?revision .}
43
45
  OPTIONAL { ?developer a ?devType .}
44
46
  OPTIONAL { ?developer foaf:name ?devName .}
45
47
  OPTIONAL { ?developer foaf:homepage ?devHomepage .}
@@ -151,35 +153,71 @@ class EarlReport
151
153
  }]
152
154
  }.freeze
153
155
 
156
+ TURTLE_PREFIXES = %(@prefix dc: <http://purl.org/dc/terms/> .
157
+ @prefix doap: <http://usefulinc.com/ns/doap#> .
158
+ @prefix earl: <http://www.w3.org/ns/earl#> .
159
+ @prefix mf: <http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#> .
160
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
161
+ @prefix foaf: <http://xmlns.com/foaf/0.1/> .
162
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
163
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
164
+ ).gsub(/^ /, '')
165
+
166
+ TURTLE_SOFTWARE = %(
167
+ # Report Generation Software
168
+ <https://rubygems.org/gems/earl-report> a earl:Software, doap:Project;
169
+ doap:name "earl-report";
170
+ doap:shortdesc "Earl Report summary generator"@en;
171
+ doap:description "EarlReport generates HTML+RDFa rollups of multiple EARL reports"@en;
172
+ doap:homepage <https://github.com/gkellogg/earl-report>;
173
+ doap:programming-language "Ruby";
174
+ doap:license <http://unlicense.org>;
175
+ doap:release <https://github.com/gkellogg/earl-report/tree/#{VERSION}>;
176
+ doap:developer <https://greggkellogg.net/foaf#me> .
177
+
178
+ <https://github.com/gkellogg/earl-report/tree/#{VERSION}> a doap:Version;
179
+ doap:name "earl-report-#{VERSION}";
180
+ doap:created "#{File.mtime(File.expand_path('../../VERSION', __FILE__)).strftime('%Y-%m-%d')}"^^xsd:date;
181
+ doap:revision "#{VERSION}" .
182
+ ).gsub(/^ /, '')
183
+
154
184
  # Convenience vocabularies
155
185
  class EARL < RDF::Vocabulary("http://www.w3.org/ns/earl#"); end
156
186
  class MF < RDF::Vocabulary("http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#"); end
157
187
 
158
188
  ##
159
189
  # Load test assertions and look for referenced software and developer information
160
- # @overload initialize(*files)
161
- # @param [Array<String>] files Assertions
162
- # @overload initialize(*files, options = {})
163
- # @param [Hash{Symbol => Object}] options
164
- # @option options [Boolean] :verbose (true)
165
- # @option options [String] :base Base IRI for loading Manifest
166
- # @option options [String] :bibRef
167
- # ReSpec bibliography reference for specification being tested
168
- # @option options [String] :json Result of previous JSON-LD generation
169
- # @option options [String, Array<String>] :manifest Test manifest
170
- # @option options [String] :name Name of specification
171
- # @option options [String] :query
172
- # Query, or file containing query for extracting information from Test manifests
173
- def initialize(*files)
174
- @options = files.last.is_a?(Hash) ? files.pop.dup : {}
175
- @options[:query] ||= MANIFEST_QUERY
176
- raise "Test Manifests must be specified with :manifest option" unless @options[:manifest] || @options[:json]
190
+ #
191
+ # @param [Array<String>] files Assertions
192
+ # @param [String] base (nil) Base IRI for loading Manifest
193
+ # @param [String] bibRef ('Unknown reference')
194
+ # ReSpec bibliography reference for specification being tested
195
+ # @param [Boolean] json (false) File is in the JSON format of a report.
196
+ # @param [String, Array<String>] manifest (nil) Test manifest(s)
197
+ # @param [String] name ('Unknown') Name of specification
198
+ # @param [String] query (MANIFEST_QUERY)
199
+ # Query, or file containing query for extracting information from Test manifests
200
+ # @param [Boolean] strict (false) Abort on any warning
201
+ # @param [Boolean] verbose (false)
202
+ def initialize(*files,
203
+ base: nil,
204
+ bibRef: 'Unknown reference',
205
+ json: false,
206
+ manifest: nil,
207
+ name: 'Unknown',
208
+ query: MANIFEST_QUERY,
209
+ strict: false,
210
+ verbose: false,
211
+ **options)
212
+ @verbose = verbose
213
+ raise "Test Manifests must be specified with :manifest option" unless manifest || json
177
214
  raise "Require at least one input file" if files.empty?
178
215
  @files = files
179
216
  @prefixes = {}
217
+ @warnings = 0
180
218
 
181
- # If provided :json, it is used for generating all other output forms
182
- if @options[:json]
219
+ # If provided json, it is used for generating all other output forms
220
+ if json
183
221
  @json_hash = ::JSON.parse(File.read(files.first))
184
222
  # Add a base_uri so relative subjects aren't dropped
185
223
  JSON::LD::Reader.open(files.first, base_uri: "http://example.org/report") do |r|
@@ -194,25 +232,25 @@ class EarlReport
194
232
  end
195
233
 
196
234
  # Load manifests, possibly with base URI
197
- status "read #{@options[:manifest].inspect}"
235
+ status "read #{manifest.inspect}"
198
236
  man_opts = {}
199
- man_opts[:base_uri] = RDF::URI(@options[:base]) if @options[:base]
237
+ man_opts[:base_uri] = RDF::URI(base) if base
200
238
  @graph = RDF::Graph.new
201
- Array(@options[:manifest]).each do |man|
239
+ Array(manifest).each do |man|
202
240
  g = RDF::Graph.load(man, unique_bnodes: true, **man_opts)
203
241
  status " loaded #{g.count} triples from #{man}"
204
242
  graph << g
205
243
  end
206
244
 
207
245
  # Hash test cases by URI
208
- tests = SPARQL.execute(@options[:query], graph)
246
+ tests = SPARQL.execute(query, graph)
209
247
  .to_a
210
248
  .inject({}) {|memo, soln| memo[soln[:uri]] = soln; memo}
211
249
 
212
250
  if tests.empty?
213
251
  raise "no tests found querying manifest.\n" +
214
252
  "Results are found using the following query, this can be overridden using the --query option:\n" +
215
- "#{@options[:query]}"
253
+ "#{query}"
216
254
  end
217
255
 
218
256
  # Manifests in graph
@@ -220,13 +258,20 @@ class EarlReport
220
258
  test_resources = tests.values.map {|v| v[:uri]}.uniq.compact
221
259
  subjects = {}
222
260
 
223
- assertion_graph = RDF::Graph.new
261
+ # Initialize test assertions with an entry for each test subject
262
+ test_assertion_lists = {}
263
+ test_assertion_lists = tests.keys.inject({}) do |memo, test|
264
+ memo.merge(test => [])
265
+ end
266
+
267
+ assertion_stats = {}
268
+
224
269
  # Read test assertion files into assertion graph
225
270
  files.flatten.each do |file|
226
271
  status "read #{file}"
227
272
  file_graph = RDF::Graph.load(file)
228
273
  if file_graph.first_object(predicate: RDF::URI('http://www.w3.org/ns/earl#testSubjects'))
229
- status " skip #{file}, which seems to be a previous rollup earl report"
274
+ warn " skip #{file}, which seems to be a previous rollup earl report"
230
275
  @files -= [file]
231
276
  else
232
277
  status " loaded #{file_graph.count} triples"
@@ -237,10 +282,10 @@ class EarlReport
237
282
 
238
283
  # Load DOAP definitions
239
284
  unless solution[:name] # not loaded
240
- status "read doap description for #{subject}"
285
+ status " read doap description for #{subject}"
241
286
  begin
242
287
  doap_graph = RDF::Graph.load(subject)
243
- status " loaded #{doap_graph.count} triples"
288
+ status " loaded #{doap_graph.count} triples"
244
289
  file_graph << doap_graph.to_a
245
290
  rescue
246
291
  warn "\nfailed to load DOAP from #{subject}: #{$!}"
@@ -260,10 +305,10 @@ class EarlReport
260
305
  if !solutions.first[:developer]
261
306
  warn "\nNo developer identified for #{solutions.first[:uri]}"
262
307
  elsif !solutions.first[:devName]
263
- status "read description for developer #{solutions.first[:developer].inspect}"
308
+ status " read description for developer #{solutions.first[:developer].inspect}"
264
309
  begin
265
310
  foaf_graph = RDF::Graph.load(solutions.first[:developer])
266
- status " loaded #{foaf_graph.count} triples"
311
+ status " loaded #{foaf_graph.count} triples"
267
312
  file_graph << foaf_graph.to_a
268
313
  # Reload solutions
269
314
  solutions = SPARQL.execute(TEST_SUBJECT_QUERY, file_graph)
@@ -272,12 +317,13 @@ class EarlReport
272
317
  end
273
318
  end
274
319
 
320
+ release = nil
275
321
  solutions.each do |solution|
276
322
  # Kepp track of subjects
277
323
  subjects[solution[:uri]] = RDF::URI(file)
278
324
 
279
325
  # Add TestSubject information to main graph
280
- name = solution[:name].to_s if solution[:name]
326
+ doapName = solution[:name].to_s if solution[:name]
281
327
  language = solution[:language].to_s if solution[:language]
282
328
  doapDesc = solution[:doapDesc] if solution[:doapDesc]
283
329
  doapDesc.language ||= :en if doapDesc
@@ -285,7 +331,7 @@ class EarlReport
285
331
  graph << RDF::Statement(solution[:uri], RDF.type, RDF::Vocab::DOAP.Project)
286
332
  graph << RDF::Statement(solution[:uri], RDF.type, EARL.TestSubject)
287
333
  graph << RDF::Statement(solution[:uri], RDF.type, EARL.Software)
288
- graph << RDF::Statement(solution[:uri], RDF::Vocab::DOAP.name, name)
334
+ graph << RDF::Statement(solution[:uri], RDF::Vocab::DOAP.name, doapName)
289
335
  graph << RDF::Statement(solution[:uri], RDF::Vocab::DOAP.developer, solution[:developer])
290
336
  graph << RDF::Statement(solution[:uri], RDF::Vocab::DOAP.homepage, solution[:homepage]) if solution[:homepage]
291
337
  graph << RDF::Statement(solution[:uri], RDF::Vocab::DOAP.description, doapDesc) if doapDesc
@@ -294,59 +340,60 @@ class EarlReport
294
340
  graph << RDF::Statement(solution[:developer], RDF::Vocab::FOAF.name, devName) if devName
295
341
  graph << RDF::Statement(solution[:developer], RDF::Vocab::FOAF.homepage, solution[:devHomepage]) if solution[:devHomepage]
296
342
 
297
- rev = RDF::Node.new
298
- graph << RDF::Statement(solution[:uri], RDF::Vocab::DOAP.release, rev)
299
- graph << RDF::Statement(rev, RDF::Vocab::DOAP.revision, (solution[:revision] || "unknown"))
343
+ # Make sure BNode identifiers don't leak
344
+ release ||= if !solution[:release] || solution[:release].node?
345
+ RDF::Node.new
346
+ else
347
+ solution[:release]
348
+ end
349
+ graph << RDF::Statement(solution[:uri], RDF::Vocab::DOAP.release, release)
350
+ graph << RDF::Statement(release, RDF::Vocab::DOAP.revision, (solution[:revision] || "unknown"))
300
351
  end
301
352
 
302
- assertion_graph << file_graph
303
- end
304
- end
353
+ # Make sure that each assertion matches a test and add reference from test to assertion
354
+ found_solutions = false
355
+ subject = nil
305
356
 
306
- # Make sure that each assertion matches a test and add reference from test to assertion
307
- found_solutions = {}
357
+ status " query assertions"
358
+ SPARQL.execute(ASSERTION_QUERY, file_graph).each do |solution|
359
+ subject = solution[:subject]
360
+ unless tests[solution[:test]]
361
+ assertion_stats["Skipped"] = assertion_stats["Skipped"].to_i + 1
362
+ warn "Skipping result for #{solution[:test]} for #{subject}, which is not defined in manifests"
363
+ next
364
+ end
365
+ unless subjects[subject]
366
+ assertion_stats["Missing Subject"] = assertion_stats["Missing Subject"].to_i + 1
367
+ warn "No test result subject found for #{subject}: in #{subjects.keys.join(', ')}"
368
+ next
369
+ end
370
+ found_solutions ||= true
371
+ assertion_stats["Found"] = assertion_stats["Found"].to_i + 1
308
372
 
309
- # Initialize test assertions with an entry for each test subject
310
- test_assertion_lists = {}
311
- test_assertion_lists = tests.keys.inject({}) do |memo, test|
312
- memo.merge(test => Array.new(subjects.length))
313
- end
373
+ # Add this solution at the appropriate index within that list
374
+ ndx = subjects.keys.find_index(subject)
375
+ ary = test_assertion_lists[solution[:test]]
314
376
 
315
- status "query assertions"
316
- assertion_stats = {}
317
- SPARQL.execute(ASSERTION_QUERY, assertion_graph).each do |solution|
318
- subject = solution[:subject]
319
- unless tests[solution[:test]]
320
- assertion_stats["Skipped"] = assertion_stats["Skipped"].to_i + 1
321
- $stderr.puts "Skipping result for #{solution[:test]} for #{subject}, which is not defined in manifests"
322
- next
323
- end
324
- unless subjects[subject]
325
- assertion_stats["Missing Subject"] = assertion_stats["Missing Subject"].to_i + 1
326
- $stderr.puts "No test result subject found for #{subject}: in #{subjects.keys.join(', ')}"
327
- next
377
+ ary[ndx] = a = RDF::Node.new
378
+ graph << RDF::Statement(a, RDF.type, EARL.Assertion)
379
+ graph << RDF::Statement(a, EARL.subject, subject)
380
+ graph << RDF::Statement(a, EARL.test, solution[:test])
381
+ graph << RDF::Statement(a, EARL.assertedBy, solution[:by])
382
+ graph << RDF::Statement(a, EARL.mode, solution[:mode]) if solution[:mode]
383
+ r = RDF::Node.new
384
+ graph << RDF::Statement(a, EARL.result, r)
385
+ graph << RDF::Statement(r, RDF.type, EARL.TestResult)
386
+ graph << RDF::Statement(r, EARL.outcome, solution[:outcome])
387
+ end
388
+
389
+ # See if subject did not report results, which may indicate a formatting error in the EARL source
390
+ warn "No results found for #{subject} using #{ASSERTION_QUERY}" unless found_solutions
328
391
  end
329
- found_solutions[subject] = true
330
- assertion_stats["Found"] = assertion_stats["Found"].to_i + 1
331
-
332
- # Add this solution at the appropriate index within that list
333
- ndx = subjects.keys.find_index(subject)
334
- ary = test_assertion_lists[solution[:test]] ||= []
335
-
336
- ary[ndx] = a = RDF::Node.new
337
- graph << RDF::Statement(a, RDF.type, EARL.Assertion)
338
- graph << RDF::Statement(a, EARL.subject, subject)
339
- graph << RDF::Statement(a, EARL.test, solution[:test])
340
- graph << RDF::Statement(a, EARL.assertedBy, solution[:by])
341
- graph << RDF::Statement(a, EARL.mode, solution[:mode]) if solution[:mode]
342
- r = RDF::Node.new
343
- graph << RDF::Statement(a, EARL.result, r)
344
- graph << RDF::Statement(r, RDF.type, EARL.TestResult)
345
- graph << RDF::Statement(r, EARL.outcome, solution[:outcome])
346
392
  end
347
393
 
348
394
  # Add ordered assertions for each test
349
395
  test_assertion_lists.each do |test, ary|
396
+ ary[subjects.length - 1] ||= nil # extend for all subjects
350
397
  # Fill any missing entries with an untested outcome
351
398
  ary.each_with_index do |a, ndx|
352
399
  unless a
@@ -368,42 +415,17 @@ class EarlReport
368
415
 
369
416
  assertion_stats.each {|stat, count| status("Assertions #{stat}: #{count}")}
370
417
 
371
- # See if any subject did not report results, which may indicate a formatting error in the EARL source
372
- subjects.reject {|s| found_solutions[s]}.each do |sub|
373
- $stderr.puts "No results found for #{sub} using #{ASSERTION_QUERY}"
374
- end
375
-
376
418
  # Add report wrapper to graph
377
- ttl = %(
378
- @prefix dc: <http://purl.org/dc/terms/> .
379
- @prefix doap: <http://usefulinc.com/ns/doap#> .
380
- @prefix earl: <http://www.w3.org/ns/earl#> .
381
- @prefix mf: <http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#> .
382
- @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
383
-
384
- <> a earl:Software, doap:Project;
385
- doap:name #{quoted(@options.fetch(:name, 'Unknown'))};
386
- dc:bibliographicCitation "#{@options.fetch(:bibRef, 'Unknown reference')}";
387
- earl:generatedBy <http://rubygems.org/gems/earl-report>;
388
- earl:assertions #{subjects.values.map {|f| f.to_ntriples}.join(",\n ")};
389
- earl:testSubjects #{subjects.keys.map {|f| f.to_ntriples}.join(",\n ")};
390
- mf:entries (#{man_uris.map {|f| f.to_ntriples}.join("\n ")}) .
391
-
392
- <http://rubygems.org/gems/earl-report> a earl:Software, doap:Project;
393
- doap:name "earl-report";
394
- doap:shortdesc "Earl Report summary generator"@en;
395
- doap:description "EarlReport generates HTML+RDFa rollups of multiple EARL reports"@en;
396
- doap:homepage <https://github.com/gkellogg/earl-report>;
397
- doap:programming-language "Ruby";
398
- doap:license <http://unlicense.org>;
399
- doap:release <https://github.com/gkellogg/earl-report/tree/#{VERSION}>;
400
- doap:developer <http://greggkellogg.net/foaf#me> .
401
-
402
- <https://github.com/gkellogg/earl-report/tree/#{VERSION}> a doap:Version;
403
- doap:name "earl-report-#{VERSION}";
404
- doap:created "#{File.mtime(File.expand_path('../../VERSION', __FILE__)).strftime('%Y-%m-%d')}"^^xsd:date;
405
- doap:revision "#{VERSION}" .
406
- ).gsub(/^ /, '')
419
+ ttl = TURTLE_PREFIXES + %(
420
+ <> a earl:Software, doap:Project;
421
+ doap:name #{quoted(name)};
422
+ dc:bibliographicCitation "#{bibRef}";
423
+ earl:generatedBy <https://rubygems.org/gems/earl-report>;
424
+ earl:assertions #{subjects.values.map {|f| f.to_ntriples}.join(",\n ")};
425
+ earl:testSubjects #{subjects.keys.map {|f| f.to_ntriples}.join(",\n ")};
426
+ mf:entries (#{man_uris.map {|f| f.to_ntriples}.join("\n ")}) .
427
+ ).gsub(/^ /, '') +
428
+ TURTLE_SOFTWARE
407
429
  RDF::Turtle::Reader.new(ttl) {|r| graph << r}
408
430
 
409
431
  # Each manifest is an earl:Report
@@ -421,6 +443,8 @@ class EarlReport
421
443
  graph << RDF::Statement.new(u, RDF.type, EARL.TestCriterion)
422
444
  graph << RDF::Statement.new(u, RDF.type, EARL.TestCase)
423
445
  end
446
+
447
+ raise "Warnings issued in strict mode" if strict && @warnings > 0
424
448
  end
425
449
 
426
450
  ##
@@ -428,48 +452,47 @@ class EarlReport
428
452
  #
429
453
  # If no `io` option is provided, the output is returned as a string
430
454
  #
455
+ # @param [Symbol] format (:html)
456
+ # @param [IO] io (nil)
457
+ # `IO` to output results
431
458
  # @param [Hash{Symbol => Object}] options
432
- # @option options [Symbol] format (:html)
433
- # @option options[IO] :io
434
- # Optional `IO` to output results
459
+ # @param [String] template
460
+ # HAML template for generating report
435
461
  # @return [String] serialized graph, if `io` is nil
436
- def generate(options = {})
437
- options = {format: :html}.merge(options)
462
+ def generate(format: :html, io: nil, template: nil, **options)
438
463
 
439
- io = options[:io]
440
-
441
- status("generate: #{options[:format]}")
464
+ status("generate: #{format}")
442
465
  ##
443
466
  # Retrieve Hashed information in JSON-LD format
444
- case options[:format]
467
+ case format
445
468
  when :jsonld, :json
446
469
  json = json_hash.to_json(JSON::LD::JSON_STATE)
447
470
  io.write(json) if io
448
471
  json
449
472
  when :turtle, :ttl
450
473
  if io
451
- earl_turtle(options)
474
+ earl_turtle(io: io)
452
475
  else
453
476
  io = StringIO.new
454
- earl_turtle(options.merge(io: io))
477
+ earl_turtle(io: io)
455
478
  io.rewind
456
479
  io.read
457
480
  end
458
481
  when :html
459
- template = case options[:template]
460
- when String then options[:tempate]
461
- when IO, StringIO then options[:template].read
482
+ haml = case template
483
+ when String then template
484
+ when IO, StringIO then template.read
462
485
  else
463
486
  File.read(File.expand_path('../earl_report/views/earl_report.html.haml', __FILE__))
464
487
  end
465
488
 
466
489
  # Generate HTML report
467
- html = Haml::Engine.new(template, format: :xhtml).render(self, tests: json_hash)
490
+ html = Haml::Engine.new(haml, format: :xhtml).render(self, tests: json_hash)
468
491
  io.write(html) if io
469
492
  html
470
493
  else
471
- writer = RDF::Writer.for(options[:format])
472
- writer.dump(@graph, io, options.merge(standard_prefixes: true))
494
+ writer = RDF::Writer.for(format)
495
+ writer.dump(@graph, io, standard_prefixes: true, **options)
473
496
  end
474
497
  end
475
498
 
@@ -482,13 +505,15 @@ class EarlReport
482
505
  @json_hash ||= begin
483
506
  # Customized JSON-LD output
484
507
  result = JSON::LD::API.fromRDF(graph) do |expanded|
485
- framed = JSON::LD::API.frame(expanded, TEST_FRAME, expanded: true, embed: '@never')
508
+ framed = JSON::LD::API.frame(expanded, TEST_FRAME,
509
+ expanded: true,
510
+ embed: '@never',
511
+ pruneBlankNodeIdentifiers: false)
486
512
  # Reorder test subjects by @id
487
- #require 'byebug'; byebug
488
- framed['testSubjects'] = framed['testSubjects'].sort_by {|t| t['@id']}
513
+ framed['testSubjects'] = Array(framed['testSubjects']).sort_by {|t| t['@id']}
489
514
 
490
515
  # Reorder test assertions to make them consistent with subject order
491
- framed['entries'].each do |manifest|
516
+ Array(framed['entries']).each do |manifest|
492
517
  manifest['entries'].each do |test|
493
518
  test['assertions'] = test['assertions'].sort_by {|a| a['subject']}
494
519
  end
@@ -504,61 +529,119 @@ class EarlReport
504
529
 
505
530
  ##
506
531
  # Output consoloated EARL report as Turtle
507
- # @param [Hash{Symbol => Object}] options
508
- # @option options [IO, StringIO] :io
532
+ # @param [IO] io ($stdout)
533
+ # `IO` to output results
509
534
  # @return [String]
510
- def earl_turtle(options)
511
- io = options[:io]
535
+ def earl_turtle(io: $stdout)
536
+ context = JSON::LD::Context.parse(json_hash['@context'])
537
+ io.write(TURTLE_PREFIXES + "\n")
512
538
 
513
- top_level = graph.first_subject(predicate: EARL.generatedBy)
539
+ # Write project header
540
+ ttl_entity(io, json_hash, context)
514
541
 
515
- # Write starting with the entire graph to get preamble
516
- writer = RDF::Turtle::Writer.new(io, standard_prefixes: true)
517
- writer << graph
542
+ # Write out each manifest entry
543
+ io.puts("# Manifests")
544
+ json_hash['entries'].each do |man|
545
+ ttl_entity(io, man, context)
518
546
 
519
- writer.send(:preprocess)
520
- writer.send(:start_document)
521
-
522
- # Write top-level object referencing manifests and subjects
523
- writer.send(:statement, top_level)
547
+ # Output each test entry with assertions
548
+ man['entries'].each do |entry|
549
+ ttl_entity(io, entry, context)
550
+ end
551
+ end
524
552
 
525
- # Write each manifest
526
- io.puts "\n# Manifests"
527
- RDF::List.new(subject: graph.first_object(subject: top_level, predicate: MF[:entries]), graph: graph).each do |manifest|
528
- writer.send(:statement, manifest)
553
+ # Output each DOAP
554
+ json_hash['testSubjects'].each do |doap|
555
+ ttl_entity(io, doap, context)
529
556
 
530
- # Write each test case
531
- RDF::List.new(subject: graph.first_object(subject: manifest, predicate: MF[:entries]), graph: graph).each do |tc|
532
- writer.send(:statement, tc)
557
+ # FOAF
558
+ dev = doap['developer']
559
+ dev = [dev] unless dev.is_a?(Array)
560
+ dev.each do |foaf|
561
+ ttl_entity(io, foaf, context)
533
562
  end
534
563
  end
564
+
565
+ io.write(TURTLE_SOFTWARE)
566
+ end
535
567
 
536
- # Write test subjects
537
- io.puts "\n# Test Subjects"
538
- graph.query(subject: top_level, predicate: EARL.testSubjects).each do |s|
539
- writer.send(:statement, s.object)
568
+ def ttl_entity(io, entity, context)
569
+ io.write(ttl_value(entity) + " " + entity.map do |dk, dv|
570
+ case dk
571
+ when '@context', '@id'
572
+ nil
573
+ when '@type'
574
+ "a " + ttl_value(dv)
575
+ when 'assertions'
576
+ "earl:assertions #{dv.map {|a| ttl_assertion(a)}.join(", ")}"
577
+ when 'entries'
578
+ "mf:entries #{ttl_value({'@list' => dv}, whitespace: "\n ")}"
579
+ when 'release'
580
+ "doap:release [doap:revision #{quoted(dv['revision'])}]"
581
+ else
582
+ dv = [dv] unless dv.is_a?(Array)
583
+ dv = dv.map {|v| v.is_a?(Hash) ? v : context.expand_value(dk, v)}
584
+ "#{ttl_value(dk)} #{ttl_value(dv, whitespace: "\n ")}"
585
+ end
586
+ end.compact.join(" ;\n ") + " .\n\n")
587
+ end
540
588
 
541
- # Write each developer
542
- graph.query(subject: s.object, predicate: RDF::Vocab::DOAP.developer).each do |d|
543
- writer.send(:statement, d.object)
589
+ def ttl_value(value, whitespace: " ")
590
+ if value.is_a?(Array)
591
+ value.map {|v| ttl_value(v)}.join(",#{whitespace}")
592
+ elsif value.is_a?(Hash)
593
+ if value.key?('@list')
594
+ "(#{value['@list'].map {|vv| ttl_value(vv)}.join(whitespace)})"
595
+ elsif value.key?('@value')
596
+ quoted(value['@value'], language: value['@language'], datatype: value['@type'])
597
+ elsif value.key?('@id')
598
+ ttl_value(value['@id'])
599
+ else
600
+ "[]"
544
601
  end
602
+ elsif value.start_with?(/https?/) || value.start_with?('/')
603
+ "<#{value}>"
604
+ elsif value.include?(':')
605
+ value
606
+ elsif json_hash['@context'][value].is_a?(Hash)
607
+ json_hash['@context'][value].fetch('@id', "earl:#{value}")
608
+ elsif value.empty?
609
+ "<>"
610
+ else
611
+ "earl:#{value}"
545
612
  end
613
+ end
546
614
 
547
- # Write generator
548
- io.puts "\n# Report Generation Software"
549
- writer.send(:statement, RDF::URI("http://rubygems.org/gems/earl-report"))
550
- writer.send(:statement, RDF::URI("https://github.com/gkellogg/earl-report/tree/#{VERSION}"))
615
+ def ttl_assertion(value)
616
+ return ttl_value(value) if value.is_a?(String)
617
+ block = [
618
+ "[",
619
+ " a earl:Assertion ;",
620
+ " earl:test #{ttl_value(value['test'])} ;",
621
+ " earl:subject #{ttl_value(value['subject'])} ;",
622
+ " earl:result [",
623
+ " a earl:TestResult ;",
624
+ " earl:outcome #{ttl_value(value['result']['outcome'])}",
625
+ " ] ;",
626
+ ]
627
+ block << " earl:assertedBy #{ttl_value(value['assertedBy'])} ;" if value['assertedBy']
628
+
629
+ block.join("\n") + "\n ]"
551
630
  end
552
631
 
553
- def quoted(string)
554
- (@turtle_writer ||= RDF::Turtle::Writer.new).send(:quoted, string)
632
+ def quoted(string, language: nil, datatype: nil)
633
+ str = (@turtle_writer ||= RDF::Turtle::Writer.new).send(:quoted, string)
634
+ str += "@#{language}" if language
635
+ str += "^^#{ttl_value(datatype)}" if datatype
636
+ str
555
637
  end
556
638
 
557
639
  def warn(message)
640
+ @warnings += 1
558
641
  $stderr.puts message
559
642
  end
560
643
 
561
644
  def status(message)
562
- $stderr.puts message if @options[:verbose]
645
+ $stderr.puts message if verbose
563
646
  end
564
647
  end