random tests succeed on Map :)
This commit is contained in:
parent
9b6183ea70
commit
e47dee53a3
@ -25,6 +25,7 @@
|
|||||||
"Y": true,
|
"Y": true,
|
||||||
"setTimeout": true,
|
"setTimeout": true,
|
||||||
"setInterval": true,
|
"setInterval": true,
|
||||||
"Operation": true
|
"Operation": true,
|
||||||
|
"getRandom": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,3 @@
|
|||||||
// returns a random element of o
|
|
||||||
// works on Object, and Array
|
|
||||||
function getRandom (o) {
|
|
||||||
if (o instanceof Array) {
|
|
||||||
return o[Math.floor(Math.random() * o.length)];
|
|
||||||
} else if (o.constructor === Object) {
|
|
||||||
var keys = [];
|
|
||||||
for (var key in o) {
|
|
||||||
keys.push(key);
|
|
||||||
}
|
|
||||||
return o[getRandom(keys)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var globalRoom = {
|
var globalRoom = {
|
||||||
users: {},
|
users: {},
|
||||||
buffers: {},
|
buffers: {},
|
||||||
@ -52,7 +38,7 @@ function flushOne(){
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setInterval(flushOne, 10);
|
// setInterval(flushOne, 10);
|
||||||
|
|
||||||
var userIdCounter = 0;
|
var userIdCounter = 0;
|
||||||
|
|
||||||
@ -85,6 +71,9 @@ class Test extends AbstractConnector {
|
|||||||
c = flushOne();
|
c = flushOne();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
flushOne() {
|
||||||
|
flushOne();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Y.Test = Test;
|
Y.Test = Test;
|
||||||
|
@ -29,11 +29,19 @@ Y.Memory = (function(){ //eslint-disable-line no-unused-vars
|
|||||||
this.os = store.os;
|
this.os = store.os;
|
||||||
}
|
}
|
||||||
*setOperation (op) {
|
*setOperation (op) {
|
||||||
|
if (op.struct === "Insert" && op.right === undefined) {
|
||||||
|
throw new Error("here!");
|
||||||
|
}
|
||||||
this.os[JSON.stringify(op.id)] = op;
|
this.os[JSON.stringify(op.id)] = op;
|
||||||
return op;
|
return op;
|
||||||
}
|
}
|
||||||
*getOperation (id) {
|
*getOperation (id) {
|
||||||
return this.os[JSON.stringify(id)];
|
var op = this.os[JSON.stringify(id)];
|
||||||
|
if (op == null) {
|
||||||
|
throw new Error("Op does not exist..");
|
||||||
|
} else {
|
||||||
|
return op;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
*removeOperation (id) {
|
*removeOperation (id) {
|
||||||
delete this.os[JSON.stringify(id)];
|
delete this.os[JSON.stringify(id)];
|
||||||
@ -82,7 +90,7 @@ Y.Memory = (function(){ //eslint-disable-line no-unused-vars
|
|||||||
var endPos = endState.clock;
|
var endPos = endState.clock;
|
||||||
|
|
||||||
for (var clock = startPos; clock <= endPos; clock++) {
|
for (var clock = startPos; clock <= endPos; clock++) {
|
||||||
var op = yield* this.getOperation([user, clock]);
|
var op = this.os[JSON.stringify([user, clock])];
|
||||||
if (op != null) {
|
if (op != null) {
|
||||||
op = Struct[op.struct].encode(op);
|
op = Struct[op.struct].encode(op);
|
||||||
ops.push(yield* this.makeOperationReady.call(this, startSS, op));
|
ops.push(yield* this.makeOperationReady.call(this, startSS, op));
|
||||||
@ -95,14 +103,11 @@ Y.Memory = (function(){ //eslint-disable-line no-unused-vars
|
|||||||
// instead of ss, you could use currSS (a ss that increments when you add an operation)
|
// instead of ss, you could use currSS (a ss that increments when you add an operation)
|
||||||
var clock;
|
var clock;
|
||||||
var o = op;
|
var o = op;
|
||||||
while (true){
|
while (o.right != null){
|
||||||
// while unknown, go to the right
|
// while unknown, go to the right
|
||||||
o = yield* this.getOperation(o.right);
|
o = yield* this.getOperation(o.right);
|
||||||
if (o == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
clock = ss[o.id[0]];
|
clock = ss[o.id[0]];
|
||||||
if (clock != null && o.id[1] < clock ) {
|
if (clock != null && o.id[1] < clock) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,10 +20,11 @@ type Insert = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function compareIds(id1, id2) {
|
function compareIds(id1, id2) {
|
||||||
if (id1 == null && id2 == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (id1 == null || id2 == null) {
|
if (id1 == null || id2 == null) {
|
||||||
|
if (id1 == null && id2 == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (id1[0] === id2[0] && id1[1] === id2[1]) {
|
if (id1[0] === id2[0] && id1[1] === id2[1]) {
|
||||||
@ -132,13 +133,21 @@ var Struct = {
|
|||||||
return ids;
|
return ids;
|
||||||
},
|
},
|
||||||
getDistanceToOrigin: function *(op){
|
getDistanceToOrigin: function *(op){
|
||||||
var d = 0;
|
if (op.left == null) {
|
||||||
var o = yield* this.getOperation(op.left);
|
return 0;
|
||||||
while (!compareIds(op.origin, (o ? o.id : null))) {
|
} else {
|
||||||
d++;
|
var d = 0;
|
||||||
o = yield* this.getOperation(o.left);
|
var o = yield* this.getOperation(op.left);
|
||||||
|
while (!compareIds(op.origin, (o ? o.id : null))) {
|
||||||
|
d++;
|
||||||
|
if (o.left == null) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
o = yield* this.getOperation(o.left);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d;
|
||||||
}
|
}
|
||||||
return d;
|
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
# $this has to find a unique position between origin and the next known character
|
# $this has to find a unique position between origin and the next known character
|
||||||
@ -159,28 +168,23 @@ var Struct = {
|
|||||||
var i; // loop counter
|
var i; // loop counter
|
||||||
var distanceToOrigin = i = yield* Struct.Insert.getDistanceToOrigin.call(this, op); // most cases: 0 (starts from 0)
|
var distanceToOrigin = i = yield* Struct.Insert.getDistanceToOrigin.call(this, op); // most cases: 0 (starts from 0)
|
||||||
var o;
|
var o;
|
||||||
|
var parent;
|
||||||
|
var start;
|
||||||
|
|
||||||
// find o. o is the first conflicting operation
|
// find o. o is the first conflicting operation
|
||||||
if (op.left != null) {
|
if (op.left != null) {
|
||||||
o = yield* this.getOperation(op.left);
|
o = yield* this.getOperation(op.left);
|
||||||
o = yield* this.getOperation(o.right);
|
o = (o.right == null) ? null : yield* this.getOperation(o.right);
|
||||||
} else if (op.right != null) {
|
} else { // left == null
|
||||||
o = yield* this.getOperation(op.right);
|
parent = yield* this.getOperation(op.parent);
|
||||||
while (o.left != null){
|
let startId = op.parentSub ? parent.map[op.parentSub] : parent.start;
|
||||||
o = yield* this.getOperation(o.left);
|
start = startId == null ? null : yield* this.getOperation(startId);
|
||||||
}
|
o = start;
|
||||||
} else { // left & right are null
|
|
||||||
var p = yield* this.getOperation(op.parent);
|
|
||||||
if (op.parentSub != null) {
|
|
||||||
o = yield* this.getOperation(p.map[op.parentSub]);
|
|
||||||
} else {
|
|
||||||
o = yield* this.getOperation(p.start);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle conflicts
|
// handle conflicts
|
||||||
while (true) {
|
while (true) {
|
||||||
if (o != null && o.id !== op.right){
|
if (o != null && !compareIds(o.id, op.right)){
|
||||||
var oOriginDistance = yield* Struct.Insert.getDistanceToOrigin.call(this, o);
|
var oOriginDistance = yield* Struct.Insert.getDistanceToOrigin.call(this, o);
|
||||||
if (oOriginDistance === i) {
|
if (oOriginDistance === i) {
|
||||||
// case 1
|
// case 1
|
||||||
@ -198,7 +202,7 @@ var Struct = {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
o = yield* this.getOperation(o.next_cl);
|
o = o.right ? yield* this.getOperation(o.right) : null;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -207,7 +211,7 @@ var Struct = {
|
|||||||
// reconnect..
|
// reconnect..
|
||||||
var left = null;
|
var left = null;
|
||||||
var right = null;
|
var right = null;
|
||||||
var parent = yield* this.getOperation(op.parent);
|
parent = parent || (yield* this.getOperation(op.parent));
|
||||||
|
|
||||||
// NOTE: You you have to call addOperation before you set any other operation!
|
// NOTE: You you have to call addOperation before you set any other operation!
|
||||||
|
|
||||||
@ -215,22 +219,16 @@ var Struct = {
|
|||||||
if (op.left != null) {
|
if (op.left != null) {
|
||||||
left = yield* this.getOperation(op.left);
|
left = yield* this.getOperation(op.left);
|
||||||
op.right = left.right;
|
op.right = left.right;
|
||||||
left.right = op.id;
|
|
||||||
if ((yield* this.addOperation(op)) === false) { // add here
|
if ((yield* this.addOperation(op)) === false) { // add here
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
left.right = op.id;
|
||||||
yield* this.setOperation(left);
|
yield* this.setOperation(left);
|
||||||
} else {
|
} else {
|
||||||
|
op.right = op.parentSub ? (parent.map[op.parentSub] || null) : parent.start;
|
||||||
if ((yield* this.addOperation(op)) === false) { // or here
|
if ((yield* this.addOperation(op)) === false) { // or here
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// only set right, if possible
|
|
||||||
if (op.parentSub != null) {
|
|
||||||
var sub = parent[op.parentSub];
|
|
||||||
op.right = sub != null ? sub : null;
|
|
||||||
} else {
|
|
||||||
op.right = parent.start;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// reconnect right
|
// reconnect right
|
||||||
if (op.right != null) {
|
if (op.right != null) {
|
||||||
@ -286,6 +284,8 @@ var Struct = {
|
|||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
execute: function* (op) {
|
execute: function* (op) {
|
||||||
|
op.start = null;
|
||||||
|
op.end = null;
|
||||||
if ((yield* this.addOperation(op)) === false) {
|
if ((yield* this.addOperation(op)) === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
251
src/y.spec.js
251
src/y.spec.js
@ -1,8 +1,53 @@
|
|||||||
/* @flow */
|
/* @flow */
|
||||||
/*eslint-env browser,jasmine */
|
/*eslint-env browser,jasmine */
|
||||||
|
|
||||||
describe("Yjs (basic)", function(){
|
// returns a random element of o
|
||||||
|
// works on Object, and Array
|
||||||
|
function getRandom (o) {
|
||||||
|
if (o instanceof Array) {
|
||||||
|
return o[Math.floor(Math.random() * o.length)];
|
||||||
|
} else if (o.constructor === Object) {
|
||||||
|
var ks = [];
|
||||||
|
for (var key in o) {
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
return o[getRandom(ks)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getRandomNumber(n) {
|
||||||
|
if (n == null) {
|
||||||
|
n = 9999;
|
||||||
|
}
|
||||||
|
return Math.floor(Math.random() * n);
|
||||||
|
}
|
||||||
|
var keys = ["a", "b", "c", "d", "e", "f", 1, 2, 3, 4, 5, 6];
|
||||||
|
|
||||||
|
function compareAllUsers(users){
|
||||||
|
var s1, s2;
|
||||||
|
function* t1(){
|
||||||
|
s1 = yield* this.getStateSet();
|
||||||
|
}
|
||||||
|
function* t2(){
|
||||||
|
s2 = yield* this.getStateSet();
|
||||||
|
}
|
||||||
|
users[0].connector.flushAll();
|
||||||
|
for (var uid = 0; uid + 1 < users.length; uid++) {
|
||||||
|
var u1 = users[uid];
|
||||||
|
var u2 = users[uid + 1];
|
||||||
|
u1.transact(t1);
|
||||||
|
u2.transact(t2);
|
||||||
|
expect(s1).toEqual(s2);
|
||||||
|
var db1 = u1.db.os;
|
||||||
|
var db2 = u2.db.os;
|
||||||
|
for (var key in db1) {
|
||||||
|
expect(db1[key]).toEqual(db2[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Yjs", function(){
|
||||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 500;
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 500;
|
||||||
|
var numberOfTests = 400;
|
||||||
beforeEach(function(){
|
beforeEach(function(){
|
||||||
this.users = [];
|
this.users = [];
|
||||||
for (var i = 0; i < 5; i++) {
|
for (var i = 0; i < 5; i++) {
|
||||||
@ -12,7 +57,7 @@ describe("Yjs (basic)", function(){
|
|||||||
},
|
},
|
||||||
connector: {
|
connector: {
|
||||||
name: "Test",
|
name: "Test",
|
||||||
debug: true
|
debug: false
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -23,91 +68,94 @@ describe("Yjs (basic)", function(){
|
|||||||
}
|
}
|
||||||
this.users = [];
|
this.users = [];
|
||||||
});
|
});
|
||||||
it("There is an initial Map type", function(){
|
|
||||||
var y = this.users[0];
|
|
||||||
y.transact(function*(root){
|
|
||||||
expect(root).not.toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it("Basic get&set of Map property (converge via sync)", function(){
|
|
||||||
var y = this.users[0];
|
|
||||||
y.transact(function*(root){
|
|
||||||
yield* root.val("stuff", "stuffy");
|
|
||||||
expect(yield* root.val("stuff")).toEqual("stuffy");
|
|
||||||
});
|
|
||||||
|
|
||||||
y.connector.flushAll();
|
describe("Basic tests", function(){
|
||||||
|
it("There is an initial Map type", function(){
|
||||||
|
var y = this.users[0];
|
||||||
|
y.transact(function*(root){
|
||||||
|
expect(root).not.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("Basic get&set of Map property (converge via sync)", function(){
|
||||||
|
var y = this.users[0];
|
||||||
|
y.transact(function*(root){
|
||||||
|
yield* root.val("stuff", "stuffy");
|
||||||
|
expect(yield* root.val("stuff")).toEqual("stuffy");
|
||||||
|
});
|
||||||
|
|
||||||
var transaction = function*(root){
|
y.connector.flushAll();
|
||||||
expect(yield* root.val("stuff")).toEqual("stuffy");
|
|
||||||
};
|
|
||||||
for (var key in this.users) {
|
|
||||||
var u = this.users[key];
|
|
||||||
u.transact(transaction);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
it("Basic get&set of Map property (converge via update)", function(){
|
|
||||||
var y = this.users[0];
|
|
||||||
y.connector.flushAll();
|
|
||||||
y.transact(function*(root){
|
|
||||||
yield* root.val("stuff", "stuffy");
|
|
||||||
expect(yield* root.val("stuff")).toEqual("stuffy");
|
|
||||||
});
|
|
||||||
|
|
||||||
var transaction = function*(root){
|
var transaction = function*(root){
|
||||||
expect(yield* root.val("stuff")).toEqual("stuffy");
|
expect(yield* root.val("stuff")).toEqual("stuffy");
|
||||||
};
|
};
|
||||||
y.connector.flushAll();
|
for (var key in this.users) {
|
||||||
|
var u = this.users[key];
|
||||||
|
u.transact(transaction);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it("Basic get&set of Map property (converge via update)", function(){
|
||||||
|
var y = this.users[0];
|
||||||
|
y.connector.flushAll();
|
||||||
|
y.transact(function*(root){
|
||||||
|
yield* root.val("stuff", "stuffy");
|
||||||
|
expect(yield* root.val("stuff")).toEqual("stuffy");
|
||||||
|
});
|
||||||
|
|
||||||
for (var key in this.users) {
|
var transaction = function*(root){
|
||||||
var u = this.users[key];
|
expect(yield* root.val("stuff")).toEqual("stuffy");
|
||||||
u.transact(transaction);
|
};
|
||||||
}
|
y.connector.flushAll();
|
||||||
});
|
|
||||||
it("Basic get&set of Map property (handle conflict)", function(){
|
|
||||||
var y = this.users[0];
|
|
||||||
y.connector.flushAll();
|
|
||||||
this.users[0].transact(function*(root){
|
|
||||||
yield* root.val("stuff", "c0");
|
|
||||||
});
|
|
||||||
this.users[1].transact(function*(root){
|
|
||||||
yield* root.val("stuff", "c1");
|
|
||||||
});
|
|
||||||
|
|
||||||
var transaction = function*(root){
|
for (var key in this.users) {
|
||||||
expect(yield* root.val("stuff")).toEqual("c0");
|
var u = this.users[key];
|
||||||
};
|
u.transact(transaction);
|
||||||
y.connector.flushAll();
|
}
|
||||||
|
});
|
||||||
|
it("Basic get&set of Map property (handle conflict)", function(){
|
||||||
|
var y = this.users[0];
|
||||||
|
y.connector.flushAll();
|
||||||
|
this.users[0].transact(function*(root){
|
||||||
|
yield* root.val("stuff", "c0");
|
||||||
|
});
|
||||||
|
this.users[1].transact(function*(root){
|
||||||
|
yield* root.val("stuff", "c1");
|
||||||
|
});
|
||||||
|
|
||||||
for (var key in this.users) {
|
var transaction = function*(root){
|
||||||
var u = this.users[key];
|
expect(yield* root.val("stuff")).toEqual("c0");
|
||||||
u.transact(transaction);
|
};
|
||||||
}
|
y.connector.flushAll();
|
||||||
});
|
|
||||||
it("Basic get&set of Map property (handle three conflicts)", function(){
|
|
||||||
var y = this.users[0];
|
|
||||||
y.connector.flushAll();
|
|
||||||
this.users[0].transact(function*(root){
|
|
||||||
yield* root.val("stuff", "c0");
|
|
||||||
});
|
|
||||||
this.users[1].transact(function*(root){
|
|
||||||
yield* root.val("stuff", "c1");
|
|
||||||
});
|
|
||||||
this.users[2].transact(function*(root){
|
|
||||||
yield* root.val("stuff", "c2");
|
|
||||||
});
|
|
||||||
this.users[3].transact(function*(root){
|
|
||||||
yield* root.val("stuff", "c3");
|
|
||||||
});
|
|
||||||
y.connector.flushAll();
|
|
||||||
var transaction = function*(root){
|
|
||||||
expect(yield* root.val("stuff")).toEqual("c0");
|
|
||||||
};
|
|
||||||
|
|
||||||
for (var key in this.users) {
|
for (var key in this.users) {
|
||||||
var u = this.users[key];
|
var u = this.users[key];
|
||||||
u.transact(transaction);
|
u.transact(transaction);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
it("Basic get&set of Map property (handle three conflicts)", function(){
|
||||||
|
var y = this.users[0];
|
||||||
|
y.connector.flushAll();
|
||||||
|
this.users[0].transact(function*(root){
|
||||||
|
yield* root.val("stuff", "c0");
|
||||||
|
});
|
||||||
|
this.users[1].transact(function*(root){
|
||||||
|
yield* root.val("stuff", "c1");
|
||||||
|
});
|
||||||
|
this.users[2].transact(function*(root){
|
||||||
|
yield* root.val("stuff", "c2");
|
||||||
|
});
|
||||||
|
this.users[3].transact(function*(root){
|
||||||
|
yield* root.val("stuff", "c3");
|
||||||
|
});
|
||||||
|
y.connector.flushAll();
|
||||||
|
var transaction = function*(root){
|
||||||
|
expect(yield* root.val("stuff")).toEqual("c0");
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var key in this.users) {
|
||||||
|
var u = this.users[key];
|
||||||
|
u.transact(transaction);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
it("can create a List type", function(){
|
it("can create a List type", function(){
|
||||||
var y = this.users[0];
|
var y = this.users[0];
|
||||||
@ -126,4 +174,47 @@ describe("Yjs (basic)", function(){
|
|||||||
u.transact(transaction);
|
u.transact(transaction);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
describe("Map random tests", function(){
|
||||||
|
var randomMapTransactions = [
|
||||||
|
function* set (map) {
|
||||||
|
yield* map.val("getRandom(keys)", getRandomNumber());
|
||||||
|
}
|
||||||
|
];
|
||||||
|
it(`succeed after ${numberOfTests} actions`, function(){
|
||||||
|
this.users[0].connector.flushAll(); // TODO: Remove!!
|
||||||
|
function* randomTransaction (root) {
|
||||||
|
var f = getRandom(randomMapTransactions);
|
||||||
|
yield* f(root);
|
||||||
|
}
|
||||||
|
for(var i = 0; i < numberOfTests; i++) {
|
||||||
|
var r = getRandomNumber(100);
|
||||||
|
if (r >= 50) {
|
||||||
|
this.users[0].connector.flushOne();
|
||||||
|
} else {
|
||||||
|
getRandom(this.users).transact(randomTransaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compareAllUsers(this.users);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("Map debug tests", function(){
|
||||||
|
beforeEach(function(){
|
||||||
|
this.u1 = this.users[0];
|
||||||
|
this.u2 = this.users[1];
|
||||||
|
this.u3 = this.users[2];
|
||||||
|
});
|
||||||
|
it("concurrent insertions #1", function(){
|
||||||
|
this.u1.transact(function*(root){
|
||||||
|
var op = {
|
||||||
|
content: 1,
|
||||||
|
left: null,
|
||||||
|
right: null,
|
||||||
|
parent: root._model,
|
||||||
|
parentSub: "a"
|
||||||
|
};
|
||||||
|
Struct.Insert.create.call(this, op);
|
||||||
|
});
|
||||||
|
compareAllUsers(this.users);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user