-- EditTestImpl.mesa
-- written by Bill Paxton, April 1981
-- last edit by Bill Paxton,  2-Nov-81 10:29:03

-- This package provides random testing for editing Text nodes

DIRECTORY
EditTest,
EditSpan,
TextEdit,
TextFind,
TextLooks,
TextLooksSupport,
UndoEvent,
RopeEdit,
GetTree,
TreeCheck,
TextNode,
EditNotify,
RunReader,
RopeReader,
Rope,
RopeInline,
CheckNode,
RandomCard,
RandomLongInt,
Inline;

EditTestImpl: PROGRAM
	IMPORTS EditTest, EditSpan, TextEdit, RopeEdit, EditNotify, TextLooks, TextLooksSupport, TextFind,
		RunReader, RopeReader, Rope, TextNode, UndoEvent, GetTree, TreeCheck,
		CheckNode, RandomCard, RandomLongInt, Inline
	EXPORTS EditTest =
BEGIN
OPEN
	rI:Rope,
	EditTest,
	spanI:EditSpan,
	editI:TextEdit,
	findI:TextFind,
	looksI:TextLooks,
	looksSI:TextLooksSupport,
	undoI:UndoEvent,
	ropeI:RopeEdit,
	nodeI:TextNode,
	runrdrI:RunReader,
	roperdrI:RopeReader,
	checkI:CheckNode,
	notify:EditNotify,
	randC:RandomCard,
	randLI:RandomLongInt;

alpha, beta: PUBLIC Node;
alphaTree, betaTree: PUBLIC Ref;
runrdr1, runrdr2: PUBLIC runrdrI.Ref;
roperdr1, roperdr2: PUBLIC roperdrI.Ref;

numTests: NAT = 28;
testCountArray: ARRAY [0..numTests) OF NAT;
	-- number of times each test is executed by Run
testProcArray: ARRAY [0..numTests) OF PROC = [
	ReplaceText, DeleteText, CopyText, MoveText, MoveTextOnto, TransposeText,
	ReplaceByChar, InsertChar, AppendChar,
	ReplaceByString, InsertString, AppendString,
	ReplaceByRope, InsertRope, AppendRope,
	Find, BackwardsFind,
	ChangeLooks, AddLooks, RemoveLooks, SetLooks, ClearLooks,
	PutGet, PutGetRope, GetBigFile, PutGetTree, PutGetToRope, PutGetToStream
	];

numNodeTests: NAT = 11;
nodeTestCountArray: ARRAY [0..numNodeTests) OF NAT;
nodeTestProcArray: ARRAY [0..numNodeTests) OF PROC = [
	ReplaceNodes, DeleteNodes, CopyNodes, MoveNodes, MoveNodesOnto, TransposeNodes,
	SplitNode, MergeNodes, NestNodes, UnNestNodes, InsertNode
	];


shortest: Offset = 20; -- don't let nodes get shorter than this
longest: Offset = 2000; -- don't let nodes get longer than this

-- ***** Looks Edit operations

noLooks: Looks = looksI.noLooks;
allLooks: Looks = looksI.allLooks;

ChangeLooks: PUBLIC PROC = {
	node: Node ← PickNode[];
	start, end: Offset;
	rem, add: Looks;
	rem ← PickLooks[]; add ← PickLooks[];
	[start,end] ← PickTwo[node];
	IF start=end THEN RETURN;
	BeforeUndo[node];
	spanI.ChangeLooks[TextSpan[node,start,end-start],rem,add,event];
	CheckLooks[node,rem,add,start,end-start] };

AddLooks: PUBLIC PROC = {
	node: Node ← PickNode[];
	start, end: Offset;
	add: Looks ← PickLooks[];
	[start,end] ← PickTwo[node];
	IF start=end THEN RETURN;
	BeforeUndo[node];
	spanI.AddLooks[TextSpan[node,start,end-start],add,event];
	CheckLooks[node,noLooks,add,start,end-start] };

