import assert from 'assert' ; import seed from 'seed-random' ; import { Item , Algorithm , newDoc , canInsertNow , getArray , mergeInto , localDelete , Doc , yjsMod , automerge , yjs , printDebugStats , sync9 , Id } from './crdts' ; let errored = false ; const runTests = ( algName , alg ) => { const random = seed ( 'ssx' ) ; const randInt = ( n ) => Math . floor ( random ( ) * n ) ; const randArrItem = ( arr ) => arr [ randInt ( arr . length ) ] ; const randBool = ( weight = 0.5 ) => random ( ) < weight ; const makeItem = < T > ( content , idOrAgent , originLeft , originRight , amSeq , sync9Parent = originLeft , sync9InsertAfter = true ) : Item < T > => ( { content , id : typeof idOrAgent === 'string' ? [ idOrAgent , 0 ] : idOrAgent , isDeleted : false , originLeft : alg === sync9 ? sync9Parent : originLeft , originRight , insertAfter : sync9InsertAfter , seq : amSeq ?? - 1 , } ) ; const integrateFuzzOnce = < T > ( ops , expectedResult ) : number => { let variants = 1 ; const doc = newDoc ( ) ; for ( let numIntegrated = 0 ; numIntegrated < ops . length ; numIntegrated ++ ) { const candidates = [ ] ; for ( const op of ops ) { if ( canInsertNow ( op , doc ) ) { candidates . push ( op ) ; } } assert ( candidates . length > 0 ) ; variants *= candidates . length ; const op = candidates [ randInt ( candidates . length ) ] ; alg . integrate ( doc , op ) ; } try { assert . deepStrictEqual ( getArray ( doc ) , expectedResult ) ; } catch ( e ) { console . log ( ) ; alg . printDoc ( doc ) ; throw e ; } return variants ; } ; const integrateFuzz = < T > ( ops , expectedResult ) => { let variants = integrateFuzzOnce ( ops , expectedResult ) ; for ( let i = 1 ; i < Math . min ( variants * 3 , 100 ) ; i ++ ) { let newVariants = integrateFuzzOnce ( ops , expectedResult ) ; variants = Math . max ( variants , newVariants ) ; } } ; const test = ( fn ) => { if ( alg . ignoreTests && alg . ignoreTests . includes ( fn . name ) ) { process . stdout . write ( `SKIPPING ${ fn . name }\n` ) ; } else { process . stdout . write ( `running ${ fn . name } ...` ) ; try { fn ( ) ; process . stdout . write ( `PASS\n` ) ; } catch ( e ) { process . stdout . write ( `FAIL:\n` ) ; console . log ( e . stack ) ; errored = true ; } } } ; const smoke = ( ) => { const doc = newDoc ( ) ; alg . integrate ( doc , makeItem ( 'a' , [ 'A' , 0 ] , null , null , 0 ) ) ; alg . integrate ( doc , makeItem ( 'b' , [ 'A' , 1 ] , [ 'A' , 0 ] , null , 1 ) ) ; assert . deepStrictEqual ( getArray ( doc ) , [ 'a' , 'b' ] ) ; } ; const smokeMerge = ( ) => { const doc = newDoc ( ) ; alg . integrate ( doc , makeItem ( 'a' , [ 'A' , 0 ] , null , null , 0 ) ) ; alg . integrate ( doc , makeItem ( 'b' , [ 'A' , 1 ] , [ 'A' , 0 ] , null , 1 ) ) ; const doc2 = newDoc ( ) ; mergeInto ( alg , doc2 , doc ) ; assert . deepStrictEqual ( getArray ( doc2 ) , [ 'a' , 'b' ] ) ; } ; const concurrentAvsB = ( ) => { const a = makeItem ( 'a' , 'A' , null , null , 0 ) ; const b = makeItem ( 'b' , 'B' , null , null , 0 ) ; integrateFuzz ( [ a , b ] , [ 'a' , 'b' ] ) ; } ; const interleavingForward = ( ) => { const ops = [ makeItem ( 'a' , [ 'A' , 0 ] , null , null , 0 ) , makeItem ( 'a' , [ 'A' , 1 ] , [ 'A' , 0 ] , null , 1 ) , makeItem ( 'a' , [ 'A' , 2 ] , [ 'A' , 1 ] , null , 2 ) , makeItem ( 'b' , [ 'B' , 0 ] , null , null , 0 ) , makeItem ( 'b' , [ 'B' , 1 ] , [ 'B' , 0 ] , null , 1 ) , makeItem ( 'b' , [ 'B' , 2 ] , [ 'B' , 1 ] , null , 2 ) , ] ; integrateFuzz ( ops , [ 'a' , 'a' , 'a' , 'b' , 'b' , 'b' ] ) ; } ; const interleavingForward2 = ( ) => { const ops = [ makeItem ( 'a' , [ 'A' , 0 ] , null , null , 0 ) , makeItem ( 'a' , [ 'X' , 0 ] , [ 'A' , 0 ] , null , 1 ) , makeItem ( 'a' , [ 'Y' , 0 ] , [ 'X' , 0 ] , null , 2 ) , makeItem ( 'b' , [ 'B' , 0 ] , null , null , 0 ) , makeItem ( 'b' , [ 'C' , 0 ] , [ 'B' , 0 ] , null , 1 ) , makeItem ( 'b' , [ 'D' , 0 ] , [ 'C' , 0 ] , null , 2 ) , ] ; integrateFuzz ( ops , [ 'a' , 'a' , 'a' , 'b' , 'b' , 'b' ] ) ; } ; const interleavingBackward = ( ) => { const ops = [ makeItem ( 'a' , [ 'A' , 0 ] , null , null , 0 ) , makeItem ( 'a' , [ 'A' , 1 ] , null , [ 'A' , 0 ] , 1 ) , makeItem ( 'a' , [ 'A' , 2 ] , null , [ 'A' , 1 ] , 2 ) , makeItem ( 'b' , [ 'B' , 0 ] , null , null , 0 ) , makeItem ( 'b' , [ 'B' , 1 ] , null , [ 'B' , 0 ] , 1 ) , makeItem ( 'b' , [ 'B' , 2 ] , null , [ 'B' , 1 ] , 2 ) , ] ; integrateFuzz ( ops , [ 'a' , 'a' , 'a' , 'b' , 'b' , 'b' ] ) ; } ; const interleavingBackward2 = ( ) => { const ops = [ makeItem ( 'a' , [ 'A' , 0 ] , null , null , 0 ) , makeItem ( 'a' , [ 'X' , 0 ] , null , [ 'A' , 0 ] , 1 , [ 'A' , 0 ] , false ) , makeItem ( 'b' , [ 'B' , 0 ] , null , null , 0 ) , makeItem ( 'b' , [ 'B' , 1 ] , null , [ 'B' , 0 ] , 1 , [ 'B' , 0 ] , false ) , ] ; integrateFuzz ( ops , [ 'a' , 'a' , 'b' , 'b' ] ) ; } ; const withTails = ( ) => { const ops = [ makeItem ( 'a' , [ 'A' , 0 ] , null , null , 0 ) , makeItem ( 'a0' , [ 'A' , 1 ] , null , [ 'A' , 0 ] , 1 , [ 'A' , 0 ] , false ) , makeItem ( 'a1' , [ 'A' , 2 ] , [ 'A' , 0 ] , null , 2 ) , makeItem ( 'b' , [ 'B' , 0 ] , null , null , 0 ) , makeItem ( 'b0' , [ 'B' , 1 ] , null , [ 'B' , 0 ] , 1 , [ 'B' , 0 ] , false ) , makeItem ( 'b1' , [ 'B' , 2 ] , [ 'B' , 0 ] , null , 2 ) , ] ; integrateFuzz ( ops , [ 'a0' , 'a' , 'a1' , 'b0' , 'b' , 'b1' ] ) ; } ; const withTails2 = ( ) => { const ops = [ makeItem ( 'a' , [ 'A' , 0 ] , null , null , 0 ) , makeItem ( 'a0' , [ 'A' , 1 ] , null , [ 'A' , 0 ] , 1 , [ 'A' , 0 ] , false ) , makeItem ( 'a1' , [ 'A' , 2 ] , [ 'A' , 0 ] , null , 2 ) , makeItem ( 'b' , [ 'B' , 0 ] , null , null , 0 ) , makeItem ( 'b0' , [ '1' , 0 ] , null , [ 'B' , 0 ] , 1 , [ 'B' , 0 ] , false ) , makeItem ( 'b1' , [ 'B' , 1 ] , [ 'B' , 0 ] , null , 2 ) , ] ; integrateFuzz ( ops , [ 'a0' , 'a' , 'a1' , 'b0' , 'b' , 'b1' ] ) ; } ; const localVsConcurrent = ( ) => { const a = makeItem ( 'a' , 'A' , null , null , 0 ) ; const c = makeItem ( 'c' , 'C' , null , null , 0 ) ; const b = makeItem ( 'b' , 'B' , null , null , 0 ) ; const d = makeItem ( 'd' , 'D' , [ 'A' , 0 ] , [ 'C' , 0 ] , 1 ) ; integrateFuzz ( [ a , b , c , d ] , [ 'a' , 'd' , 'b' , 'c' ] ) ; } ; const fuzzer1 = ( ) => { const ops = [ makeItem ( 3 , [ '0' , 0 ] , null , null , 0 ) , makeItem ( 5 , [ '1' , 0 ] , null , null , 0 ) , makeItem ( 9 , [ '1' , 1 ] , null , [ '1' , 0 ] , 1 ) , makeItem ( 1 , [ '2' , 0 ] , null , null , 0 ) , makeItem ( 4 , [ '2' , 1 ] , [ '0' , 0 ] , [ '2' , 0 ] , 1 ) , makeItem ( 10 , [ '1' , 2 ] , [ '2' , 1 ] , [ '1' , 1 ] , 2 ) , makeItem ( 7 , [ '2' , 2 ] , [ '2' , 1 ] , [ '2' , 0 ] , 2 ) , ] ; const doc = newDoc < number > ( ) ; ops . forEach ( op => alg . integrate ( doc , op ) ) ; console . log ( getArray ( doc ) ) ; } ; const fuzzSequential = ( ) => { const doc = newDoc ( ) ; let expectedContent = [ ] ; const alphabet = 'xyz123' ; const agents = 'ABCDE' ; let nextContent = 1 ; for ( let i = 0 ; i < 1000 ; i ++ ) { if ( doc . length === 0 || randBool ( 0.5 ) ) { const pos = randInt ( doc . length + 1 ) ; const content = '' + nextContent ++ ; const agent = randArrItem ( agents ) ; alg . localInsert ( doc , agent , pos , content ) ; expectedContent . splice ( pos , 0 , content ) ; } else { const pos = randInt ( doc . length ) ; const agent = randArrItem ( agents ) ; localDelete ( doc , agent , pos ) ; expectedContent . splice ( pos , 1 ) ; } assert . deepStrictEqual ( doc . length , expectedContent . length ) ; assert . deepStrictEqual ( getArray ( doc ) , expectedContent ) ; } } ; const fuzzMultidoc = ( ) => { const agents = [ 'A' , 'B' , 'C' ] ; for ( let j = 0 ; j < 10 ; j ++ ) { process . stdout . write ( '.' ) ; const docs = new Array ( 3 ) . fill ( null ) . map ( ( _ , i ) => { const doc = newDoc ( ) as any ; doc . agent = agents [ i ] ; return doc ; } ) ; const randDoc = ( ) => docs [ randInt ( docs . length ) ] ; let nextItem = 0 ; for ( let i = 0 ; i < 1000 ; i ++ ) { for ( let j = 0 ; j < 3 ; j ++ ) { const doc = randDoc ( ) ; if ( true ) { const pos = randInt ( doc . length + 1 ) ; const content = ++ nextItem ; alg . localInsert ( doc , doc . agent , pos , content ) ; } else { const pos = randInt ( doc . length ) ; localDelete ( doc , doc . agent , pos ) ; } } const a = randDoc ( ) ; const b = randDoc ( ) ; if ( a !== b ) { mergeInto ( alg , a , b ) ; mergeInto ( alg , b , a ) ; try { assert . deepStrictEqual ( getArray ( a ) , getArray ( b ) ) ; } catch ( e ) { console . log ( '\n' ) ; alg . printDoc ( a ) ; console . log ( '\n ---------------\n' ) ; alg . printDoc ( b ) ; throw e ; } } } } } ; console . log ( `--- Running tests for ${ algName } ---` ) ; const tests = [ smoke , smokeMerge , concurrentAvsB , interleavingForward , interleavingForward2 , interleavingBackward , interleavingBackward2 , withTails , withTails2 , localVsConcurrent , fuzzSequential , fuzzMultidoc ] ; tests . forEach ( test ) ; console . log ( '\n\n' ) ; } ; runTests ( 'yjsmod' , yjsMod ) ; runTests ( 'yjs' , yjs ) ; runTests ( 'automerge' , automerge ) ; runTests ( 'sync9' , sync9 ) ; printDebugStats ( ) ; process . exit ( errored ? 1 : 0 ) ;