From 9f27199ff59276110a86e8fd6729e8a049379b3b Mon Sep 17 00:00:00 2001 From: Tom Johnson Date: Sun, 3 Jan 2016 13:26:59 -0800 Subject: [PATCH 1/5] Test `Mutable#apply_changeset` Recent changes expand changesets to be applicable to any mutable. Responsibility for applying the changeset atomically lives within this method in mutable, which can be called directly or through `Changeset#apply`. --- lib/rdf/spec/mutable.rb | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/lib/rdf/spec/mutable.rb b/lib/rdf/spec/mutable.rb index a753771..d537feb 100644 --- a/lib/rdf/spec/mutable.rb +++ b/lib/rdf/spec/mutable.rb @@ -200,5 +200,42 @@ end end end + + describe '#apply_changeset' do + let(:changeset) { RDF::Changeset.new } + + it 'is a no-op when changeset is empty' do + expect { subject.apply_changeset(changeset) } + .not_to change { subject.statements } + end + + it 'inserts statements' do + changeset.insert(*non_bnode_statements) + + expect { subject.apply_changeset(changeset) } + .to change { subject.statements } + .to contain_exactly(*non_bnode_statements) + end + + it 'deletes statements' do + subject.insert(*non_bnode_statements) + deletes = non_bnode_statements.take(10) + + changeset.delete(*deletes) + subject.apply_changeset(changeset) + + expect(subject).not_to include(*deletes) + end + + it 'deletes before inserting' do + statement = non_bnode_statements.first + + changeset.insert(statement) + changeset.delete(statement) + subject.apply_changeset(changeset) + + expect(subject).to include(statement) + end + end end end From dee47555b21b9131ffbc856fc7a5684fc93dfe84 Mon Sep 17 00:00:00 2001 From: Tom Johnson Date: Sun, 3 Jan 2016 15:12:27 -0800 Subject: [PATCH 2/5] Rework transaction specs for 2.0 --- lib/rdf/spec/transaction.rb | 175 ++++++++++++++---------------------- 1 file changed, 67 insertions(+), 108 deletions(-) diff --git a/lib/rdf/spec/transaction.rb b/lib/rdf/spec/transaction.rb index 933b31b..43b0852 100644 --- a/lib/rdf/spec/transaction.rb +++ b/lib/rdf/spec/transaction.rb @@ -6,153 +6,112 @@ shared_examples "an RDF::Transaction" do |klass| include RDF::Spec::Matchers - subject { klass.new(graph_name: RDF::URI("name")) } + subject { klass.new(repository, mutable: true) } + let(:repository) { RDF::Repository.new } - describe "#initialize" do - subject {klass} - it "accepts a graph" do - g = double("graph") - this = subject.new(graph: g) - expect(this.graph).to eq g - end + it { is_expected.to be_readable } - it "accepts a graph_name" do - c = double("graph_name") - this = subject.new(graph: c) - expect(this.graph).to eq c - expect(this.graph_name).to eq c + describe "#initialize" do + it 'accepts a repository' do + repo = double('repository') - this = subject.new(graph_name: c) - expect(this.graph).to eq c - expect(this.graph_name).to eq c + expect(klass.new(repo).repository).to eq repo end - it "accepts inserts" do - g = double("inserts") - this = subject.new(insert: g) - expect(this.inserts).to eq g + it 'defaults immutable (read only)' do + expect(klass.new(repository).mutable?).to be false end - it "accepts deletes" do - g = double("deletes") - this = subject.new(delete: g) - expect(this.deletes).to eq g + it 'allows mutability' do + expect(klass.new(repository, mutable: true)).to be_mutable end end - its(:deletes) {is_expected.to be_a(RDF::Enumerable)} - its(:inserts) {is_expected.to be_a(RDF::Enumerable)} - it {is_expected.to be_mutable} - it {is_expected.to_not be_readable} - it "does not respond to #load" do - expect {subject.load("http://example/")}.to raise_error(NoMethodError) + expect { subject.load("http://example/") }.to raise_error(NoMethodError) end it "does not respond to #update" do - expect {subject.update(RDF::Statement.new)}.to raise_error(NoMethodError) + expect { subject.update(RDF::Statement.new) }.to raise_error(NoMethodError) end it "does not respond to #clear" do - expect {subject.clear}.to raise_error(NoMethodError) + expect { subject.clear }.to raise_error(NoMethodError) end - describe "#execute" do - let(:s) {RDF::Statement.new(RDF::URI("s"), RDF::URI("p"), RDF::URI("o"))} - let(:r) {double("repository")} - - it "deletes statements" do - statement = s.dup - statement.graph_name = (subject.graph_name rescue nil) - expect(r).to receive(:delete).with(statement) - expect(r).not_to receive(:insert) - subject.delete(s) - subject.execute(r) + describe '#buffered?' do + it 'is false when changeset is empty' do + expect(subject).not_to be_buffered end - it "inserts statements" do - statement = s.dup - statement.graph_name = (subject.graph_name rescue nil) - expect(r).not_to receive(:delete) - expect(r).to receive(:insert).with(statement) - subject.insert(s) - subject.execute(r) + it 'is true if changeset has changes' do + subject.insert([:s, :p, :o]) + expect(subject).to be_buffered end + end - context 'with graph names' do - let(:s) {RDF::Statement.new(RDF::URI("s"), RDF::URI("p"), RDF::URI("o"))} - let(:s_with_c) {RDF::Statement.new(RDF::URI("s"), RDF::URI("p"), RDF::URI("o"), graph_name: RDF::URI('c_st'))} - - it "deletes statements" do - statement = s.dup - statement.graph_name = subject.graph_name - expect(r).to receive(:delete).with(statement, s_with_c) - expect(r).not_to receive(:insert) - subject.delete(s) - subject.delete(s_with_c) - subject.execute(r) - end - - it "inserts statements" do - statement = s.dup - statement.graph_name = subject.graph_name - expect(r).not_to receive(:delete) - expect(r).to receive(:insert).with(statement, s_with_c) - subject.insert(s) - subject.insert(s_with_c) - subject.execute(r) - end + describe '#changes' do + it 'is a changeset' do + expect(subject.changes).to be_a RDF::Changeset end - it "calls before_execute" do - is_expected.to receive(:before_execute).with(r, {}) - subject.execute(r) + it 'is initially empty' do + expect(subject.changes).to be_empty end + end - it "calls after_execute" do - is_expected.to receive(:after_execute).with(r, {}) - subject.execute(r) + describe "#delete" do + let(:st) { RDF::Statement(:s, RDF::URI('p'), 'o') } + + it 'adds to deletes' do + expect { subject.delete(st) } + .to change { subject.changes.deletes }.to contain_exactly(st) end - it "returns self" do - expect(subject.execute(r)).to eq subject + it 'adds multiple to deletes' do + sts = [st] << RDF::Statement(:x, RDF::URI('y'), 'z') + + expect { subject.delete(*sts) } + .to change { subject.changes.deletes }.to contain_exactly(*sts) end - end - describe "#delete_statement" do - let(:s) {RDF::Statement.new(RDF::URI("s"), RDF::URI("p"), RDF::URI("o"))} - it "adds statement to #deletes" do - subject.delete(s) - expect(subject.deletes.to_a).to eq [s] + it 'adds enumerable to deletes' do + sts = [st] << RDF::Statement(:x, RDF::URI('y'), 'z') + sts.extend(RDF::Enumerable) + + expect { subject.delete(sts) } + .to change { subject.changes.deletes }.to contain_exactly(*sts) end end - describe "#insert_statement" do - let(:s) {RDF::Statement.new(RDF::URI("s"), RDF::URI("p"), RDF::URI("o"))} - it "adds statement to #inserts" do - subject.insert(s) - expect(subject.inserts.to_a).to eq [s] + describe "#insert" do + let(:st) { RDF::Statement(:s, RDF::URI('p'), 'o') } + + it 'adds to inserts' do + expect { subject.insert(st) } + .to change { subject.changes.inserts }.to contain_exactly(st) end - end - context 'with graph names' do - let(:s) {RDF::Statement.new(RDF::URI("s"), RDF::URI("p"), RDF::URI("o"))} - let(:s_with_c) {RDF::Statement.new(RDF::URI("s"), RDF::URI("p"), RDF::URI("o"), graph_name: RDF::URI('c_st'))} + it 'adds multiple to inserts' do + sts = [st] << RDF::Statement(:x, RDF::URI('y'), 'z') + + expect { subject.insert(*sts) } + .to change { subject.changes.inserts }.to contain_exactly(*sts) + end + + it 'adds enumerable to inserts' do + sts = [st] << RDF::Statement(:x, RDF::URI('y'), 'z') + sts.extend(RDF::Enumerable) - describe "#delete_statement" do - it "adds statement to #deletes" do - subject.delete(s) - subject.delete(s_with_c) - expect(subject.deletes.to_a).to contain_exactly(s, s_with_c) - end + expect { subject.insert(sts) } + .to change { subject.changes.inserts }.to contain_exactly(*sts) end + end - describe "#insert_statement" do - it "adds statement to #inserts" do - subject.insert(s) - subject.insert(s_with_c) - expect(subject.inserts.to_a).to contain_exactly(s, s_with_c) - end + describe '#execute' do + it 'calls changes#apply with repository' do + expect(subject.changes).to receive(:apply).with(subject.repository) + subject.execute end end end From 986d8a3f0630c8292fea6b0d9687118d416c73a8 Mon Sep 17 00:00:00 2001 From: Tom Johnson Date: Tue, 12 Jan 2016 16:42:38 -0800 Subject: [PATCH 3/5] Soften RDF::Transaction buffering requirements Not every `Transaction` needs to keep an up-to-date `Changeset` in buffer. This change loosens the requirements. some of the tests are removed to the `rdf` test suite in `spec/transaction_spec.rb`. --- lib/rdf/spec/transaction.rb | 48 ++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/lib/rdf/spec/transaction.rb b/lib/rdf/spec/transaction.rb index 43b0852..ddf5259 100644 --- a/lib/rdf/spec/transaction.rb +++ b/lib/rdf/spec/transaction.rb @@ -43,11 +43,6 @@ it 'is false when changeset is empty' do expect(subject).not_to be_buffered end - - it 'is true if changeset has changes' do - subject.insert([:s, :p, :o]) - expect(subject).to be_buffered - end end describe '#changes' do @@ -64,23 +59,33 @@ let(:st) { RDF::Statement(:s, RDF::URI('p'), 'o') } it 'adds to deletes' do - expect { subject.delete(st) } - .to change { subject.changes.deletes }.to contain_exactly(st) + subject.repository.insert(st) + + expect do + subject.delete(st) + subject.execute + end.to change { subject.repository.empty? }.from(false).to(true) end it 'adds multiple to deletes' do sts = [st] << RDF::Statement(:x, RDF::URI('y'), 'z') + subject.repository.insert(*sts) - expect { subject.delete(*sts) } - .to change { subject.changes.deletes }.to contain_exactly(*sts) + expect do + subject.delete(*sts) + subject.execute + end.to change { subject.repository.empty? }.from(false).to(true) end it 'adds enumerable to deletes' do sts = [st] << RDF::Statement(:x, RDF::URI('y'), 'z') sts.extend(RDF::Enumerable) + subject.repository.insert(sts) - expect { subject.delete(sts) } - .to change { subject.changes.deletes }.to contain_exactly(*sts) + expect do + subject.delete(sts) + subject.execute + end.to change { subject.repository.empty? }.from(false).to(true) end end @@ -88,23 +93,32 @@ let(:st) { RDF::Statement(:s, RDF::URI('p'), 'o') } it 'adds to inserts' do - expect { subject.insert(st) } - .to change { subject.changes.inserts }.to contain_exactly(st) + expect do + subject.insert(st) + subject.execute + end.to change { subject.repository.statements } + .to contain_exactly(st) end it 'adds multiple to inserts' do sts = [st] << RDF::Statement(:x, RDF::URI('y'), 'z') - expect { subject.insert(*sts) } - .to change { subject.changes.inserts }.to contain_exactly(*sts) + expect do + subject.insert(*sts) + subject.execute + end.to change { subject.repository.statements } + .to contain_exactly(*sts) end it 'adds enumerable to inserts' do sts = [st] << RDF::Statement(:x, RDF::URI('y'), 'z') sts.extend(RDF::Enumerable) - expect { subject.insert(sts) } - .to change { subject.changes.inserts }.to contain_exactly(*sts) + expect do + subject.insert(sts) + subject.execute + end.to change { subject.repository.statements } + .to contain_exactly(*sts) end end From ac0515df813fac5f48ac41980a594f41a28e9de7 Mon Sep 17 00:00:00 2001 From: Tom Johnson Date: Wed, 13 Jan 2016 11:27:18 -0800 Subject: [PATCH 4/5] Expect Transaction spec users to give a Repository `RDF::Transaction` subclasses will generally depend upon a specific `RDF::Repository`. It's best to expect the user of the shared examples to provide the repository class. This follows the pattern seen in the `RDF::Enumerable` examples and others. --- lib/rdf/spec/transaction.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/rdf/spec/transaction.rb b/lib/rdf/spec/transaction.rb index ddf5259..c8cad87 100644 --- a/lib/rdf/spec/transaction.rb +++ b/lib/rdf/spec/transaction.rb @@ -6,8 +6,12 @@ shared_examples "an RDF::Transaction" do |klass| include RDF::Spec::Matchers + before do + raise 'repository must be set with `let(:repository)' unless + defined? repository + end + subject { klass.new(repository, mutable: true) } - let(:repository) { RDF::Repository.new } it { is_expected.to be_readable } From 9644ab9e41f2ca2535188f2176aa9a51b78c4220 Mon Sep 17 00:00:00 2001 From: Tom Johnson Date: Sun, 24 Jan 2016 15:43:49 -0800 Subject: [PATCH 5/5] Add snapshot tests --- lib/rdf/spec/repository.rb | 26 +++++++++++++++++++++++++- lib/rdf/spec/transaction.rb | 32 ++++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/lib/rdf/spec/repository.rb b/lib/rdf/spec/repository.rb index 8c80013..c4d6844 100644 --- a/lib/rdf/spec/repository.rb +++ b/lib/rdf/spec/repository.rb @@ -43,7 +43,7 @@ it_behaves_like 'an RDF::Mutable' end - + # FIXME: This should be condition on the repository being mutable context "as a durable repository" do require 'rdf/spec/durable' @@ -55,4 +55,28 @@ it_behaves_like 'an RDF::Durable' end + + context "with snapshot support" do + it 'returns a queryable #snapshot' do + if repository.supports? :snapshots + expect(repository.snapshot).to be_a RDF::Queryable + end + end + + it 'gives an accurate snapshot' do + if repository.supports? :snapshots + snap = repository.snapshot + expect(snap.query([:s, :p, :o])) + .to contain_exactly(*repository.query([:s, :p, :o])) + end + end + + it 'gives static snapshot' do + if repository.supports? :snapshots + snap = repository.snapshot + expect { repository.clear } + .not_to change { snap.query([:s, :p, :o]).to_a } + end + end + end end diff --git a/lib/rdf/spec/transaction.rb b/lib/rdf/spec/transaction.rb index c8cad87..5fd4bac 100644 --- a/lib/rdf/spec/transaction.rb +++ b/lib/rdf/spec/transaction.rb @@ -11,13 +11,24 @@ defined? repository end - subject { klass.new(repository, mutable: true) } + subject { klass.new(repository, mutable: true) } it { is_expected.to be_readable } + it { is_expected.to be_queryable } + + context "when querying statements" do + require 'rdf/spec/queryable' + let(:queryable) do + repository.insert(*RDF::Spec.quads) + q = klass.new(repository, mutable: true) + end + it_behaves_like 'an RDF::Queryable' + end describe "#initialize" do it 'accepts a repository' do repo = double('repository') + allow(repo).to receive_messages(:supports? => false) expect(klass.new(repo).repository).to eq repo end @@ -127,9 +138,22 @@ end describe '#execute' do - it 'calls changes#apply with repository' do - expect(subject.changes).to receive(:apply).with(subject.repository) - subject.execute + context 'after rollback' do + before { subject.rollback } + + it 'does not execute' do + expect { subject.execute } + .to raise_error RDF::Transaction::TransactionError + end + end + end + + describe '#rollback' do + before { subject.insert(st); subject.delete(st) } + let(:st) { RDF::Statement(:s, RDF::URI('p'), 'o') } + + it 'empties changes when available' do + expect { subject.rollback }.to change { subject.changes }.to be_empty end end end