RemoveLooks: PUBLIC PROC = {
	node: Node ← PickNode[];
	start, end: Offset;
	rem: Looks ← PickLooks[];
	[start,end] ← PickTwo[node];
	IF start=end THEN RETURN;
	BeforeUndo[node];
	spanI.RemoveLooks[TextSpan[node,start,end-start],rem,event];
	CheckLooks[node,rem,noLooks,start,end-start] };

SetLooks: PUBLIC PROC = {
	node: Node ← PickNode[];
	start, end: Offset;
	new: Looks ← PickLooks[];
	[start,end] ← PickTwo[node];
	IF start=end THEN RETURN;
	BeforeUndo[node];
	spanI.SetLooks[TextSpan[node,start,end-start],new,event];
	CheckLooks[node,allLooks,new,start,end-start] };

ClearLooks: PUBLIC PROC = {
	node: Node ← PickNode[];
	start, end: Offset;
	[start,end] ← PickTwo[node];
	IF start=end THEN RETURN;
	BeforeUndo[node];
	spanI.ClearLooks[TextSpan[node,start,end-start],event];
	CheckLooks[node,allLooks,noLooks,start,end-start] };

CheckLooks: PROC [
	node: Node, remove, add: Looks, start, len: Offset] = {
	out: Looks ← looksSI.LooksAND[remove,looksSI.LooksNOT[add]];
		-- out contains looks that we are removing and not adding
	loc, runLen: Offset ← start;
	end: Offset ← start+len;
	runs: Runs ← editI.GetRuns[node];
	IF runs=NIL THEN {
		IF add # noLooks THEN ERROR;
		RETURN };
	runrdrI.SetPosition[runrdr1,runs,start];
	UNTIL loc >= end DO -- check the runs in the changed section
		looks: Looks;
		[runLen,looks] ← runrdrI.Get[runrdr1];
		IF looksSI.LooksAND[looks,add] # add THEN ERROR;
		IF looksSI.LooksAND[looks,out] # noLooks THEN ERROR;
		loc ← loc+runLen;
		ENDLOOP;
	TestUndo[] };

-- ***** Display text node as string

nodeText: STRING ← [2000];
ShowNode: PROC [n: Node] RETURNS [s: STRING] = { -- for debugging
	s ← nodeText; s.length ← 0; AppendNode[n,s,0] };

AppendNode: PROC [n: Node, s: STRING, indent: NAT] = {
	THROUGH [0..indent) DO s[s.length] ← ' ; s.length ← s.length+1; ENDLOOP;
	roperdrI.SetPosition[roperdr1,n.rope];
	FOR i:Offset ← editI.Size[n], i-1 UNTIL i=0 DO
		s[s.length] ← roperdrI.Get[roperdr1];
		s.length ← s.length+1; ENDLOOP;
	s[s.length] ← 'M-100B; s.length ← s.length+1;
	FOR c:editI.Ref ← n.child, nodeI.Next[c] UNTIL c=NIL DO
		WITH cc:c SELECT FROM
			text => AppendNode[@cc,s,indent+4];
			ENDCASE;
		ENDLOOP };

-- ***** Find and BackwardsFind
		
