diff --git a/quit/core.py b/quit/core.py index 34950746..d32dd64f 100644 --- a/quit/core.py +++ b/quit/core.py @@ -414,6 +414,21 @@ def getFileReferenceAndContext(self, blob, commit): return quitWorkingData return self._blobs.get(blob) + def applyQueryOnCommit(self, parsedQuery, parent_commit_ref, target_ref, query=None, + default_graph=[], named_graph=[]): + """Apply an update query on the graph and the git repository.""" + graph, commitid = self.instance(parent_commit_ref) + resultingChanges, exception = graph.update(parsedQuery) + if exception: + # TODO need to revert or invalidate the graph at this point. + pass + oid = self.commit(graph, resultingChanges, 'New Commit from QuitStore', parent_commit_ref, + target_ref, query=query, default_graph=default_graph, + named_graph=named_graph) + if exception: + raise exception + return oid + def commit(self, graph, delta, message, parent_commit_ref, target_ref, query=None, default_graph=[], named_graph=[], **kwargs): """Commit changes after applying deltas to the blobs. diff --git a/quit/git.py b/quit/git.py index 1ebce394..d3d95bf4 100644 --- a/quit/git.py +++ b/quit/git.py @@ -3,7 +3,7 @@ import re import logging -from _pygit2 import GitError +from _pygit2 import GitError, Oid from os.path import expanduser, join from quit.exceptions import RepositoryNotFound, RevisionNotFound, NodeNotFound, RemoteNotFound from quit.exceptions import QuitGitRefNotFound, QuitGitRepoError, QuitGitPushError @@ -117,7 +117,13 @@ def lookup(self, name): except KeyError: pass except ValueError: - return self._repository.get(name).id + pass + try: + revison = self._repository.get(name) + if revison: + return revison.id + except Exception as e: + logger.exception(e) raise RevisionNotFound(name) def revision(self, id='HEAD'): @@ -278,9 +284,9 @@ def pull(self, remote_name=None, refspec=None, **kwargs): local_branch = groups.group("dst") logger.debug("pull: parsed refspec is: {}, {}, {}".format(plus, remote_branch, local_branch)) - remote_master_id = self.fetch(remote_name=remote_name, remote_branch=remote_branch) - if remote_master_id is not None: - self.merge(target=local_branch, branch=remote_master_id, **kwargs) + remote_reference = self.fetch(remote_name=remote_name, remote_branch=remote_branch) + if remote_reference is not None: + self.merge(target=local_branch, branch=remote_reference, **kwargs) def push(self, remote_name=None, refspec=None): """Push changes on a local repository to a remote repository. diff --git a/quit/web/modules/endpoint.py b/quit/web/modules/endpoint.py index 6db967de..58b8ccd8 100644 --- a/quit/web/modules/endpoint.py +++ b/quit/web/modules/endpoint.py @@ -8,7 +8,10 @@ from quit.helpers import parse_sparql_request, parse_query_type from quit.web.app import render_template, feature_required from quit.exceptions import UnSupportedQuery, SparqlProtocolError, NonAbsoluteBaseError -from quit.exceptions import FromNamedError +from quit.exceptions import FromNamedError, QuitMergeConflict, RevisionNotFound +import datetime +import uuid +import base64 logger = logging.getLogger('quit.modules.endpoint') @@ -89,56 +92,106 @@ def sparql(branch_or_ref): except SparqlProtocolError: return make_response('Sparql Protocol Error', 400) - try: - graph, commitid = quit.instance(branch_or_ref) - except Exception as e: - logger.exception(e) - return make_response('No branch or reference given.', 400) - if queryType in ['InsertData', 'DeleteData', 'Modify', 'DeleteWhere', 'Load']: - res, exception = graph.update(parsedQuery) + if branch_or_ref: + commit_id = quit.repository.revision(branch_or_ref).id + else: + commit_id = None + + parent_commit_id = request.values.get('parent_commit_id', None) or None + if parent_commit_id and parent_commit_id != commit_id: + resolution_method = request.values.get('resolution_method', None) or None + if resolution_method == "reject": + logger.debug("rejecting update because {} is at {} but {} was expected".format( + branch_or_ref, commit_id, parent_commit_id)) + return make_response('reject', 409) # alternative 412 + elif resolution_method in ("merge", "branch"): + logger.debug(("writing update to a branch of {} because it is at {} but {} was " + "expected").format(branch_or_ref, commit_id, parent_commit_id)) + try: + quit.repository.lookup(parent_commit_id) + except RevisionNotFound: + return make_response("The provided parent commit (parent_commit_id={}) " + "could not be found.".format(parent_commit_id), 400) + + time = datetime.datetime.now().strftime('%Y-%m-%d-%H%M%S') + shortUUID = (base64.urlsafe_b64encode(uuid.uuid1().bytes).decode("utf-8") + ).rstrip('=\n').replace('/', '_') + target_branch = "tmp/{}_{}".format(time, shortUUID) + target_ref = "refs/heads/" + target_branch + logger.debug("target ref is: {}".format(target_ref)) + oid = quit.applyQueryOnCommit(parsedQuery, parent_commit_id, target_ref, + query=query, default_graph=default_graph, + named_graph=named_graph) + + if resolution_method == "merge": + logger.debug(("going to merge update into {} because it is at {} but {} was " + "expected").format(branch_or_ref, commit_id, parent_commit_id)) + try: + quit.repository.merge(target=branch_or_ref, branch=target_ref) + oid = quit.repository.revision(branch_or_ref).id + # delete temporary branch + tmp_branch = quit.repository._repository.branches.get(target_branch) + tmp_branch.delete() + response = make_response('success', 200) + target_branch = branch_or_ref + except QuitMergeConflict as e: + response = make_response('merge failed', 400) + else: + response = make_response('branched', 200) + response.headers["X-CurrentBranch"] = target_branch + response.headers["X-CurrentCommit"] = oid + return response + + # Add info about temporary branch + else: + graph, commitid = quit.instance(parent_commit_id) - try: target_head = request.values.get('target_head', branch_or_ref) or default_branch target_ref = 'refs/heads/{}'.format(target_head) - - oid = quit.commit(graph, res, 'New Commit from QuitStore', branch_or_ref, - target_ref, query=query, default_graph=default_graph, - named_graph=named_graph) - if exception is not None: - logger.exception(exception) - return 'Update query not executed (completely), (detected UnSupportedQuery)', 400 - response = make_response('', 200) - response.headers["X-CurrentBranch"] = target_ref - if oid is not None: - response.headers["X-CurrentCommit"] = oid - else: - response.headers["X-CurrentCommit"] = commitid - return response + try: + oid = quit.applyQueryOnCommit(parsedQuery, branch_or_ref, target_ref, + query=query, default_graph=default_graph, + named_graph=named_graph) + response = make_response('', 200) + response.headers["X-CurrentBranch"] = target_head + if oid is not None: + response.headers["X-CurrentCommit"] = oid + else: + response.headers["X-CurrentCommit"] = commitid + return response + except Exception as e: + # query ok, but unsupported query type or other problem during commit + logger.exception(e) + return make_response('Error after executing the update query.', 400) + elif queryType in ['SelectQuery', 'DescribeQuery', 'AskQuery', 'ConstructQuery']: + try: + graph, commitid = quit.instance(branch_or_ref) except Exception as e: - # query ok, but unsupported query type or other problem during commit logger.exception(e) - return make_response('Error after executing the update query.', 400) - elif queryType in ['SelectQuery', 'DescribeQuery', 'AskQuery', 'ConstructQuery']: + return make_response('No branch or reference given.', 400) + try: res = graph.query(parsedQuery) except FromNamedError: return make_response('FROM NAMED not supported, yet', 400) except UnSupportedQuery: return make_response('Unsupported Query', 400) - else: - logger.debug("Unsupported Type: {}".format(queryType)) - return make_response("Unsupported Query Type: {}".format(queryType), 400) - mimetype = _getBestMatchingMimeType(request, queryType) + mimetype = _getBestMatchingMimeType(request, queryType) - if not mimetype: - return make_response("Mimetype: {} not acceptable".format(mimetype), 406) + if not mimetype: + return make_response("Mimetype: {} not acceptable".format(mimetype), 406) - response = create_result_response(res, mimetype, serializations[mimetype]) - if commitid: - response.headers["X-CurrentCommit"] = commitid - return response + response = create_result_response(res, mimetype, serializations[mimetype]) + if branch_or_ref: + response.headers["X-CurrentBranch"] = branch_or_ref + if commitid: + response.headers["X-CurrentCommit"] = commitid + return response + else: + logger.debug("Unsupported Type: {}".format(queryType)) + return make_response("Unsupported Query Type: {}".format(queryType), 400) @endpoint.route("/provenance", methods=['POST', 'GET']) diff --git a/tests/test_endpoint.py b/tests/test_endpoint.py index bdc2af39..16b19d44 100644 --- a/tests/test_endpoint.py +++ b/tests/test_endpoint.py @@ -2,11 +2,415 @@ import unittest from context import quit +import quit.application as quitApp +from quit.web.app import create_app +from tempfile import TemporaryDirectory +import json class EndpointTests(unittest.TestCase): """Test endpoint features.""" - pass + def setUp(self): + return + + def tearDown(self): + return + + def testInsertDataNoSnapshotIsolation(self): + """Test inserting data without checking the snapshot isolation using the commit id. + """ + # Prepate a git Repository + with TemporaryDirectory() as repo: + + # Start Quit + args = quitApp.parseArgs(['-t', repo]) + objects = quitApp.initialize(args) + config = objects['config'] + app = create_app(config).test_client() + + # execute INSERT DATA query + update = """INSERT DATA { + GRAPH { + a ; + "Take out the organic waste" . + }} + """ + response = app.post('/sparql', data=dict(update=update)) + self.assertEqual(response.status_code, 200) + + # execute SELECT query + select = "SELECT * WHERE {graph {?s a ; ?p ?o .}} ORDER BY ?s ?p ?o" + select_resp = app.post('/sparql', data=dict(query=select), headers=dict(accept="application/sparql-results+json")) + self.assertEqual(select_resp.status_code, 200) + + # execute SELECT query + select = "SELECT * WHERE {graph {?s a ; ?p ?o .}} ORDER BY ?s ?p ?o" + select_resp = app.post('/sparql', data=dict(query=select), headers=dict(accept="application/sparql-results+json")) + self.assertEqual(select_resp.status_code, 200) + + # execute INSERT DATA query + update = """INSERT DATA { + GRAPH { + . + }} + """ + response = app.post('/sparql', data=dict(update=update)) + self.assertEqual(response.status_code, 200) + + # execute INSERT DATA query + update = """DELETE { + GRAPH { + ?todo ?task . + }} + INSERT { + GRAPH { + ?todo "Take out the organic waste and the residual waste" . + }} + WHERE { + BIND ("Take out the organic waste" as ?task) + GRAPH { + ?todo ?task + } + } + """ + response = app.post('/sparql', data=dict(update=update)) + self.assertEqual(response.status_code, 200) + + # execute SELECT query + select = "SELECT * WHERE {graph {?s a ; ?p ?o .}} ORDER BY ?s ?p ?o" + select_resp = app.post('/sparql', data=dict(query=select), headers=dict(accept="application/sparql-results+json")) + self.assertEqual(select_resp.status_code, 200) + + obj = json.loads(select_resp.data.decode("utf-8")) + + self.assertEqual(len(obj["results"]["bindings"]), 3) + + self.assertDictEqual(obj["results"]["bindings"][0], { + "s": {'type': 'uri', 'value': 'http://ex.org/garbage'}, + "p": {'type': 'uri', 'value': 'http://ex.org/status'}, + "o": {'type': 'uri', 'value': 'http://ex.org/completed'}}) + self.assertDictEqual(obj["results"]["bindings"][1], { + "s": {'type': 'uri', 'value': 'http://ex.org/garbage'}, + "p": {'type': 'uri', 'value': 'http://ex.org/task'}, + "o": {'type': 'literal', 'value': 'Take out the organic waste and the residual waste'}}) + self.assertDictEqual(obj["results"]["bindings"][2], { + "s": {'type': 'uri', 'value': 'http://ex.org/garbage'}, + "p": {'type': 'uri', 'value': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'}, + "o": {'type': 'uri', 'value': 'http://ex.org/Todo'}}) + + + def testInsertDataOverlappingWithReject(self): + """Test inserting data from two clients (simulated) with overlapping update requests. + """ + # Prepate a git Repository + with TemporaryDirectory() as repo: + + # Start Quit + args = quitApp.parseArgs(['-t', repo]) + objects = quitApp.initialize(args) + config = objects['config'] + app = create_app(config).test_client() + + # execute INSERT DATA query + update = """INSERT DATA { + GRAPH { + a ; + "Take out the organic waste" . + }} + """ + response = app.post('/sparql', data=dict(update=update)) + self.assertEqual(response.status_code, 200) + + # Client A: execute SELECT query + selectA = "SELECT * WHERE {graph {?s a ; ?p ?o .}} ORDER BY ?s ?p ?o" + selectA_resp = app.post('/sparql', data=dict(query=selectA), headers=dict(accept="application/sparql-results+json")) + self.assertEqual(selectA_resp.status_code, 200) + branchA = selectA_resp.headers['X-CurrentBranch'] + commitA = selectA_resp.headers['X-CurrentCommit'] + + # Client B: execute SELECT query + selectB = "SELECT * WHERE {graph {?s a ; ?p ?o .}} ORDER BY ?s ?p ?o" + selectB_resp = app.post('/sparql', data=dict(query=selectB), headers=dict(accept="application/sparql-results+json")) + self.assertEqual(selectB_resp.status_code, 200) + branchB = selectB_resp.headers['X-CurrentBranch'] + commitB = selectB_resp.headers['X-CurrentCommit'] + self.assertEqual(commitA, commitB) + self.assertEqual(branchA, branchB) + + # Client B: update operation + updateB = """INSERT DATA { + GRAPH { + . + }} + """ + response = app.post('/sparql', data=dict(update=updateB, parent_commit_id=commitB, resolution_method='reject')) + self.assertEqual(response.status_code, 200) + self.assertEqual(branchB, response.headers['X-CurrentBranch']) + self.assertNotEqual(commitB, response.headers['X-CurrentCommit']) + + # Client A: update operation + updateA = """DELETE { + GRAPH { + ?todo ?task . + }} + INSERT { + GRAPH { + ?todo "Take out the organic waste and the residual waste" . + }} + WHERE { + BIND ("Take out the organic waste" as ?task) + GRAPH { + ?todo ?task + } + } + """ + response = app.post('/sparql', data=dict(update=updateA, parent_commit_id=commitA, resolution_method='reject')) + # FAILURE. The second request should be rejected because it asumes a different commit + self.assertEqual(response.status_code, 409) + + # check the result + select = "SELECT * WHERE {graph {?s a ; ?p ?o .}} ORDER BY ?s ?p ?o" + select_resp = app.post('/sparql', data=dict(query=select), headers=dict(accept="application/sparql-results+json")) + self.assertEqual(select_resp.status_code, 200) + self.assertEqual(branchA, select_resp.headers['X-CurrentBranch']) + self.assertNotEqual(commitA, select_resp.headers['X-CurrentCommit']) + + obj = json.loads(select_resp.data.decode("utf-8")) + + self.assertEqual(len(obj["results"]["bindings"]), 3) + + self.assertDictEqual(obj["results"]["bindings"][0], { + "s": {'type': 'uri', 'value': 'http://ex.org/garbage'}, + "p": {'type': 'uri', 'value': 'http://ex.org/status'}, + "o": {'type': 'uri', 'value': 'http://ex.org/completed'}}) + self.assertDictEqual(obj["results"]["bindings"][1], { + "s": {'type': 'uri', 'value': 'http://ex.org/garbage'}, + "p": {'type': 'uri', 'value': 'http://ex.org/task'}, + "o": {'type': 'literal', 'value': 'Take out the organic waste'}}) + self.assertDictEqual(obj["results"]["bindings"][2], { + "s": {'type': 'uri', 'value': 'http://ex.org/garbage'}, + "p": {'type': 'uri', 'value': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'}, + "o": {'type': 'uri', 'value': 'http://ex.org/Todo'}}) + + + def testInsertDataOverlappingWithBranch(self): + """Test inserting data from two clients (simulated) with overlapping update requests and branch resolution + """ + # Prepate a git Repository + with TemporaryDirectory() as repo: + + # Start Quit + args = quitApp.parseArgs(['-t', repo]) + objects = quitApp.initialize(args) + config = objects['config'] + app = create_app(config).test_client() + + # execute INSERT DATA query + update = """INSERT DATA { + GRAPH { + a ; + "Take out the organic waste" . + }} + """ + response = app.post('/sparql', data=dict(update=update)) + self.assertEqual(response.status_code, 200) + + # Client A: execute SELECT query + selectA = "SELECT * WHERE {graph {?s a ; ?p ?o .}} ORDER BY ?s ?p ?o" + selectA_resp = app.post('/sparql', data=dict(query=selectA), headers=dict(accept="application/sparql-results+json")) + self.assertEqual(selectA_resp.status_code, 200) + branchA = selectA_resp.headers['X-CurrentBranch'] + commitA = selectA_resp.headers['X-CurrentCommit'] + + # Client B: execute SELECT query + selectB = "SELECT * WHERE {graph {?s a ; ?p ?o .}} ORDER BY ?s ?p ?o" + selectB_resp = app.post('/sparql', data=dict(query=selectB), headers=dict(accept="application/sparql-results+json")) + self.assertEqual(selectB_resp.status_code, 200) + branchB = selectB_resp.headers['X-CurrentBranch'] + commitB = selectB_resp.headers['X-CurrentCommit'] + self.assertEqual(commitA, commitB) + self.assertEqual(branchA, branchB) + + # Client B: update operation + updateB = """INSERT DATA { + GRAPH { + . + }} + """ + response = app.post('/sparql', data=dict(update=updateB, parent_commit_id=commitB, resolution_method='branch')) + self.assertEqual(response.status_code, 200) + self.assertEqual(branchB, response.headers['X-CurrentBranch']) + newCommitB = response.headers['X-CurrentCommit'] + self.assertNotEqual(commitB, newCommitB) + + # Client A: update operation + updateA = """DELETE { + GRAPH { + ?todo ?task . + }} + INSERT { + GRAPH { + ?todo "Take out the organic waste and the residual waste" . + }} + WHERE { + BIND ("Take out the organic waste" as ?task) + GRAPH { + ?todo ?task + } + } + """ + response = app.post('/sparql', data=dict(update=updateA, parent_commit_id=commitA, resolution_method='branch')) + # FAILURE. The second request should be rejected because it asumes a different commit + self.assertEqual(response.status_code, 200) + newBranchA = response.headers['X-CurrentBranch'] + self.assertNotEqual(branchA, newBranchA) + newCommitA = response.headers['X-CurrentCommit'] + self.assertNotEqual(commitA, newCommitA) + self.assertNotEqual(newCommitB, newCommitA) + + # check the result on the master branch + select = "SELECT * WHERE {graph {?s a ; ?p ?o .}} ORDER BY ?s ?p ?o" + select_resp = app.post('/sparql', data=dict(query=select), headers=dict(accept="application/sparql-results+json")) + self.assertEqual(select_resp.status_code, 200) + self.assertEqual(branchB, select_resp.headers['X-CurrentBranch']) + self.assertEqual(newCommitB, select_resp.headers['X-CurrentCommit']) + + obj = json.loads(select_resp.data.decode("utf-8")) + + self.assertEqual(len(obj["results"]["bindings"]), 3) + + self.assertDictEqual(obj["results"]["bindings"][0], { + "s": {'type': 'uri', 'value': 'http://ex.org/garbage'}, + "p": {'type': 'uri', 'value': 'http://ex.org/status'}, + "o": {'type': 'uri', 'value': 'http://ex.org/completed'}}) + self.assertDictEqual(obj["results"]["bindings"][1], { + "s": {'type': 'uri', 'value': 'http://ex.org/garbage'}, + "p": {'type': 'uri', 'value': 'http://ex.org/task'}, + "o": {'type': 'literal', 'value': 'Take out the organic waste'}}) + self.assertDictEqual(obj["results"]["bindings"][2], { + "s": {'type': 'uri', 'value': 'http://ex.org/garbage'}, + "p": {'type': 'uri', 'value': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'}, + "o": {'type': 'uri', 'value': 'http://ex.org/Todo'}}) + + + # check the result on the newly created branch + select = "SELECT * WHERE {graph {?s a ; ?p ?o .}} ORDER BY ?s ?p ?o" + select_resp = app.post('/sparql/{}'.format(newBranchA), data=dict(query=select), headers=dict(accept="application/sparql-results+json")) + self.assertEqual(select_resp.status_code, 200) + self.assertEqual(newBranchA, select_resp.headers['X-CurrentBranch']) + self.assertEqual(newCommitA, select_resp.headers['X-CurrentCommit']) + + obj = json.loads(select_resp.data.decode("utf-8")) + + self.assertEqual(len(obj["results"]["bindings"]), 2) + + self.assertDictEqual(obj["results"]["bindings"][0], { + "s": {'type': 'uri', 'value': 'http://ex.org/garbage'}, + "p": {'type': 'uri', 'value': 'http://ex.org/task'}, + "o": {'type': 'literal', 'value': 'Take out the organic waste and the residual waste'}}) + self.assertDictEqual(obj["results"]["bindings"][1], { + "s": {'type': 'uri', 'value': 'http://ex.org/garbage'}, + "p": {'type': 'uri', 'value': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'}, + "o": {'type': 'uri', 'value': 'http://ex.org/Todo'}}) + + + def testInsertDataOverlappingWithMerge(self): + """Test inserting data from two clients (simulated) with overlapping update requests and merge resolution + """ + # Prepate a git Repository + with TemporaryDirectory() as repo: + + # Start Quit + args = quitApp.parseArgs(['-t', repo]) + objects = quitApp.initialize(args) + config = objects['config'] + app = create_app(config).test_client() + + # execute INSERT DATA query + update = """INSERT DATA { + GRAPH { + a ; + "Take out the organic waste" . + }} + """ + response = app.post('/sparql', data=dict(update=update)) + self.assertEqual(response.status_code, 200) + + # Client A: execute SELECT query + selectA = "SELECT * WHERE {graph {?s a ; ?p ?o .}} ORDER BY ?s ?p ?o" + selectA_resp = app.post('/sparql', data=dict(query=selectA), headers=dict(accept="application/sparql-results+json")) + self.assertEqual(selectA_resp.status_code, 200) + branchA = selectA_resp.headers['X-CurrentBranch'] + commitA = selectA_resp.headers['X-CurrentCommit'] + + # Client B: execute SELECT query + selectB = "SELECT * WHERE {graph {?s a ; ?p ?o .}} ORDER BY ?s ?p ?o" + selectB_resp = app.post('/sparql', data=dict(query=selectB), headers=dict(accept="application/sparql-results+json")) + self.assertEqual(selectB_resp.status_code, 200) + branchB = selectB_resp.headers['X-CurrentBranch'] + commitB = selectB_resp.headers['X-CurrentCommit'] + self.assertEqual(commitA, commitB) + self.assertEqual(branchA, branchB) + + # Client B: update operation + updateB = """INSERT DATA { + GRAPH { + . + }} + """ + response = app.post('/sparql', data=dict(update=updateB, parent_commit_id=commitB, resolution_method='merge')) + self.assertEqual(response.status_code, 200) + self.assertEqual(branchB, response.headers['X-CurrentBranch']) + newCommitB = response.headers['X-CurrentCommit'] + self.assertNotEqual(commitB, newCommitB) + + # Client A: update operation + updateA = """DELETE { + GRAPH { + ?todo ?task . + }} + INSERT { + GRAPH { + ?todo "Take out the organic waste and the residual waste" . + }} + WHERE { + BIND ("Take out the organic waste" as ?task) + GRAPH { + ?todo ?task + } + } + """ + response = app.post('/sparql', data=dict(update=updateA, parent_commit_id=commitA, resolution_method='merge')) + # FAILURE. The second request should be rejected because it asumes a different commit + self.assertEqual(response.status_code, 200) + self.assertEqual(branchA, response.headers['X-CurrentBranch']) + newCommitA = response.headers['X-CurrentCommit'] + self.assertNotEqual(commitA, newCommitA) + self.assertNotEqual(newCommitB, newCommitA) + + # check the result on the master branch + select = "SELECT * WHERE {graph {?s a ; ?p ?o .}} ORDER BY ?s ?p ?o" + select_resp = app.post('/sparql', data=dict(query=select), headers=dict(accept="application/sparql-results+json")) + self.assertEqual(select_resp.status_code, 200) + self.assertEqual(branchB, select_resp.headers['X-CurrentBranch']) + self.assertEqual(newCommitA, select_resp.headers['X-CurrentCommit']) + + obj = json.loads(select_resp.data.decode("utf-8")) + + self.assertEqual(len(obj["results"]["bindings"]), 3) + + self.assertDictEqual(obj["results"]["bindings"][0], { + "s": {'type': 'uri', 'value': 'http://ex.org/garbage'}, + "p": {'type': 'uri', 'value': 'http://ex.org/status'}, + "o": {'type': 'uri', 'value': 'http://ex.org/completed'}}) + self.assertDictEqual(obj["results"]["bindings"][1], { + "s": {'type': 'uri', 'value': 'http://ex.org/garbage'}, + "p": {'type': 'uri', 'value': 'http://ex.org/task'}, + "o": {'type': 'literal', 'value': 'Take out the organic waste and the residual waste'}}) + self.assertDictEqual(obj["results"]["bindings"][2], { + "s": {'type': 'uri', 'value': 'http://ex.org/garbage'}, + "p": {'type': 'uri', 'value': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type'}, + "o": {'type': 'uri', 'value': 'http://ex.org/Todo'}}) if __name__ == '__main__':