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 +4 -4
- data/README.md +15 -10
- data/VERSION +1 -1
- data/bin/earl-report +4 -2
- data/lib/earl_report.rb +250 -167
- data/lib/earl_report/views/earl_report.html.haml +17 -14
- data/spec/earl_report_spec.rb +117 -29
- data/spec/spec_helper.rb +15 -8
- data/spec/test-files/doap.ttl +11 -11
- data/spec/test-files/foaf.ttl +2 -2
- data/spec/test-files/report-complete.ttl +15 -15
- data/spec/test-files/report-no-doap.ttl +4 -4
- data/spec/test-files/report-no-foaf.ttl +14 -14
- data/spec/test-files/report-no-test.ttl +48 -0
- data/spec/test-files/results.html +29 -31
- data/spec/test-files/results.jsonld +73 -73
- data/spec/test-files/results.ttl +61 -74
- metadata +36 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 77b18a547a9e1721a2c32e83abc8230dfb5c8a4a7170874cda1b4cf41d50d4d8
|
4
|
+
data.tar.gz: 04654044c3dfca443518097550ef07bd68b74203ca9dfc239a49c1a62a5c7556
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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://
|
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 <
|
21
|
-
earl:subject <
|
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
|
-
<
|
32
|
+
<https://rubygems.org/gems/rdf-turtle> a doap:Project, earl:TestSubject, earl:Software ;
|
33
33
|
doap:name "RDF::Turtle" ;
|
34
|
-
doap:developer <
|
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
|
-
<
|
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 <
|
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 <
|
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](
|
135
|
+
* [Gregg Kellogg](https://github.com/gkellogg) - <https://greggkellogg.net/>
|
131
136
|
|
132
137
|
## License
|
133
138
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
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
|
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
|
-
#
|
161
|
-
#
|
162
|
-
# @
|
163
|
-
#
|
164
|
-
#
|
165
|
-
#
|
166
|
-
#
|
167
|
-
#
|
168
|
-
#
|
169
|
-
#
|
170
|
-
#
|
171
|
-
#
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
182
|
-
if
|
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 #{
|
235
|
+
status "read #{manifest.inspect}"
|
198
236
|
man_opts = {}
|
199
|
-
man_opts[:base_uri] = RDF::URI(
|
237
|
+
man_opts[:base_uri] = RDF::URI(base) if base
|
200
238
|
@graph = RDF::Graph.new
|
201
|
-
Array(
|
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(
|
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
|
-
"#{
|
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
|
-
|
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
|
-
|
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 "
|
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 "
|
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
|
-
|
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,
|
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
|
-
|
298
|
-
|
299
|
-
|
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
|
-
|
303
|
-
|
304
|
-
|
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
|
-
|
307
|
-
|
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
|
-
|
310
|
-
|
311
|
-
|
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
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
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
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
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
|
-
# @
|
433
|
-
#
|
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(
|
437
|
-
options = {format: :html}.merge(options)
|
462
|
+
def generate(format: :html, io: nil, template: nil, **options)
|
438
463
|
|
439
|
-
|
440
|
-
|
441
|
-
status("generate: #{options[:format]}")
|
464
|
+
status("generate: #{format}")
|
442
465
|
##
|
443
466
|
# Retrieve Hashed information in JSON-LD format
|
444
|
-
case
|
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(
|
474
|
+
earl_turtle(io: io)
|
452
475
|
else
|
453
476
|
io = StringIO.new
|
454
|
-
earl_turtle(
|
477
|
+
earl_turtle(io: io)
|
455
478
|
io.rewind
|
456
479
|
io.read
|
457
480
|
end
|
458
481
|
when :html
|
459
|
-
|
460
|
-
when String then
|
461
|
-
when IO, StringIO then
|
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(
|
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(
|
472
|
-
writer.dump(@graph, io,
|
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,
|
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
|
-
|
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 [
|
508
|
-
#
|
532
|
+
# @param [IO] io ($stdout)
|
533
|
+
# `IO` to output results
|
509
534
|
# @return [String]
|
510
|
-
def earl_turtle(
|
511
|
-
|
535
|
+
def earl_turtle(io: $stdout)
|
536
|
+
context = JSON::LD::Context.parse(json_hash['@context'])
|
537
|
+
io.write(TURTLE_PREFIXES + "\n")
|
512
538
|
|
513
|
-
|
539
|
+
# Write project header
|
540
|
+
ttl_entity(io, json_hash, context)
|
514
541
|
|
515
|
-
# Write
|
516
|
-
|
517
|
-
|
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
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
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
|
-
#
|
526
|
-
|
527
|
-
|
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
|
-
#
|
531
|
-
|
532
|
-
|
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
|
-
|
537
|
-
io.
|
538
|
-
|
539
|
-
|
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
|
-
|
542
|
-
|
543
|
-
|
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
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
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
|
645
|
+
$stderr.puts message if verbose
|
563
646
|
end
|
564
647
|
end
|