backFindFlag: BOOLEAN ← FALSE;
BackwardsFind: PUBLIC PROC = {
	DoFind: PROC = {
		[found,at,atEnd,before,after] ←
			findI.BackwardsFind[pattern: pattern, text: text,
				literal: FALSE, word: FALSE,
				ignoreLooks: FALSE, ignoreCase: TRUE,
				looksExact: FALSE] };
	pattern, text: Node;
	found: BOOLEAN;
	patternLooks, textLooks: Looks;
	at, atEnd, before, after: Offset;
	IF backFindFlag THEN RETURN;
	backFindFlag ← TRUE; -- only do this once
	text ← editI.FromString["abc DEF .ghi. xjkly lmn ghi"];
			--       0    5    0    5    0    5 
	-- first just simple test, no special characters
	pattern ← editI.FromString["DEF"];
	DoFind;
	IF ~found OR at#4 OR atEnd#7 OR before#at OR after#atEnd THEN ERROR;
	-- test for only lower case
	pattern ← editI.FromString["'D'E'F"];
	DoFind;
	IF ~found OR at#4 OR atEnd#7 OR before#at OR after#atEnd THEN ERROR;
	-- pattern in lowercase, target in upper case
	pattern ← editI.FromString["def"];
	DoFind;
	IF ~found OR at#4 OR atEnd#7 OR before#at OR after#atEnd THEN ERROR;
	-- use # inside pattern
	pattern ← editI.FromString["D#F"];
	DoFind;
	IF ~found OR at#4 OR atEnd#7 OR before#at OR after#atEnd THEN ERROR;
	-- use * inside pattern to match 0 characters
	pattern ← editI.FromString["D*EF"];
	DoFind;
	IF ~found OR at#4 OR atEnd#7 OR before#at OR after#atEnd THEN ERROR;
	-- use * inside pattern to match 1 character
	pattern ← editI.FromString["D*F"];
	DoFind;
	IF ~found OR at#4 OR atEnd#7 OR before#at OR after#atEnd THEN ERROR;
	-- use @ inside pattern
	pattern ← editI.FromString["D@F"];
	DoFind;
	IF ~found OR at#4 OR atEnd#7 OR before#at OR after#atEnd THEN ERROR;
	-- use & inside pattern to match 0 characters
	pattern ← editI.FromString["D&EF"];
	DoFind;
	IF ~found OR at#4 OR atEnd#7 OR before#at OR after#atEnd THEN ERROR;
	-- use & inside pattern to match 1 character
	pattern ← editI.FromString["D&F"];
	DoFind;
	IF ~found OR at#4 OR atEnd#7 OR before#at OR after#atEnd THEN ERROR;
	-- use ! and {}'s
	pattern ← editI.FromString["!{DEF}!"];
	DoFind;
	IF ~found OR at#4 OR atEnd#7 OR before#at-1 OR after#atEnd+1 THEN ERROR;
	-- use word search
	pattern ← editI.FromString["DEF"];
	[found,at,atEnd,before,after] ← findI.BackwardsFind[pattern: pattern, text: text,
				literal: FALSE, word: TRUE,
				ignoreLooks: FALSE, ignoreCase: TRUE,
				looksExact: FALSE];
	IF ~found OR at#4 OR atEnd#7 OR before#at-1 OR after#atEnd+1 THEN ERROR;
	-- use ~ and {}'s
	pattern ← editI.FromString["DEF~{GHI}~x"];
	DoFind;
	IF ~found OR at#9 OR atEnd#12 THEN ERROR;
	-- use | at start
	pattern ← editI.FromString["|abc"];
	DoFind;
	IF ~found OR at#0 OR atEnd#3 THEN ERROR;
	-- use | at end
	pattern ← editI.FromString["ghi|"];
	DoFind;
	IF ~found OR at#24 OR atEnd#27 THEN ERROR;
	-- use | at start and end
	pattern ← editI.FromString["|abc*{ghi}|"];
	DoFind;
	IF ~found OR at#24 OR atEnd#27 THEN ERROR;
	-- requires unwinding wildcard stack
	pattern ← editI.FromString["b{YX}~YX&."];
	text ← editI.FromString["bYX-YXaYXa."];
	DoFind;
	IF ~found OR at#1 OR atEnd#3 THEN ERROR;
	-- test looks in patterns
	patternLooks ← PickLooks[];
	textLooks ← looksSI.LooksOR[PickLooks[],patternLooks];
	text ← editI.FromString["abc DEF .ghi. xjkly lmn ghi"];
	pattern ← editI.FromString["DEF"];
	editI.AddLooks[pattern,patternLooks];
	editI.AddLooks[text,textLooks,4,3];
	DoFind;
	IF ~found OR at#4 OR atEnd#7 THEN ERROR;
	pattern ← editI.FromString["D#@"];
	editI.AddLooks[pattern,patternLooks];
	DoFind;
	IF ~found OR at#4 OR atEnd#7 THEN ERROR;
	pattern ← editI.FromString["D*@"];
	editI.AddLooks[pattern,patternLooks];
	DoFind;
	IF ~found OR at#4 OR atEnd#7 THEN ERROR;
	pattern ← editI.FromString["D&@"];
	editI.AddLooks[pattern,patternLooks];
	DoFind;
	IF ~found OR at#4 OR atEnd#7 THEN ERROR;
	pattern ← editI.FromString["D@@"];
	editI.AddLooks[pattern,patternLooks];
	DoFind;
	IF ~found OR at#4 OR atEnd#7 THEN ERROR;
	};

