Skip to content

Commit

Permalink
Merge pull request #537 from streamich/extension-tests
Browse files Browse the repository at this point in the history
Extension tests
  • Loading branch information
streamich authored Mar 6, 2024
2 parents 4ef2fcc + 20f30b7 commit 2741edc
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 82 deletions.
82 changes: 0 additions & 82 deletions src/json-crdt-extensions/mval/__tests__/extension.spec.ts

This file was deleted.

3 changes: 3 additions & 0 deletions src/json-crdt/nodes/vec/VecNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export class VecNode<Value extends JsonNode[] = JsonNode[]> implements JsonNode<

/**
* @ignore
* @returns Returns the extension data node if this is an extension node,
* otherwise `undefined`. The node is cached after the first access.
*/
public ext(): JsonNode | undefined {
if (this.__extNode) return this.__extNode;
Expand All @@ -92,6 +94,7 @@ export class VecNode<Value extends JsonNode[] = JsonNode[]> implements JsonNode<

/**
* @ignore
* @returns Returns extension ID if this is an extension node, otherwise -1.
*/
public getExtId(): number {
if (this.elements.length !== 2) return -1;
Expand Down
119 changes: 119 additions & 0 deletions src/json-crdt/nodes/vec/__tests__/VecNode-extension.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import {ITimestampStruct, delayed, s} from '../../../../json-crdt-patch';
import {printTree} from '../../../../util/print/printTree';
import {Printable} from '../../../../util/print/types';
import {ext} from '../../../extensions';
import {ExtensionApi, ExtensionDefinition, ExtensionJsonNode} from '../../../extensions/types';
import {Model, NodeApi} from '../../../model';
import {StrNode} from '../../nodes';
import {JsonNode} from '../../types';

test('treats a simple "vec" node as a vector', () => {
const model = Model.withLogicalClock();
model.api.root({
vec: s.vec(),
});
expect(model.view()).toEqual({
vec: [],
});
expect(model.api.vec(['vec']).node.isExt()).toBe(false);
});

test('does not treat "vec" node as extension, if extension is not registered in registry', () => {
const model = Model.withLogicalClock();
model.api.root({
vec: s.vec(),
});
const vec = model.api.vec(['vec']);
const buf = new Uint8Array(3);
buf[0] = 42;
buf[1] = vec.node.id.sid % 256;
buf[2] = vec.node.id.time % 256;
vec.push(buf, 123);
expect(vec.node.isExt()).toBe(false);
});

describe('sample extension', () => {
const DoubleConcatExt: ExtensionDefinition<StrNode, any, any> = {
id: 123,
name: 'double-concat',
new: (value: string = '') =>
ext(
123,
delayed((builder) => builder.json(value)),
),
Node: class CntNode implements ExtensionJsonNode, Printable {
public readonly id: ITimestampStruct;

constructor(public readonly data: StrNode) {
this.id = data.id;
}

public name(): string {
return 'double-concat';
}

public view(): string {
const str = String(this.data.view());
return str + str;
}

public children(callback: (node: JsonNode) => void): void {}

public child?(): JsonNode | undefined {
return this.data;
}

public container(): JsonNode | undefined {
return this.data.container();
}

public api: undefined | unknown = undefined;

public toString(tab?: string): string {
return `${this.name()} (${this.view()})` + printTree(tab, [(tab) => this.data.toString(tab)]);
}
},
Api: class CntApi extends NodeApi<any> implements ExtensionApi<any> {
public ins(index: number, text: string): this {
const {api, node} = this;
const dataApi = api.wrap(node.data as StrNode);
dataApi.ins(index, text);
return this;
}
},
};

test('can run an extension', () => {
const model = Model.withLogicalClock();
model.ext.register(DoubleConcatExt);
model.api.root({
str: DoubleConcatExt.new('foo'),
});
expect(model.view()).toEqual({
str: 'foofoo',
});
});

test('can use extension node', () => {
const model = Model.withLogicalClock();
model.ext.register(DoubleConcatExt);
model.api.root({
str: DoubleConcatExt.new('abc'),
});
const extNode = model.api.in(['str']).asExt(DoubleConcatExt).node;
expect(extNode.view()).toBe('abcabc');
});

test('can use extension API node', () => {
const model = Model.withLogicalClock();
model.ext.register(DoubleConcatExt);
model.api.root({
str: DoubleConcatExt.new('abc'),
});
const extStr = model.api.in(['str']).asExt(DoubleConcatExt);
extStr.ins(1, '.');
expect(model.view()).toEqual({
str: 'a.bca.bc',
});
});
});
98 changes: 98 additions & 0 deletions src/json-crdt/nodes/vec/__tests__/extension.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {ValueMvExt} from '../../../../json-crdt-extensions/mval';
import {konst} from '../../../../json-crdt-patch/builder/Konst';
import {Model} from '../../../../json-crdt/model';

test('can specify extension name', () => {
expect(ValueMvExt.name).toBe('mval');
});

test('can create a new multi-value register', () => {
const model = Model.withLogicalClock();
model.ext.register(ValueMvExt);
model.api.root({
mv: ValueMvExt.new(),
});
expect(model.view()).toEqual({
mv: [],
});
});

test('can provide initial value', () => {
const model = Model.withLogicalClock();
model.ext.register(ValueMvExt);
model.api.root({
mv: ValueMvExt.new({foo: 'bar'}),
});
expect(model.view()).toEqual({
mv: [{foo: 'bar'}],
});
});

test('can read view from node or API node', () => {
const model = Model.withLogicalClock();
model.ext.register(ValueMvExt);
model.api.root({
mv: ValueMvExt.new('foo'),
});
const api = model.api.in('mv').asExt(ValueMvExt);
expect(api.view()).toEqual(['foo']);
expect(api.node.view()).toEqual(['foo']);
});

test('exposes API to edit extension data', () => {
const model = Model.withLogicalClock();
model.ext.register(ValueMvExt);
model.api.root({
mv: ValueMvExt.new(),
});
const nodeApi = model.api.in('mv').asExt(ValueMvExt);
nodeApi.set(konst('lol'));
expect(model.view()).toEqual({
mv: ['lol'],
});
});

describe('extension validity checks', () => {
test('does not treat ArrNode as extension if header is too long', () => {
const model = Model.withLogicalClock();
model.ext.register(ValueMvExt);
model.api.root({
mv: ValueMvExt.new(),
});
const buf = new Uint8Array(4);
buf.set(model.api.const(['mv', 0]).node.view() as Uint8Array, 0);
model.api.vec(['mv']).set([[0, buf]]);
expect(model.view()).toEqual({
mv: [buf, []],
});
expect(model.api.vec(['mv']).node.isExt()).toBe(false);
});

test('does not treat ArrNode as extension if header sid is wrong', () => {
const model = Model.withLogicalClock();
model.ext.register(ValueMvExt);
model.api.root({
mv: ValueMvExt.new(),
});
const buf = model.api.const(['mv', 0]).node.view() as Uint8Array;
buf[1] += 1;
expect(model.view()).toEqual({
mv: [buf, []],
});
expect(model.api.vec(['mv']).node.isExt()).toBe(false);
});

test('does not treat ArrNode as extension if header time is wrong', () => {
const model = Model.withLogicalClock();
model.ext.register(ValueMvExt);
model.api.root({
mv: ValueMvExt.new(),
});
const buf = model.api.const(['mv', 0]).node.view() as Uint8Array;
buf[2] += 1;
expect(model.view()).toEqual({
mv: [buf, []],
});
expect(model.api.vec(['mv']).node.isExt()).toBe(false);
});
});

0 comments on commit 2741edc

Please sign in to comment.