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__':