findFlag: BOOLEAN ← FALSE;
Find: PUBLIC PROC = {
	DoFind: PROC = {
		[found,at,atEnd,before,after] ←
			findI.Find[pattern: pattern, text: text,
				literal: FALSE, word: FALSE,
				ignoreLooks: FALSE, ignoreCase: TRUE,
				looksExact: FALSE] };
	pattern, text: Node;
	found: BOOLEAN;
	patternLooks, textLooks: Looks;
	at, atEnd, before, after: Offset;
	IF findFlag THEN RETURN;
	findFlag ← TRUE; -- only do this once
	text ← editI.FromString["abc DEF .ghi. xjkly lmn ghi"];
			--       0    5    0    5    0    5 
	-- first just simple test, no special characters
	pattern ← editI.FromString["ghi"];
	DoFind;
	IF ~found OR at#9 OR atEnd#12 OR before#at OR after#atEnd THEN ERROR;
	-- test for only lower case
	pattern ← editI.FromString["'g'h'i"];
	DoFind;
	IF ~found OR at#9 OR atEnd#12 OR before#at OR after#atEnd THEN ERROR;
	-- pattern in uppercase, target in lower case
	pattern ← editI.FromString["GHI"];
	DoFind;
	IF ~found OR at#9 OR atEnd#12 OR before#at OR after#atEnd THEN ERROR;
	-- use # inside pattern
	pattern ← editI.FromString["G#I"];
	DoFind;
	IF ~found OR at#9 OR atEnd#12 OR before#at OR after#atEnd THEN ERROR;
	-- use * inside pattern to match 0 characters
	pattern ← editI.FromString["G*HI"];
	DoFind;
	IF ~found OR at#9 OR atEnd#12 OR before#at OR after#atEnd THEN ERROR;
	-- use * inside pattern to match 1 character
	pattern ← editI.FromString["G*I"];
	DoFind;
	IF ~found OR at#9 OR atEnd#12 OR before#at OR after#atEnd THEN ERROR;
	-- use @ inside pattern
	pattern ← editI.FromString["G@I"];
	DoFind;
	IF ~found OR at#9 OR atEnd#12 OR before#at OR after#atEnd THEN ERROR;
	-- use & inside pattern to match 0 characters
	pattern ← editI.FromString["G&HI"];
	DoFind;
	IF ~found OR at#9 OR atEnd#12 OR before#at OR after#atEnd THEN ERROR;
	-- use & inside pattern to match 1 character
	pattern ← editI.FromString["G&I"];
	DoFind;
	IF ~found OR at#9 OR atEnd#12 OR before#at OR after#atEnd THEN ERROR;
	-- use ! and {}'s
	pattern ← editI.FromString["!{GHI}!"];
	DoFind;
	IF ~found OR at#9 OR atEnd#12 OR before#at-1 OR after#atEnd+1 THEN ERROR;
	-- use word search
	pattern ← editI.FromString["GHI"];
	[found,at,atEnd,before,after] ← findI.Find[pattern: pattern, text: text,
				literal: FALSE, word: TRUE,
				ignoreLooks: FALSE, ignoreCase: TRUE,
				looksExact: FALSE];
	IF ~found OR at#9 OR atEnd#12 OR before#at-1 OR after#atEnd+1 THEN ERROR;
	-- use ~ and {}'s
	pattern ← editI.FromString["DEF~{GHI}~x"];
	DoFind;
	IF ~found OR at#9 OR atEnd#12 THEN ERROR;
	-- use | at start
	pattern ← editI.FromString["|abc"];
	DoFind;
	IF ~found OR at#0 OR atEnd#3 THEN ERROR;
	-- use | at end
	pattern ← editI.FromString["ghi|"];
	DoFind;
	IF ~found OR at#24 OR atEnd#27 THEN ERROR;
	-- use | at start and end
	pattern ← editI.FromString["|abc*{ghi}|"];
	DoFind;
	IF ~found OR at#24 OR atEnd#27 THEN ERROR;
	-- requires unwinding wildcard stack
	pattern ← editI.FromString[".&XY~{XY}b"];
	text ← editI.FromString[".aXYaXY-XYb"];
	DoFind;
	IF ~found OR at#8 OR atEnd#10 THEN ERROR;
	-- test looks in patterns
	patternLooks ← PickLooks[];
	textLooks ← looksSI.LooksOR[PickLooks[],patternLooks];
	text ← editI.FromString["abc DEF .ghi. xjkly lmn ghi"];
	pattern ← editI.FromString["ghi"];
	editI.AddLooks[pattern,patternLooks];
	editI.AddLooks[text,textLooks,24,3];
	DoFind;
	IF ~found OR at#24 OR atEnd#27 THEN ERROR;
	pattern ← editI.FromString["@#i"];
	editI.AddLooks[pattern,patternLooks];
	DoFind;
	IF ~found OR at#24 OR atEnd#27 THEN ERROR;
	pattern ← editI.FromString["@*i"];
	editI.AddLooks[pattern,patternLooks];
	DoFind;
	IF ~found OR at#24 OR atEnd#27 THEN ERROR;
	pattern ← editI.FromString["@&i"];
	editI.AddLooks[pattern,patternLooks];
	DoFind;
	IF ~found OR at#24 OR atEnd#27 THEN ERROR;
	pattern ← editI.FromString["@@i"];
	editI.AddLooks[pattern,patternLooks];
	DoFind;
	IF ~found OR at#24 OR atEnd#27 THEN ERROR;
	};


