blob: 7574772abf88c63ac140275f51d3341a80c637ff [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package accord.verify;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class StrictSerializabilityVerifierTest
{
private static final int[][] SEQUENCES = new int[][] { null, new int[]{ }, new int[] { 1 }, new int[] { 1, 2 }, new int[] { 1, 2, 3 } };
static class History
{
final int keys;
int overlap, nextAt;
List<Observation> observations = new ArrayList<>();
History(int keys)
{
this.keys = keys;
}
History w(int writeKey, int writeSeq)
{
return rw(writeKey, writeSeq, (int[])null);
}
History ws(int ... writes)
{
return rw(writes, (int[])null);
}
History rw(int writeKey, int writeSeq, int ... reads)
{
int[] writes;
if (writeKey < 0)
{
writes = new int[0];
}
else
{
writes = new int[writeKey + 1];
Arrays.fill(writes, -1);
writes[writeKey] = writeSeq;
}
return rw(writes, reads);
}
History rw(int[] writes, int ... reads)
{
int start = overlap > 0 ? this.overlap : ++nextAt;
int end = ++nextAt;
observations.add(new Observation(writes, reads, start, end));
return this;
}
History overlap()
{
overlap = ++nextAt;
return this;
}
History deoverlap()
{
overlap = 0;
return this;
}
History r(int ... reads)
{
return rw(-1, -1, reads);
}
void assertNoViolation()
{
run(Runnable::run);
}
void assertViolation()
{
run(runnable -> assertThrows(HistoryViolation.class, runnable::run));
}
private void run(Consumer<Runnable> onLast)
{
StrictSerializabilityVerifier verifier = new StrictSerializabilityVerifier("", keys);
for (int i = 0 ; i < observations.size() ; ++i)
{
Observation observation = observations.get(i);
verifier.begin();
if (observation.reads != null)
{
for (int key = 0 ; key < observation.reads.length ; ++key)
{
if (observation.reads[key] >= 0)
verifier.witnessRead(key, SEQUENCES[observation.reads[key] + 1]);
}
}
for (int j = 0 ; j < observation.writes.length ; ++j)
{
if (observation.writes[j] >= 0)
verifier.witnessWrite(j, observation.writes[j]);
}
if (i == observations.size() - 1) onLast.accept(() -> verifier.apply(observation.start, observation.end));
else verifier.apply(observation.start, observation.end);
}
}
}
static class Observation
{
final int[] writes;
final int[] reads;
final int start, end;
Observation(int[] writes, int[] reads, int start, int end)
{
this.writes = writes;
this.reads = reads;
this.start = start;
this.end = end;
}
}
@Test
public void lostRead()
{
new History(1).r(1).r(0).assertViolation();
new History(1).r(2).r(1).assertViolation();
new History(2).r(1, 2).r(2, 1).assertViolation();
}
@Test
public void lostWrite()
{
new History(1).w(0, 1).r(0).assertViolation();
new History(1).rw(0, 1, 0).r(0).assertViolation();
}
@Test
public void readBeforeWrite()
{
new History(1).r(1).rw(0, 1, 0).assertViolation();
new History(1).r(1).w(0, 1).assertViolation();
}
@Test
public void readCycle()
{
new History(2).overlap().r(0, 1).r(1, 0).assertViolation();
new History(3).overlap().r(1, 0, -1).r(-1, 1, 0).r(0, -1, 1).assertViolation();
new History(4).overlap().r(1, 0, -1, -1).r(-1, 1, 0, -1).r(-1, -1, 1, 0).r(0, -1, -1, 1).assertViolation();
}
@Test
public void readWriteCycle()
{
new History(2).overlap().rw(0, 1, -1, 0).rw(1, 1, 1, -1).assertNoViolation();
new History(2).overlap().rw(0, 1, -1, 1).rw(1, 1, 0, -1).assertNoViolation();
new History(2).overlap().rw(0, 1, -1, 0).rw(1, 1, 0, -1).assertViolation();
new History(2).overlap().rw(0, 1, -1, 0).w(1, 1).r(0, 1).assertViolation();
}
@Test
public void simpleWrite()
{
new History(1).r(0).w(0, 1).r(1).assertNoViolation();
new History(1).rw(0, 1, 0).r(1).assertNoViolation();
}
@Test
public void blindWrites()
{
new History(2).ws(1, 1).r(1, -1).ws(-1, 2).assertNoViolation();
new History(2).ws(1, 1).ws(-1, 2).assertNoViolation();
new History(2).ws(1, 1).ws(2, 2).r(2).assertNoViolation();
new History(2).ws(1, 1).ws(2, 2).r(2).r(1).assertViolation();
new History(2).ws(1, 1).overlap().ws(2, 2).r(2).r(1).deoverlap().r(2).assertNoViolation();
new History(2).ws(1, 1).ws(2, 2).r(1).assertViolation();
}
private void fromLog(String log)
{
class Read
{
final int pk, id, count;
final int[] seq;
Read(int pk, int id, int count, int[] seq)
{
this.pk = pk;
this.id = id;
this.count = count;
this.seq = seq;
}
}
class Write
{
final int pk, id;
final boolean success;
Write(int pk, int id, boolean success)
{
this.pk = pk;
this.id = id;
this.success = success;
}
}
class Witness
{
final int start, end;
final List<Object> actions = new ArrayList<>();
Witness(int start, int end)
{
this.start = start;
this.end = end;
}
public void read(int pk, int id, int count, int[] seq)
{
actions.add(new Read(pk, id, count, seq));
}
public void write(int pk, int id, boolean success)
{
actions.add(new Write(pk, id, success));
}
public void process(StrictSerializabilityVerifier verifier)
{
verifier.begin();
for (Object a : actions)
{
if (a instanceof Read)
{
Read read = (Read) a;
verifier.witnessRead(read.pk, read.seq);
}
else
{
Write write = (Write) a;
if (write.success)
verifier.witnessWrite(write.pk, write.id);
}
}
verifier.apply(start, end);
}
}
List<Witness> witnesses = new ArrayList<>();
Witness current = null;
for (String line : log.split("\n"))
{
if (line.startsWith("Witness"))
{
if (current != null)
witnesses.add(current);
Matcher matcher = Pattern.compile("Witness\\(start=(.+), end=(.+)\\)").matcher(line);
if (!matcher.find()) throw new AssertionError("Unable to match start/end of " + line);
current = new Witness(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)));
}
else if (line.startsWith("\tread"))
{
Matcher matcher = Pattern.compile("\tread\\(pk=(.+), id=(.+), count=(.+), seq=\\[(.*)\\]\\)").matcher(line);
if (!matcher.find()) throw new AssertionError("Unable to match read of " + line);
int pk = Integer.parseInt(matcher.group(1));
int id = Integer.parseInt(matcher.group(2));
int count = Integer.parseInt(matcher.group(3));
String seqStr = matcher.group(4);
int[] seq = seqStr.isEmpty() ? new int[0] : Stream.of(seqStr.split(",")).map(String::trim).mapToInt(Integer::parseInt).toArray();
current.read(pk, id, count, seq);
}
else if (line.startsWith("\twrite"))
{
Matcher matcher = Pattern.compile("\twrite\\(pk=(.+), id=(.+), success=(.+)\\)").matcher(line);
if (!matcher.find()) throw new AssertionError("Unable to match write of " + line);
int pk = Integer.parseInt(matcher.group(1));
int id = Integer.parseInt(matcher.group(2));
boolean success = Boolean.parseBoolean(matcher.group(3));
current.write(pk, id, success);
}
else
{
throw new IllegalArgumentException("Unknown line: " + line);
}
}
if (current != null)
witnesses.add(current);
StrictSerializabilityVerifier validator = new StrictSerializabilityVerifier("", 3);
for (Witness w : witnesses)
w.process(validator);
}
@Test
public void seenBehavior()
{
fromLog("Witness(start=4, end=7)\n" +
"\tread(pk=0, id=2, count=0, seq=[])\n" +
"\twrite(pk=0, id=2, success=true)\n" +
"Witness(start=3, end=8)\n" +
"\tread(pk=2, id=0, count=0, seq=[])\n" +
"\twrite(pk=2, id=0, success=true)\n" +
"\twrite(pk=1, id=0, success=true)\n" +
"Witness(start=5, end=9)\n" +
"\tread(pk=0, id=3, count=1, seq=[2])\n" +
"\twrite(pk=0, id=3, success=true)\n" +
"Witness(start=2, end=10)\n" +
"\twrite(pk=2, id=1, success=true)\n" +
"\twrite(pk=1, id=1, success=true)\n" +
"Witness(start=6, end=11)\n" +
"\tread(pk=0, id=4, count=2, seq=[2, 3])\n" +
"\twrite(pk=0, id=4, success=true)\n" +
"Witness(start=12, end=14)\n" +
"\twrite(pk=0, id=5, success=true)\n" +
"Witness(start=13, end=16)\n" +
"\tread(pk=1, id=6, count=2, seq=[0, 1])\n" +
"\twrite(pk=1, id=6, success=true)\n" +
"\twrite(pk=2, id=6, success=true)\n" +
"Witness(start=15, end=18)\n" +
"\tread(pk=0, id=7, count=4, seq=[2, 3, 4, 5])\n" +
"\twrite(pk=0, id=7, success=true)\n" +
"Witness(start=17, end=20)\n" +
"\tread(pk=1, id=8, count=3, seq=[0, 1, 6])\n" +
"\twrite(pk=1, id=8, success=true)\n" +
"\twrite(pk=2, id=8, success=true)\n");
}
}