From 1409f161793a289819df1f31eccb579b71f45475 Mon Sep 17 00:00:00 2001 From: David Robillard Date: Thu, 12 Aug 2021 22:28:59 -0400 Subject: Fix pretty-printing nested empty lists and add test suite The earlier "test" was just hitting the code without actually checking the output. This new suite is a set of pretty-printed documents which serd must reproduce from a model exactly to pass. This should make it easy to add cases in the future, since each case is just a document, as it should look. --- src/describe.c | 11 +- src/n3.c | 6 +- src/writer.c | 5 + test/good/manifest.ttl | 7 - test/good/pretty.trig | 98 ------------- test/good/test-pretty.nt | 46 ------- test/good/test-pretty.ttl | 44 ------ test/meson.build | 2 + test/pretty/anonymous-in-list-object.ttl | 11 ++ test/pretty/anonymous-object.ttl | 10 ++ test/pretty/anonymous-subject-and-object.ttl | 4 + test/pretty/anonymous-subject.ttl | 4 + test/pretty/empty-anonymous-object.ttl | 4 + test/pretty/empty-list-object.ttl | 4 + test/pretty/empty-list-subject-and-object.ttl | 4 + test/pretty/empty-list-subject.ttl | 4 + test/pretty/list-in-object.ttl | 10 ++ test/pretty/list-object.ttl | 8 ++ test/pretty/list-subject-with-extras.ttl | 9 ++ test/pretty/list-subject-with-list-extras.ttl | 12 ++ test/pretty/list-subject.ttl | 8 ++ test/pretty/manifest.ttl | 121 ++++++++++++++++ test/pretty/many-objects.ttl | 9 ++ test/pretty/meson.build | 17 +++ .../pretty/nested-list-object-with-empty-lists.ttl | 11 ++ test/pretty/nested-list-object.ttl | 13 ++ test/pretty/nested-list-subject.ttl | 13 ++ test/run_pretty_suite.py | 153 +++++++++++++++++++++ 28 files changed, 446 insertions(+), 202 deletions(-) delete mode 100644 test/good/pretty.trig delete mode 100644 test/good/test-pretty.nt delete mode 100644 test/good/test-pretty.ttl create mode 100644 test/pretty/anonymous-in-list-object.ttl create mode 100644 test/pretty/anonymous-object.ttl create mode 100644 test/pretty/anonymous-subject-and-object.ttl create mode 100644 test/pretty/anonymous-subject.ttl create mode 100644 test/pretty/empty-anonymous-object.ttl create mode 100644 test/pretty/empty-list-object.ttl create mode 100644 test/pretty/empty-list-subject-and-object.ttl create mode 100644 test/pretty/empty-list-subject.ttl create mode 100644 test/pretty/list-in-object.ttl create mode 100644 test/pretty/list-object.ttl create mode 100644 test/pretty/list-subject-with-extras.ttl create mode 100644 test/pretty/list-subject-with-list-extras.ttl create mode 100644 test/pretty/list-subject.ttl create mode 100644 test/pretty/manifest.ttl create mode 100644 test/pretty/many-objects.ttl create mode 100644 test/pretty/meson.build create mode 100644 test/pretty/nested-list-object-with-empty-lists.ttl create mode 100644 test/pretty/nested-list-object.ttl create mode 100644 test/pretty/nested-list-subject.ttl create mode 100755 test/run_pretty_suite.py diff --git a/src/describe.c b/src/describe.c index 9bd9d3ae..718eaf1c 100644 --- a/src/describe.c +++ b/src/describe.c @@ -204,7 +204,7 @@ write_range_statement(const SerdSink* const sink, // First write inline list subject, which this statement will follow if (zix_hash_insert(list_subjects, subject) != ZIX_STATUS_EXISTS) { st = write_list( - sink, model, list_subjects, 2, SERD_LIST_S, subject, graph); + sink, model, list_subjects, 2, flags | SERD_LIST_S, subject, graph); } } } @@ -226,9 +226,12 @@ write_range_statement(const SerdSink* const sink, serd_cursor_free(iter); } else if (object_style == LIST_O) { // Write list object like "( ... )" - flags |= SERD_LIST_O; - if (!(st = serd_sink_write_statement(sink, flags, statement))) { - st = write_list(sink, model, list_subjects, depth + 1, 0, object, graph); + if (!(st = + serd_sink_write_statement(sink, flags | SERD_LIST_O, statement))) { + flags = flags & ~((unsigned)SERD_LIST_S); + + st = + write_list(sink, model, list_subjects, depth + 1, flags, object, graph); } } else { diff --git a/src/n3.c b/src/n3.c index ff142976..3b12dfec 100644 --- a/src/n3.c +++ b/src/n3.c @@ -866,11 +866,11 @@ read_collection(SerdReader* const reader, return SERD_ERR_OVERFLOW; } - if (ctx.subject) { - // subject predicate _:head + if (ctx.subject) { // Reading a collection object *ctx.flags |= (end ? 0 : SERD_LIST_O); TRY(st, emit_statement(reader, ctx, *dest)); - } else { + *ctx.flags &= ~((unsigned)SERD_LIST_O); + } else { // Reading a collection subject *ctx.flags |= (end ? 0 : SERD_LIST_S); } diff --git a/src/writer.c b/src/writer.c index 5f9fdaab..452d3679 100644 --- a/src/writer.c +++ b/src/writer.c @@ -1064,6 +1064,11 @@ write_list_statement(SerdWriter* const writer, SerdStatus st = SERD_SUCCESS; bool is_end = false; + if (serd_node_equals(predicate, writer->world->rdf_first) && + serd_node_equals(object, writer->world->rdf_nil)) { + return esink("()", 2, writer); + } + TRY(st, write_list_obj(writer, flags, predicate, object, &is_end)); if (is_end) { pop_context(writer); diff --git a/test/good/manifest.ttl b/test/good/manifest.ttl index b546b9f7..0198a8c8 100644 --- a/test/good/manifest.ttl +++ b/test/good/manifest.ttl @@ -46,7 +46,6 @@ <#test-non-curie-uri> <#test-num> <#test-prefix> - <#test-pretty> <#test-rel> <#test-semi-dot> <#test-uri-escape> @@ -276,12 +275,6 @@ mf:action ; mf:result . -<#test-pretty> - rdf:type rdft:TestTurtleEval ; - mf:name "test-pretty" ; - mf:action ; - mf:result . - <#test-rel> rdf:type rdft:TestTurtleEval ; mf:name "test-rel" ; diff --git a/test/good/pretty.trig b/test/good/pretty.trig deleted file mode 100644 index 140769ce..00000000 --- a/test/good/pretty.trig +++ /dev/null @@ -1,98 +0,0 @@ -@prefix : . -@prefix rdf: . - -:a - :b :c , - :d , - :e ; - :f :g , - :h . - -( - 1 -) - :isA :List . - -[] - :isA :Blank . - -( - 2 -) - :sameAs ( - 2 - ) . - -[] - :sameAs [] . - -( - 1 - 2 -) - a :List ; - rdf:value 3 . - -( - ( - 3 - ) - ( - 4 - ) -) - a :NestedList ; - :sum 7 . - -[ - a :BlankSubject -] - a rdf:Resource . - -[ - a :BlankSubject -] . - -[] - :blank [ - :nestedEmptyBlank [] ; - :nestedNonEmptyBlanks [ - rdf:value 1 - ] , [ - rdf:value 2 - ] - ] ; - :lists ( - 3 - 4 - ) , ( - 5 - 6 - ) , ( - [ - rdf:value 7 - ] - [ - rdf:value 8 - ] - ) . - -:s - a :Thing ; - :predicate1 :object1 , - [ - a :SubThing ; - :predicate2 :object2 - ] , [ - a :OtherSubThing ; - :p3 :o3 - ] ; - :p4 :o4 . - -eg:graph { - :a - :b :c ; - :d [ - :e :f - ] . -} diff --git a/test/good/test-pretty.nt b/test/good/test-pretty.nt deleted file mode 100644 index 9251563a..00000000 --- a/test/good/test-pretty.nt +++ /dev/null @@ -1,46 +0,0 @@ - . -_:b1 . - . -_:b2 _:b3 . -_:b4 "apple" . -_:b4 _:b5 . -_:b5 "banana" . -_:b5 _:b6 . -_:b6 "pear" . -_:b6 . -_:b4 . -_:b7 _:b8 . -_:b8 . -_:b8 _:b9 . -_:b9 . -_:b9 . -_:b7 _:b10 . -_:b10 _:b11 . -_:b11 . -_:b11 _:b12 . -_:b12 . -_:b12 . -_:b10 . -_:b7 . -_:b13 _:b14 . -_:b14 "apple" . -_:b14 _:b15 . -_:b15 "banana" . -_:b15 _:b16 . -_:b16 "pear" . -_:b16 . -_:b17 . -_:b17 . -_:b17 . -_:b18 _:b19 . -_:b19 . -_:b19 . -_:b18 _:b20 . -_:b20 . -_:b21 _:b22 . -_:b22 _:b23 . -_:b23 . -_:b22 _:b24 . -_:b24 _:b25 . -_:b25 . -_:b24 . diff --git a/test/good/test-pretty.ttl b/test/good/test-pretty.ttl deleted file mode 100644 index 4eb7204f..00000000 --- a/test/good/test-pretty.ttl +++ /dev/null @@ -1,44 +0,0 @@ -@prefix : . - -() :isA :List . - -[] :isA :Blank . - -() :sameAs () . - -[] :sameAs [] . - -( - "apple" - "banana" - "pear" -) a :List . - -( - (:a :b) - (:c :d) -) a :List . - -[] - :list ( - "apple" - "banana" - "pear" - ) . - -[] - :a :b , :c , :d . - -[] - :a [ - :b :c ; - :d :e ; - ] , [ - :f :g - ] . - -[] - :list ( - [ a :Apple ] - [ a :Banana ] - ) . \ No newline at end of file diff --git a/test/meson.build b/test/meson.build index c0ed56e4..c314e1d4 100644 --- a/test/meson.build +++ b/test/meson.build @@ -2,6 +2,7 @@ autoship = find_program('autoship', required: false) run_filter_suite = find_program('run_filter_suite.py') run_pipe_suite = find_program('run_pipe_suite.py') +run_pretty_suite = find_program('run_pretty_suite.py') run_sort_suite = find_program('run_sort_suite.py') wrapper = meson.get_cross_property('exe_wrapper', '') @@ -404,6 +405,7 @@ if is_variable('serd_pipe') and is_variable('serd_sort') subdir('good') subdir('lax') subdir('pattern') + subdir('pretty') subdir('terse') endif diff --git a/test/pretty/anonymous-in-list-object.ttl b/test/pretty/anonymous-in-list-object.ttl new file mode 100644 index 00000000..a6b202f4 --- /dev/null +++ b/test/pretty/anonymous-in-list-object.ttl @@ -0,0 +1,11 @@ +@prefix eg: . + +eg:s + eg:p ( + [ + a eg:Spy + ] + [ + a eg:Ninja + ] + ) . diff --git a/test/pretty/anonymous-object.ttl b/test/pretty/anonymous-object.ttl new file mode 100644 index 00000000..f592fed1 --- /dev/null +++ b/test/pretty/anonymous-object.ttl @@ -0,0 +1,10 @@ +@prefix eg: . + +eg:s + eg:p1 [ + eg:p2 eg:o2 ; + eg:p3 eg:o3 + ] , [ + eg:p4 eg:o4 ; + eg:p5 eg:o5 + ] . diff --git a/test/pretty/anonymous-subject-and-object.ttl b/test/pretty/anonymous-subject-and-object.ttl new file mode 100644 index 00000000..8904cca6 --- /dev/null +++ b/test/pretty/anonymous-subject-and-object.ttl @@ -0,0 +1,4 @@ +@prefix eg: . + +[] + eg:p [] . diff --git a/test/pretty/anonymous-subject.ttl b/test/pretty/anonymous-subject.ttl new file mode 100644 index 00000000..e0e467e2 --- /dev/null +++ b/test/pretty/anonymous-subject.ttl @@ -0,0 +1,4 @@ +@prefix eg: . + +[] + eg:p eg:o . diff --git a/test/pretty/empty-anonymous-object.ttl b/test/pretty/empty-anonymous-object.ttl new file mode 100644 index 00000000..f3b8d7f7 --- /dev/null +++ b/test/pretty/empty-anonymous-object.ttl @@ -0,0 +1,4 @@ +@prefix eg: . + +eg:s + eg:p [] . diff --git a/test/pretty/empty-list-object.ttl b/test/pretty/empty-list-object.ttl new file mode 100644 index 00000000..b4045ec0 --- /dev/null +++ b/test/pretty/empty-list-object.ttl @@ -0,0 +1,4 @@ +@prefix eg: . + +eg:s + eg:list () . diff --git a/test/pretty/empty-list-subject-and-object.ttl b/test/pretty/empty-list-subject-and-object.ttl new file mode 100644 index 00000000..3b84980e --- /dev/null +++ b/test/pretty/empty-list-subject-and-object.ttl @@ -0,0 +1,4 @@ +@prefix eg: . + +() + eg:list () . diff --git a/test/pretty/empty-list-subject.ttl b/test/pretty/empty-list-subject.ttl new file mode 100644 index 00000000..0610eb07 --- /dev/null +++ b/test/pretty/empty-list-subject.ttl @@ -0,0 +1,4 @@ +@prefix eg: . + +() + a eg:ExampleList . diff --git a/test/pretty/list-in-object.ttl b/test/pretty/list-in-object.ttl new file mode 100644 index 00000000..ce887157 --- /dev/null +++ b/test/pretty/list-in-object.ttl @@ -0,0 +1,10 @@ +@prefix eg: . + +eg:s + eg:p [ + eg:list ( + "apple" + "banana" + "cherry" + ) + ] . diff --git a/test/pretty/list-object.ttl b/test/pretty/list-object.ttl new file mode 100644 index 00000000..735d9df6 --- /dev/null +++ b/test/pretty/list-object.ttl @@ -0,0 +1,8 @@ +@prefix eg: . + +eg:s + eg:list ( + "apple" + "banana" + "cherry" + ) . diff --git a/test/pretty/list-subject-with-extras.ttl b/test/pretty/list-subject-with-extras.ttl new file mode 100644 index 00000000..dcfb8753 --- /dev/null +++ b/test/pretty/list-subject-with-extras.ttl @@ -0,0 +1,9 @@ +@prefix eg: . + +( + "apple" + "banana" + "cherry" +) + eg:with eg:someProperties ; + a eg:ExampleList . diff --git a/test/pretty/list-subject-with-list-extras.ttl b/test/pretty/list-subject-with-list-extras.ttl new file mode 100644 index 00000000..0a0cd0e0 --- /dev/null +++ b/test/pretty/list-subject-with-list-extras.ttl @@ -0,0 +1,12 @@ +@prefix eg: . + +( + "apple" + "banana" + "cherry" +) + eg:list ( + "asparagus" + "beet" + "carrot" + ) . diff --git a/test/pretty/list-subject.ttl b/test/pretty/list-subject.ttl new file mode 100644 index 00000000..eb1c7a2e --- /dev/null +++ b/test/pretty/list-subject.ttl @@ -0,0 +1,8 @@ +@prefix eg: . + +( + "apple" + "banana" + "cherry" +) + a eg:ExampleList . diff --git a/test/pretty/manifest.ttl b/test/pretty/manifest.ttl new file mode 100644 index 00000000..faae5857 --- /dev/null +++ b/test/pretty/manifest.ttl @@ -0,0 +1,121 @@ +@prefix mf: . +@prefix rdfs: . +@prefix rdft: . +@prefix serd: . + +<> + a mf:Manifest ; + rdfs:comment "Serd pretty-printing test cases" ; + mf:entries ( + <#anonymous-in-list-object> + <#anonymous-object> + <#anonymous-subject-and-object> + <#anonymous-subject> + <#empty-anonymous-object> + <#empty-list-object> + <#empty-list-subject-and-object> + <#empty-list-subject> + <#list-in-object> + <#list-object> + <#list-subject-with-extras> + <#list-subject-with-list-extras> + <#list-subject> + <#many-objects> + <#nested-list-object-with-empty-lists> + <#nested-list-object> + <#nested-list-subject> + ) . + +<#anonymous-in-list-object> + a serd:TestTurtlePrint ; + mf:action ; + mf:name "anonymous-in-list-object" . + +<#anonymous-object> + a serd:TestTurtlePrint ; + mf:action ; + mf:name "anonymous-object" . + +<#anonymous-subject> + a serd:TestTurtlePrint ; + mf:action ; + mf:name "anonymous-subject" . + +<#anonymous-subject-and-object> + a serd:TestTurtlePrint ; + mf:action ; + mf:name "anonymous-subject-and-object" . + +<#empty-anonymous-object> + a serd:TestTurtlePrint ; + mf:action ; + mf:name "empty-anonymous-object" . + +<#empty-list-object> + a serd:TestTurtlePrint ; + mf:action ; + mf:name "empty-list-object" . + +<#empty-list-subject> + a serd:TestTurtlePrint ; + mf:action ; + mf:name "empty-list-subject" . + +<#empty-list-subject-and-object> + a serd:TestTurtlePrint ; + mf:action ; + mf:name "empty-list-subject-and-object" . + +<#list-in-object> + a serd:TestTurtlePrint ; + mf:action ; + mf:name "list-in-object" . + +<#list-object> + a serd:TestTurtlePrint ; + mf:action ; + mf:name "list-object" . + +<#list-subject> + a serd:TestTurtlePrint ; + mf:action ; + mf:name "list-subject" . + +<#list-subject-with-extras> + a serd:TestTurtlePrint ; + mf:action ; + mf:name "list-subject-with-extras" . + +<#list-subject-with-list-extras> + a serd:TestTurtlePrint ; + mf:action ; + mf:name "list-subject-with-list-extras" . + +<#many-objects> + a serd:TestTurtlePrint ; + mf:action ; + mf:name "many-objects" . + +<#nested-list-object> + a serd:TestTurtlePrint ; + mf:action ; + mf:name "nested-list-object" . + +<#nested-list-object-with-empty-lists> + a serd:TestTurtlePrint ; + mf:action ; + mf:name "nested-list-object-with-empty-lists" . + +<#nested-list-subject> + a serd:TestTurtlePrint ; + mf:action ; + mf:name "nested-list-subject" . + +serd:TestTurtlePrint + a rdfs:Class ; + rdfs:comment "Tests that a Turtle document pretty-prints exactly as written." ; + rdfs:label "Turtle Pretty-Printing" ; + rdfs:subClassOf rdft:Test . + +rdft:Test + rdfs:subClassOf mf:ManifestEntry . diff --git a/test/pretty/many-objects.ttl b/test/pretty/many-objects.ttl new file mode 100644 index 00000000..534741dd --- /dev/null +++ b/test/pretty/many-objects.ttl @@ -0,0 +1,9 @@ +@prefix eg: . + +eg:s + eg:p1 "apple" , + "banana" , + "cherry" ; + eg:p2 "asparagus" , + "beet" , + "carrot" . diff --git a/test/pretty/meson.build b/test/pretty/meson.build new file mode 100644 index 00000000..634f1e0e --- /dev/null +++ b/test/pretty/meson.build @@ -0,0 +1,17 @@ +base_uri = 'http://drobilla.net/sw/serd/test/good/' + +args = [files('manifest.ttl')] + +test('pretty', + run_pretty_suite, + args: pipe_test_script_args + ['-o', meson.current_build_dir() / 'pipe'] + args, + env: test_env, + suite: ['suite', 'extra', 'pipe'], + timeout: 240) + +test('pretty', + run_pretty_suite, + args: sort_test_script_args + ['-o', meson.current_build_dir() / 'sort'] + args, + env: test_env, + suite: ['suite', 'extra', 'sort'], + timeout: 240) diff --git a/test/pretty/nested-list-object-with-empty-lists.ttl b/test/pretty/nested-list-object-with-empty-lists.ttl new file mode 100644 index 00000000..4760fd2b --- /dev/null +++ b/test/pretty/nested-list-object-with-empty-lists.ttl @@ -0,0 +1,11 @@ +@prefix eg: . + +eg:s + eg:list ( + ( + () + ) + ( + () + ) + ) . diff --git a/test/pretty/nested-list-object.ttl b/test/pretty/nested-list-object.ttl new file mode 100644 index 00000000..c4b1898f --- /dev/null +++ b/test/pretty/nested-list-object.ttl @@ -0,0 +1,13 @@ +@prefix eg: . + +eg:s + eg:list ( + ( + eg:l1e1 + eg:l1e2 + ) + ( + eg:l2e1 + eg:l2e2 + ) + ) . diff --git a/test/pretty/nested-list-subject.ttl b/test/pretty/nested-list-subject.ttl new file mode 100644 index 00000000..b32aa133 --- /dev/null +++ b/test/pretty/nested-list-subject.ttl @@ -0,0 +1,13 @@ +@prefix eg: . + +( + ( + eg:l1e1 + eg:l1e2 + ) + ( + eg:l2e1 + eg:l2e2 + ) +) + a eg:ExampleList . diff --git a/test/run_pretty_suite.py b/test/run_pretty_suite.py new file mode 100755 index 00000000..a17dd0f8 --- /dev/null +++ b/test/run_pretty_suite.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 + +"""Run the RDF-based test suite for serd-filter.""" + +import serd_test_util + +import argparse +import datetime +import difflib +import itertools +import os +import re +import shlex +import subprocess +import sys +import tempfile +import urllib.parse + + +def log_error(message): + """Log an error message to stderr""" + + sys.stderr.write("error: ") + sys.stderr.write(message) + + +def _uri_path(test_dir, uri): + path = urllib.parse.urlparse(uri).path + drive = os.path.splitdrive(path[1:])[0] + path = path if not drive else path[1:] + return os.path.join(test_dir, os.path.basename(path)) + + +def test_suite( + manifest_path, + base_uri, + command_prefix, + out_dir, +): + """Run all tests in the manifest.""" + + mf = "http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#" + suite_dir = os.path.dirname(manifest_path) + + model, instances = serd_test_util.load_rdf( + command_prefix + ["-B", base_uri], manifest_path + ) + + os.makedirs(out_dir, exist_ok=True) + + class Results: + def __init__(self): + self.n_tests = 0 + self.n_failures = 0 + + def run_test(entry, results): + """Run a single test entry from the manifest.""" + + input_uri = model[entry][mf + "action"][0] + input_path = _uri_path(suite_dir, input_uri) + + output_path = os.path.join(out_dir, os.path.basename(input_path)) + + command = command_prefix + [ + "-B", + base_uri, + "-O", + "turtle", + "-o", + output_path, + input_path, + ] + + # Read the flat input and pretty-print output + results.n_tests += 1 + try: + subprocess.run(command, check=True) + + # Check that the output is exactly the same as the expected result + if not serd_test_util.file_equals(input_path, output_path): + results.n_failures += 1 + log_error( + "Output {} differs from {}\n".format( + output_path, input_path + ) + ) + + except Exception as e: + log_error(str(e)) + results.n_failures += 1 + + # Run all test types in the test suite + results = Results() + for klass, instances in instances.items(): + if klass == "http://drobilla.net/ns/serd#TestTurtlePrint": + for entry in instances: + run_test(entry, results) + + # Print result summary + if results.n_failures > 0: + log_error( + "{}/{} tests failed\n".format(results.n_failures, results.n_tests) + ) + else: + sys.stdout.write("All {} tests passed\n".format(results.n_tests)) + + return results.n_failures + + +def main(): + """Run the command line tool.""" + + parser = argparse.ArgumentParser( + usage="%(prog)s [OPTION]... MANIFEST -- [TOOL_OPTION]...", + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + parser.add_argument("--tool", default="tools/serd-sort", help="executable") + parser.add_argument("--wrapper", default="", help="executable wrapper") + + parser.add_argument( + "-o", "--out-dir", default="test/pretty", help="output directory" + ) + + parser.add_argument( + "--base-uri", + default="http://drobilla.net/sw/serd/test/", + help="base URI", + ) + parser.add_argument("manifest", help="test suite manifest.ttl file") + + args = parser.parse_args(sys.argv[1:]) + wrapper_prefix = shlex.split(args.wrapper) + command_prefix = wrapper_prefix + [args.tool] + + return test_suite( + args.manifest, + args.base_uri, + command_prefix, + args.out_dir, + ) + + +if __name__ == "__main__": + try: + sys.exit(main()) + except subprocess.CalledProcessError as e: + if e.stderr is not None: + sys.stderr.write(e.stderr.decode("utf-8")) + + sys.stderr.write("error: %s\n" % e) + sys.exit(e.returncode) -- cgit v1.2.1