-- ***** Support routines for node edits

LocateSpan: PUBLIC PROC [span: Span] RETURNS [start, len, size: Offset] = {
	first: Ref ← span.start.node;
	last: Ref ← span.end.node;
	root: Ref ← nodeI.Root[first];
	size ← 0; start ← len ← -1;
	FOR n: Ref ← nodeI.FirstChild[root], nodeI.StepForward[n] DO
		SELECT n FROM
			NIL => RETURN;
			first => { start ← size; IF n=last THEN len ← 1 };
			last => len ← size-start+1;
			ENDCASE;
		size ← size+1;
		ENDLOOP };

LocateNode: PUBLIC PROC [node: Ref] RETURNS [loc, size: Offset] = {
	root: Ref ← nodeI.Root[node];
	size ← 0; loc ← -1;
	FOR n: Ref ← nodeI.FirstChild[root], nodeI.StepForward[n] DO
		SELECT n FROM
			NIL => RETURN;
			node => loc ← size;
			ENDCASE;
		size ← size+1;
		ENDLOOP };

PickTrees: PUBLIC PROC RETURNS [tree1, tree2: Ref] = {
	RETURN [PickTree[], PickTree[]] };

PickTree: PUBLIC PROC RETURNS [tree: Ref] = {
	RETURN [IF RandomBoolean[] THEN alphaTree ELSE betaTree] };

PickPlace: PUBLIC PROC RETURNS [Place] = {
	RETURN [SELECT randLI.Choose[1,3] FROM
		1 => before, 2 => after, 3 => child, ENDCASE => ERROR] };

PickNodeSpan: PUBLIC PROC [tree: Ref] RETURNS [span: Span, start, len, size: Offset] = {
	end: Offset;
	first, last: Ref;
	IF (size ← TreeSize[tree]) = 0 THEN RETURN [nodeI.nullSpan, 0, 0, 0];
	DO	[start, end] ← ChooseTwo[0,size];
		IF (len ← end-start) > 0 THEN EXIT;
		ENDLOOP;
	first ← FindFirstNode[tree, start];
	last ← FindLastNode[first, len];
	span ← nodeI.MakeNodeSpan[first, last] };

PickNodeLoc: PUBLIC PROC [tree: Ref] RETURNS [dest: Location, loc, size: Offset] = {
	node: Ref;
	IF (size ← TreeSize[tree]) = 0 THEN RETURN [nodeI.nullLocation, 0, 0];
	loc ← Choose[0,size-1];
	node ← FindTreeNode[tree, loc];
	dest ← nodeI.MakeNodeLoc[node] };

FindTreeNode, FindFirstNode: PUBLIC PROC [tree: Ref, loc: Offset] RETURNS [node: Ref] = {
	place: Offset ← 0;
	FOR n: Ref ← nodeI.FirstChild[tree], nodeI.StepForward[n] DO
		IF place=loc OR n=NIL THEN RETURN [n];
		place ← place+1;
		ENDLOOP };


FindLastNode: PUBLIC PROC [first: Ref, len: Offset] RETURNS [last: Ref] = {
	IF len <= 0 THEN RETURN [NIL];
	FOR n: Ref ← first, nodeI.StepForward[n] DO
		IF (len←len-1)=0 OR n=NIL THEN RETURN [n];
		ENDLOOP };

TreeSize: PUBLIC PROC [tree: Ref] RETURNS [size: Offset] = {
	size ← 0;
	FOR n: Ref ← nodeI.FirstChild[tree], nodeI.StepForward[n] UNTIL n=NIL DO
		size ← size+1; ENDLOOP };
biggestTree: Offset = 200;
smallestTree: Offset = 50;
AdjustTree: PUBLIC PROC [tree: Ref, size: Offset] = {
	DO SELECT size FROM
		> biggestTree => size ← DeleteFromTree[tree, size, size-biggestTree];
		< smallestTree => size ← InsertInTree[tree, size, smallestTree-size];
		ENDCASE => RETURN;
		ENDLOOP };


-- ***** Support routines

Choose: PUBLIC PROC [min,max: Offset] RETURNS [Offset] = {
	IF min >= max THEN RETURN [max];
	RETURN [SELECT randLI.Choose[1,8] FROM
		1 => min, 2 => max, ENDCASE => LOOPHOLE[randLI.Choose[min,max]]] };

ChooseTwo: PUBLIC PROC [min,max: Offset] RETURNS [first,second: Offset] = {
	temp: Offset;
	first ← Choose[min,max]; second ← Choose[min,max];
	IF first > second THEN { temp←first; first←second; second←temp }};

ChooseNAT: PUBLIC PROC [min,max: NAT] RETURNS [NAT] = {
	maxNAT: NAT = LAST[NAT];
	IF min >= max THEN RETURN [max];
	RETURN [SELECT randLI.Choose[1,8] FROM
		1 => min, 2 => max,
		ENDCASE => LOOPHOLE[Inline.LowHalf[randLI.Choose[min,max]]]] };

ChooseTwoNATs: PUBLIC PROC [min,max: NAT] RETURNS [first,second: NAT] = {
	temp: NAT;
	first ← ChooseNAT[min,max]; second ← ChooseNAT[min,max];
	IF first > second THEN { temp←first; first←second; second←temp }};

RandomBoolean: PUBLIC PROC RETURNS [BOOLEAN] =
	{ RETURN [randC.Choose[0,1]=0] };

OneInN: PUBLIC PROC [n: NAT] RETURNS [BOOLEAN] =
	{ RETURN [randC.Choose[1,n]=1] };

PickNode: PUBLIC PROC RETURNS [Node] = { RETURN [
	IF RandomBoolean[] THEN alpha ELSE beta] };

PickNodes: PUBLIC PROC RETURNS [source,dest: Node] = {
	RETURN [PickNode[], PickNode[]] };

PickOne: PUBLIC PROC [node: Node] RETURNS [loc: Offset] = {
	-- picks a random places within node
	RETURN [Choose[0,editI.Size[node]]] };

PickTwo: PUBLIC PROC [node: Node] RETURNS [start, end: Offset] = {
	-- picks two random places within node
	-- orders them such that start <= end
	size, temp: Offset;
	IF (size ← editI.Size[node]) = 0 THEN RETURN;
	start ← Choose[0,size]; end ← Choose[0,size];
	IF start > end THEN { temp←start; start←end; end←temp }};

PickLooks: PUBLIC PROC RETURNS [Looks] = {
	RETURN [LOOPHOLE[randLI.Random[]]] };

PickRope: PUBLIC PROC RETURNS [Rope] = {
	source: Node ← PickNode[];
	sourceStart, sourceEnd: Offset;
	[sourceStart,sourceEnd] ← PickTwo[source];
	RETURN [ropeI.Substr[editI.GetRope[source],sourceStart,sourceEnd-sourceStart]] };

AdjustLengths: PUBLIC PROC = { AdjustLength[alpha]; AdjustLength[beta] };

AdjustLength: PUBLIC PROC [node: Node] = {
	DO SELECT editI.Size[node] FROM
		> longest => DeleteFromNode[node];
		< shortest => InsertStringInNode[node];
		ENDCASE => RETURN;
		ENDLOOP };
	
event: PUBLIC Event ← undoI.Create[];
undoEvent: PUBLIC Event ← undoI.Create[];
txt1, txt2: Node;
t1Rope, t2Rope: rI.Ref;
t1Runs, t2Runs: looksI.Runs;

BeforeUndo: PUBLIC PROC [n1, n2: Node ← NIL] = {
	txt1 ← n1;
	t1Rope ← txt1.rope; t1Runs ← txt1.runs;
	IF n2 = NIL OR n2 = n1 THEN txt2 ← NIL
	ELSE { txt2 ← n2; t2Rope ← txt2.rope; t2Runs ← txt2.runs };
	undoI.Reset[event] };

TestUndo: PUBLIC PROC = { -- 
	t1RopeBefore, t2RopeBefore, t1RopeAfter, t2RopeAfter: rI.Ref;
	t1RunsBefore, t2RunsBefore, t1RunsAfter, t2RunsAfter: looksI.Runs;
	oldSize1, oldSize2, newSize1, newSize2: Offset;
	node1, node2: nodeI.RefTextNode;
	t1RopeBefore ← t1Rope; t2RopeBefore ← t2Rope; -- set by PreEdit
	t1RunsBefore ← t1Runs; t2RunsBefore ← t2Runs; -- set by PreEdit
	IF (node1 ← txt1) # NIL THEN { t1RopeAfter ← node1.rope; t1RunsAfter ← node1.runs };
	IF (node2 ← txt2) # NIL THEN { t2RopeAfter ← node2.rope; t2RunsAfter ← node2.runs };
	undoI.Reset[undoEvent];
	undoI.Undo[event,undoEvent];
	IF node1 # NIL THEN {
		oldSize1 ← ropeI.Size[t1RopeBefore];
		IF ropeI.Size[node1.rope] # oldSize1 THEN ERROR;
		CheckRopes[node1.rope,0,t1RopeBefore,0,oldSize1];
		CheckRuns[node1.runs,0,t1RunsBefore,0,oldSize1] };
	IF node2 # NIL THEN {
		oldSize2 ← ropeI.Size[t2RopeBefore];
		IF ropeI.Size[node2.rope] # oldSize2 THEN ERROR;
		CheckRopes[node2.rope,0,t2RopeBefore,0,oldSize2];
		CheckRuns[node2.runs,0,t2RunsBefore,0,oldSize2] };
	undoI.Undo[undoEvent]; -- undo the previous undo
	IF node1 # NIL THEN {
		newSize1 ← ropeI.Size[t1RopeAfter];
		IF ropeI.Size[node1.rope] # newSize1 THEN ERROR;
		CheckRopes[node1.rope,0,t1RopeAfter,0,newSize1];
		CheckRuns[node1.runs,0,t1RunsAfter,0,newSize1] };
	IF node2 # NIL THEN {
		newSize2 ← ropeI.Size[t2RopeAfter];
		IF ropeI.Size[node2.rope] # newSize2 THEN ERROR;
		CheckRopes[node2.rope,0,t2RopeAfter,0,newSize2];
		CheckRuns[node2.runs,0,t2RunsAfter,0,newSize2] }};


-- ***** Initialization

numEdits: PUBLIC NAT ← 0;

PreEdit: PROC [t1, t2: nodeI.Ref, change: REF READONLY notify.Change] = {
	IF t1 = NIL THEN ERROR;
	IF t1.kind # text THEN ERROR;
	IF t2 # NIL AND t2.kind # text THEN ERROR };

PostEdit: PROC [t1, t2: nodeI.Ref, change: REF READONLY notify.Change] = {
	OPEN checkI;
	IF t1 # NIL THEN WITH x:t1 SELECT FROM
		text => CheckTextNode[@x];
		ENDCASE => ERROR;
	IF t2 # NIL THEN WITH x:t2 SELECT FROM
		text => CheckTextNode[@x];
		ENDCASE => ERROR;
	};

Flatten: PROC [text: Node] = { -- flatten if more than pieceMax pieces
	pieceMax: Offset = 250; 
	oldRope: Rope ← text.rope;
	oldRuns: Runs ← text.runs;
	newRope: Rope;
	newRuns: Runs;
	oldSize: Offset ← ropeI.Size[oldRope];
	ropePieces: Offset ← ropeI.CountPieces[oldRope];
	IF ropePieces > pieceMax THEN {
		text.rope ← newRope ← ropeI.Flatten[oldRope];
		text.runs ← newRuns ← looksI.Flatten[oldRuns];
		checkI.CheckTextNode[text];
		CheckRopes[newRope,0,oldRope,0,oldSize];
		CheckRuns[newRuns,0,oldRuns,0,oldSize];
		text.count ← 0 }
	ELSE { text.count ← text.count/2 }};

InitReaders: PROC = {
	runrdr1 ← runrdrI.Create[]; runrdr2 ← runrdrI.Create[];
	roperdr1 ← roperdrI.Create[]; roperdr2 ← roperdrI.Create[] };

InitNodes: PROC = {
	alpha ← editI.FromRope[rI.FromString[
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"]];
	beta ← editI.FromRope[rI.FromString[
"ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ"]];
	AdjustLengths[];
	};

InitTrees: PROC = {
	treeFileName: Rope ← rI.FromString["TestTree.Text"];
	alphaTree ← GetTree.ReadIndent[treeFileName];
	TreeCheck.Verify[alphaTree];
	betaTree ←  GetTree.ReadIndent[treeFileName];
	TreeCheck.Verify[betaTree];
	};

brkAt: NAT ← LAST[NAT];
testNum: NAT;

Run: PUBLIC PROC [shuffle: NAT ← 0, numberOfTests: NAT ← LAST[NAT],
	mode: RunMode ← both] = {
	[] ← randC.InitRandom[];
	[] ← randLI.InitRandom[];
	notify.AddNotifyProc[PreEdit,before];
	notify.AddNotifyProc[PostEdit,after];
	InitReaders[]; InitNodes[]; InitTrees[];
	FOR i:CARDINAL IN [0..numTests) DO
		testCountArray[i] ← 0; ENDLOOP;
	FOR i:CARDINAL IN [0..numNodeTests) DO
		nodeTestCountArray[i] ← 0; ENDLOOP;
	THROUGH [0..shuffle) DO [] ← randC.Choose[0,numTests-1]; ENDLOOP;
	UNTIL numEdits >= numberOfTests DO
		IF mode=nodesOnly OR (mode=both AND RandomBoolean[]) THEN { -- do a node test this time
			testNum ← randC.Choose[0,numNodeTests-1];
			nodeTestProcArray[testNum][];
			nodeTestCountArray[testNum] ← nodeTestCountArray[testNum]+1 }
		ELSE { -- do a text test
			testNum ← randC.Choose[0,numTests-1];
			testProcArray[testNum][];
			testCountArray[testNum] ← testCountArray[testNum]+1;
			IF alpha.count >= 30 THEN Flatten[alpha];
			IF beta.count >= 30 THEN Flatten[beta] };
		IF (numEdits ← numEdits+1) >= brkAt THEN { -- break here
			foo: NAT ← 0; foo ← foo+1 };
		ENDLOOP };

END.