1 module auxil.test;
2 
3 version(unittest) import unit_threaded : Name;
4 
5 import auxil.model;
6 
7 @safe private
8 struct PrettyPrintingVisitor
9 {
10 	import std.experimental.allocator.mallocator : Mallocator;
11 	import automem.vector : Vector;
12 
13 	Vector!(char, Mallocator) output;
14 	private Vector!(char, Mallocator) _indentation;
15 	DefaultVisitor default_visitor;
16 	alias default_visitor this;
17 
18 	this(float size) @nogc
19 	{
20 		default_visitor = DefaultVisitor(size);
21 	}
22 
23 	auto processItem(T...)(T msg)
24 	{
25 		() @trusted {
26 			output ~= _indentation[];
27 			import nogc.conv : text;
28 			output ~= text(msg, "\n")[];
29 		} ();
30 	}
31 
32 	void indent() @nogc @trusted
33 	{
34 		_indentation ~= '\t';
35 	}
36 
37 	void unindent() @nogc @trusted
38 	{
39 		if (_indentation.length)
40 			_indentation.popBack;
41 	}
42 
43 	void enterTree(Order order, Data, Model)(auto ref const(Data) data, ref Model model)
44 	{
45 		position = 0;
46 	}
47 
48 	void enterNode(Order order, Data, Model)(ref const(Data) data, ref Model model)
49 	{
50 		import std.conv : to;
51 		import auxil.traits : hasRenderHeader;
52 
53 		static if (hasRenderHeader!data)
54 		{
55 			import auxil.model : FixedAppender;
56 			FixedAppender!512 app;
57 			data.renderHeader(app);
58 			() @trusted { processItem(app[]); } ();
59 		}
60 		else
61 			processItem("Caption: ", Data.stringof);
62 	}
63 
64 	void processLeaf(Order order, Data, Model)(ref const(Data) data, ref Model model)
65 	{
66 		processItem(data);
67 	}
68 }
69 
70 version(unittest) @Name("aggregates_trivial")
71 @safe
72 unittest
73 {
74 	static struct Test
75 	{
76 		float f = 7.7;
77 		int i = 8;
78 		string s = "some text";
79 	}
80 
81 	static struct StructWithStruct
82 	{
83 		double d = 8.8;
84 		long l = 999;
85 		Test t;
86 	}
87 
88 	static class TestClass
89 	{
90 
91 	}
92 
93 	static struct StructWithPointerAndClass
94 	{
95 		double* d;
96 		TestClass tc;
97 	}
98 
99 	static struct StructWithNestedClass
100 	{
101 		TestClass tc;
102 	}
103 
104 	auto visitor = PrettyPrintingVisitor(9);
105 	auto d = StructWithStruct();
106 	auto m = makeModel(d);
107 	m.visitForward(d, visitor);
108 	assert(m.size == 10);
109 	d.d = 0;
110 	d.l = 1;
111 	d.t.f = 2;
112 	d.t.i = 3;
113 	d.t.s = "s";
114 	m.visitForward(d, visitor);
115 	m.collapsed = false;
116 	m.visitForward(d, visitor);
117 	m.t.collapsed = false;
118 	m.visitForward(d, visitor);
119 }
120 
121 version(unittest) @Name("static_array")
122 @nogc
123 unittest
124 {
125 	version(Windows) {}
126 	else {
127 		import core.stdc.locale;
128 		setlocale(LC_NUMERIC, "C");
129 	}
130 	float[3] d = [1.1f, 2.2f, 3.3f];
131 	auto m = Model!d();
132 
133 	auto visitor = PrettyPrintingVisitor(9);
134 	visitor.processItem;
135 	m.collapsed = false;
136 	m.visitForward(d, visitor);
137 
138 	visitor.output ~= '\0';
139 	version(none)
140 	{
141 		import core.stdc.stdio : printf;
142 		printf("%s\nlength: %ld\n", visitor.output[].ptr, visitor.output.length);
143 	}
144 
145 	import std.algorithm : equal;
146 	assert(visitor.output[].equal("
147 Caption: float[3]
148 	1.100000
149 	2.200000
150 	3.300000
151 \0"
152 	));
153 }
154 
155 version(unittest) @Name("dynamic_array")
156 unittest
157 {
158 	float[] d = [1.1f, 2.2f, 3.3f];
159 	auto m = Model!d();
160 
161 	auto visitor = PrettyPrintingVisitor(9);
162 	visitor.processItem;
163 	m.collapsed = false;
164 	m.model.length = d.length;
165 	m.visitForward(d, visitor);
166 
167 	d ~= [4.4f, 5.5f];
168 	m.model.length = d.length;
169 	m.visitForward(d, visitor);
170 
171 	d = d[2..3];
172 	m.model.length = d.length;
173 	m.visitForward(d, visitor);
174 
175 	visitor.output ~= '\0';
176 	version(none)
177 	{
178 		import core.stdc.stdio : printf;
179 		printf("%s\nlength: %ld\n", visitor.output[].ptr, visitor.output.length);
180 	}
181 
182 	import std.algorithm : equal;
183 	assert(visitor.output[].equal("
184 Caption: float[]
185 	1.100000
186 	2.200000
187 	3.300000
188 Caption: float[]
189 	1.100000
190 	2.200000
191 	3.300000
192 	4.400000
193 	5.500000
194 Caption: float[]
195 	3.300000
196 \0"
197 	));
198 }
199 
200 version(unittest) @Name("aggregate_with_only_member")
201 @nogc unittest
202 {
203 	static struct OneMember
204 	{
205 		string one = "one";
206 	}
207 
208 	auto d = OneMember();
209 	auto m = Model!d();
210 	import std.traits : FieldNameTuple;
211 	import std.meta : AliasSeq;
212 	static assert(FieldNameTuple!(typeof(m)) == AliasSeq!("single_member_model"));
213 
214 	auto visitor = PrettyPrintingVisitor(9);
215 	visitor.processItem;
216 	m.visitForward(d, visitor);
217 
218 	visitor.output ~= '\0';
219 
220 	version(none)
221 	{
222 		import core.stdc.stdio : printf;
223 		printf("---\n%s\n---\nlength: %ld\n",
224 			visitor.output[].ptr, visitor.output.length);
225 	}
226 
227 	import std.algorithm : equal;
228 	assert(visitor.output[].equal("
229 one
230 \0"
231 	));
232 }
233 
234 version(unittest) @Name("aggregate_with_render_header")
235 unittest
236 {
237 	static struct Aggregate
238 	{
239 		float f;
240 		long l;
241 
242 		void renderHeader(W)(ref W writer) @trusted const
243 		{
244 			import core.stdc.stdio : snprintf;
245 
246 			char[128] buffer;
247 			const l = snprintf(&buffer[0], buffer.length, "Custom header %f, %ld", f, l);
248 			writer.put(buffer[0..l]);
249 		}
250 	}
251 
252 	import auxil.traits : hasRenderHeader;
253 	static assert(hasRenderHeader!Aggregate);
254 
255 	auto d = Aggregate();
256 
257 	static assert(hasRenderHeader!d);
258 	auto m = Model!d();
259 	m.collapsed = false;
260 
261 	auto visitor = PrettyPrintingVisitor(9);
262 	visitor.processItem;
263 	m.visitForward(d, visitor);
264 
265 	visitor.output ~= '\0';
266 
267 	version(none)
268 	{
269 		import core.stdc.stdio : printf;
270 		printf("---\n%s\n---\nlength: %ld\n",
271 			visitor.output[].ptr, visitor.output.length);
272 	}
273 
274 	import std.algorithm : equal;
275 	assert(visitor.output[].equal("
276 Custom header nan, 0
277 	nan
278 	0
279 \0"
280 	));
281 }
282 
283 version(unittest)
284 {
285 	import auxil.traits : renderedAs, renderedAsMember;
286 
287 	private struct Proxy
288 	{
289 		float f;
290 
291 		this(ref const(Test3) t3)
292 		{
293 			f = t3.f;
294 		}
295 	}
296 
297 	@renderedAs!Proxy
298 	private struct Test3
299 	{
300 		int i;
301 		float f;
302 	}
303 
304 	@renderedAs!int
305 	private struct Test4
306 	{
307 		int i;
308 		float f;
309 
310 		int opCast(T : int)() const
311 		{
312 			return i;
313 		}
314 	}
315 
316 	@renderedAsMember!"t4.i"
317 	private struct Test5
318 	{
319 		Test4 t4;
320 		float f;
321 	}
322 }
323 
324 version(unittest) @Name("aggregate_proxy")
325 unittest
326 {
327 	import auxil.traits :hasRenderedAs;
328 	{
329 		auto test3 = Test3(123, 123.0f);
330 
331 		auto d = Proxy(test3);
332 		auto m = makeModel(d);
333 
334 		static assert( hasRenderedAs!Test3);
335 		static assert(!hasRenderedAs!test3);
336 		import std.traits : FieldNameTuple;
337 		import std.meta : AliasSeq;
338 		static assert(FieldNameTuple!(typeof(m)) == AliasSeq!("single_member_model"));
339 
340 		auto visitor = PrettyPrintingVisitor(9);
341 		visitor.processItem;
342 		m.visitForward(d, visitor);
343 
344 		visitor.output ~= '\0';
345 
346 		version(none)
347 		{
348 			import core.stdc.stdio : printf;
349 			printf("---\n%s\n---\nlength: %ld\n",
350 				visitor.output[].ptr, visitor.output.length);
351 		}
352 
353 		import std.algorithm : equal;
354 		assert(visitor.output[].equal("
355 123.000000
356 \0"
357 		));
358 	}
359 
360 	{
361 		auto d = Test3();
362 		auto m = makeModel(d);
363 		static assert(!m.Collapsable);
364 
365 		auto visitor = PrettyPrintingVisitor(9);
366 		visitor.processItem;
367 		m.visitForward(d, visitor);
368 
369 		visitor.output ~= '\0';
370 
371 		version(none)
372 		{
373 			import core.stdc.stdio : printf;
374 			printf("---\n%s\n---\nlength: %ld\n",
375 				visitor.output[].ptr, visitor.output.length);
376 		}
377 
378 		import std.algorithm : equal;
379 		assert(visitor.output[].equal("
380 nan
381 \0"
382 		));
383 	}
384 
385 	{
386 		Test4 test4;
387 		test4.i = 112;
388 		auto m = makeModel(test4);
389 
390 		static assert(is(m.Proxy));
391 		static assert(is(typeof(m.proxy) == int));
392 		assert(m.proxy == 112);
393 	}
394 
395 	{
396 		auto d = Test5(Test4(11, 22.2));
397 		auto m = makeModel(d);
398 		static assert(!m.Collapsable);
399 
400 		auto visitor = PrettyPrintingVisitor(9);
401 		visitor.processItem;
402 		m.visitForward(d, visitor);
403 
404 		visitor.output ~= '\0';
405 
406 		version(none)
407 		{
408 			import core.stdc.stdio : printf;
409 			printf("---\n%s\n---\nlength: %ld\n",
410 				visitor.output[].ptr, visitor.output.length);
411 		}
412 
413 		import std.algorithm : equal;
414 		assert(visitor.output[].equal("
415 11
416 \0"
417 		));
418 	}
419 
420 	{
421 		@("wrongAttribute")
422 		@("renderedAsMember.f", "123")
423 		@("123")
424 		static struct Test
425 		{
426 			int i;
427 			float f;
428 		}
429 		auto d = Test(11, 1.0);
430 		auto m = makeModel(d);
431 		static assert(!m.Collapsable);
432 
433 		auto visitor = PrettyPrintingVisitor(9);
434 		visitor.processItem;
435 		m.visitForward(d, visitor);
436 
437 		visitor.output ~= '\0';
438 
439 		version(none)
440 		{
441 			import core.stdc.stdio : printf;
442 			printf("---\n%s\n---\nlength: %ld\n",
443 				visitor.output[].ptr, visitor.output.length);
444 		}
445 
446 		import std.algorithm : equal;
447 		assert(visitor.output[].equal("
448 1.000000
449 \0"
450 		));
451 	}
452 }
453 
454 version(unittest) @Name("TaggedAlgebraic")
455 unittest
456 {
457 	import taggedalgebraic : TaggedAlgebraic, Void, get;
458 	import unit_threaded : should, be;
459 
460 	static struct Struct
461 	{
462 		double d;
463 		char c;
464 	}
465 
466 	struct Payload
467 	{
468 		Void v;
469 		float f;
470 		int i;
471 		string sg;
472 		Struct st;
473 		string[] sa;
474 		double[] da;
475 	}
476 
477 	alias Data = TaggedAlgebraic!Payload;
478 
479 	Data[] data = [
480 		Data(1.2f),
481 		Data(4),
482 		Data("string"),
483 		Data(Struct(100, 'd')),
484 		Data(["str0", "str1", "str2"]),
485 		Data([0.1, 0.2, 0.3]),
486 	];
487 
488 	auto model = makeModel(data);
489 	assert(model.length == data.length);
490 	assert(model[4].get!(Model!(string[])).length == data[4].length);
491 
492 	// default value
493 	model.collapsed.should.be == true;
494 
495 	// change it directly
496 	model.collapsed = false;
497 	model.collapsed.should.be == false;
498 
499 	// change the value of the root by tree path
500 	setPropertyByTreePath!"collapsed"(data, model, [], true);
501 	model.collapsed.should.be == true;
502 	{
503 		const r = getPropertyByTreePath!("collapsed", bool)(data, model, []);
504 		r.isNull.should.be == false;
505 		r.get.should.be == true;
506 	}
507 
508 	// change the root once again
509 	setPropertyByTreePath!"collapsed"(data, model, [], false);
510 	model.collapsed.should.be == false;
511 
512 	// the value of the child by tree path
513 	setPropertyByTreePath!"collapsed"(data, model, [3], false);
514 	model.model[3].collapsed.should.be == false;
515 
516 	foreach(ref e; model.model)
517 		e.collapsed = false;
518 
519 	auto visitor = PrettyPrintingVisitor(9);
520 	visitor.processItem;
521 	model.visitForward(data, visitor);
522 
523 	data[4] ~= "recently added 4th element";
524 	model[4].update(data[4]);
525 	model.visitForward(data, visitor);
526 
527 	data[4] = data[4].get!(string[])[3..$];
528 	data[4].get!(string[])[0] = "former 4th element, now the only one";
529 	model[4].update(data[4]);
530 	model.visitForward(data, visitor);
531 
532 	visitor.output ~= '\0';
533 	version(none)
534 	{
535 		import core.stdc.stdio : printf;
536 		printf("%s\nlength: %ld\n", visitor.output[].ptr, visitor.output.length);
537 	}
538 
539 	import std.algorithm : equal;
540 	assert(visitor.output[].equal("
541 Caption: TaggedAlgebraic!(Payload)[]
542 	1.200000
543 	4
544 	string
545 	Caption: Struct
546 		100.000000
547 		d
548 	Caption: string[]
549 		str0
550 		str1
551 		str2
552 	Caption: double[]
553 		0.100000
554 		0.200000
555 		0.300000
556 Caption: TaggedAlgebraic!(Payload)[]
557 	1.200000
558 	4
559 	string
560 	Caption: Struct
561 		100.000000
562 		d
563 	Caption: string[]
564 		str0
565 		str1
566 		str2
567 		recently added 4th element
568 	Caption: double[]
569 		0.100000
570 		0.200000
571 		0.300000
572 Caption: TaggedAlgebraic!(Payload)[]
573 	1.200000
574 	4
575 	string
576 	Caption: Struct
577 		100.000000
578 		d
579 	Caption: string[]
580 		former 4th element, now the only one
581 	Caption: double[]
582 		0.100000
583 		0.200000
584 		0.300000
585 \0"
586 	));
587 }
588 
589 version(unittest) @Name("nogc_dynamic_array")
590 @nogc
591 unittest
592 {
593 	import std.experimental.allocator.mallocator : Mallocator;
594 	import automem.vector : Vector;
595 	Vector!(float, Mallocator) data;
596 	data ~= 0.1f;
597 	data ~= 0.2f;
598 	data ~= 0.3f;
599 
600 	auto model = makeModel(data[]);
601 
602 	auto visitor = PrettyPrintingVisitor(14);
603 	visitor.processItem;
604 	model.visitForward(data[], visitor);
605 	assert(model.size == visitor.size + model.Spacing);
606 
607 	model.collapsed = false;
608 	model.visitForward(data[], visitor);
609 
610 	assert(model.size == 4*(visitor.size + model.Spacing));
611 	foreach(e; model.model)
612 		assert(e.size == (visitor.size + model.Spacing));
613 
614 	visitor.output ~= '\0';
615 	version(none)
616 	{
617 		import core.stdc.stdio : printf;
618 		printf("%s\nlength: %ld\n", visitor.output[].ptr, visitor.output.length);
619 	}
620 
621 	import std.algorithm : equal;
622 	assert(visitor.output[].equal("
623 Caption: float[]
624 Caption: float[]
625 	0.100000
626 	0.200000
627 	0.300000
628 \0"
629 	));
630 }
631 
632 version(unittest) @Name("size_measuring")
633 unittest
634 {
635 	import taggedalgebraic : TaggedAlgebraic, Void, get;
636 	import unit_threaded : should, be;
637 
638 	static struct Struct
639 	{
640 		double d;
641 		char c;
642 	}
643 
644 	struct Payload
645 	{
646 		Void v;
647 		float f;
648 		int i;
649 		string sg;
650 		Struct st;
651 		string[] sa;
652 		double[] da;
653 	}
654 
655 	alias Data = TaggedAlgebraic!Payload;
656 
657 	Data[] data = [
658 		Data(1.2f),
659 		Data(4),
660 		Data("string"),
661 		Data(Struct(100, 'd')),
662 		Data(["str0", "str1", "str2"]),
663 		Data([0.1, 0.2, 0.3]),
664 	];
665 
666 	auto model = makeModel(data);
667 	assert(model.length == data.length);
668 	assert(model[4].get!(Model!(string[])).length == data[4].length);
669 
670 	model.size.should.be == 0;
671 	auto visitor = PrettyPrintingVisitor(17);
672 	model.visitForward(data, visitor);
673 
674 	model.collapsed.should.be == true;
675 	model.size.should.be ~ (visitor.size + model.Spacing);
676 	model.size.should.be ~ 18.0;
677 	visitor.position.should.be ~ 0.0;
678 
679 	setPropertyByTreePath!"collapsed"(data, model, [], false);
680 	model.visitForward(data, visitor);
681 	model.size.should.be ~ (visitor.size + model.Spacing)*7;
682 	model.size.should.be ~ 18.0*7;
683 	visitor.position.should.be ~ 6*18.0;
684 
685 	setPropertyByTreePath!"collapsed"(data, model, [3], false);
686 	model.visitForward(data, visitor);
687 	model.size.should.be ~ (visitor.size + model.Spacing)*9;
688 	model.size.should.be ~ 18.0*9;
689 	visitor.position.should.be ~ (6+2)*18.0;
690 
691 	setPropertyByTreePath!"collapsed"(data, model, [4], false);
692 	model.visitForward(data, visitor);
693 	model.size.should.be ~ (visitor.size + model.Spacing)*12;
694 	model.size.should.be ~ 18.0*12;
695 	visitor.position.should.be ~ (6+2+3)*18.0;
696 
697 	setPropertyByTreePath!"collapsed"(data, model, [5], false);
698 	model.visitForward(data, visitor);
699 	model.size.should.be ~ (visitor.size + model.Spacing)*15;
700 	model.size.should.be ~ 18.0*15;
701 	visitor.position.should.be ~ (6+2+3+3)*18.0;
702 
703 	visitor.destination = visitor.destination.nan;
704 	model.visitForward(data, visitor);
705 	model.size.should.be == 270;
706 	visitor.position.should.be == 252;
707 
708 	visitor.position = 0;
709 	visitor.destination = 100;
710 	model.visitForward(data, visitor);
711 	model.size.should.be == 126;
712 	visitor.position.should.be == 90;
713 }
714 
715 struct RelativeMeasurer
716 {
717 	DefaultVisitorImpl!(SizeEnabled.no, TreePathEnabled.yes) default_visitor;
718 	alias default_visitor this;
719 
720 	TreePosition[] output;
721 
722 	void enterTree(Order order, Data, Model)(ref const(Data) data, ref Model model)
723 	{
724 		output = null;
725 	}
726 
727 	void enterNode(Order order, Data, Model)(ref const(Data) data, ref Model model)
728 	{
729 		static if (order == Order.Sinking)
730 			output ~= TreePosition(tree_path.value, position);
731 	}
732 
733 	void leaveNode(Order order, Data, Model)(ref const(Data) data, ref Model model)
734 	{
735 		static if (order == Order.Bubbling)
736 			output ~= TreePosition(tree_path.value, position);
737 	}
738 
739 	void processLeaf(Order order, Data, Model)(ref const(Data) data, ref Model model)
740 	{
741 		output ~= TreePosition(tree_path.value, position);
742 	}
743 }
744 
745 struct TreePosition
746 {
747 	import std.experimental.allocator.mallocator : Mallocator;
748 	import automem : Vector;
749 	Vector!(int, Mallocator) path;
750 	double size;
751 
752 	@disable this();
753 
754 	this(P, S)(P p, S s)
755 		if (!is(P == void[]))
756 	{
757 		path = p;
758 		size = s;
759 	}
760 
761 	this(S)(void[] p, S s)
762 	{
763 		size = s;
764 	}
765 
766 	import std.range : isOutputRange;
767 	import std.format : FormatSpec;
768 
769 	void toString(Writer) (ref Writer w, scope const ref FormatSpec!char fmt) const
770 		if (isOutputRange!(Writer, char))
771 	{
772 		import std.algorithm : copy;
773 		import std.conv : text;
774 
775 		copy(typeof(this).stringof, w);
776 		w.put('(');
777 		copy(text(path[], ", ", size), w);
778 		w.put(')');
779 	}
780 }
781 
782 version(unittest)
783 {
784 	import unit_threaded;
785 
786 	class Fixture : TestCase
787 	{
788 		import unit_threaded : should, be;
789 		import taggedalgebraic : TaggedAlgebraic;
790 
791 		override void setup()
792 		{
793 			data = [
794 				Data(1),
795 				Data(2.0),
796 				Data(3.0f),
797 				Data(Test(100, -1001)),
798 				Data(Test2(1_000_000, "test2", Test(200, -11), [11, 12, 123])),
799 				Data("text"),
800 			];
801 
802 			v = RelativeMeasurer();
803 			model = makeModel(data);
804 			model.collapsed = false;
805 			setPropertyByTreePath!"collapsed"(data, model, [3], false);
806 			setPropertyByTreePath!"collapsed"(data, model, [4], false);
807 			setPropertyByTreePath!"collapsed"(data, model, [4, 2], false);
808 			setPropertyByTreePath!"collapsed"(data, model, [4, 3], false);
809 
810 			// measure size
811 			{
812 				auto mv = MeasuringVisitor(9);
813 				model.visitForward(data, mv);
814 			}
815 		}
816 
817 	package:
818 
819 		static struct Test
820 		{
821 			ushort us;
822 			long l;
823 		}
824 
825 		static struct Test2
826 		{
827 			size_t st;
828 			string s;
829 			Test t;
830 			float[] fa;
831 		}
832 
833 		static union Payload
834 		{
835 			int i;
836 			float f;
837 			double d;
838 			string str;
839 			Test t;
840 			Test2 t2;
841 		}
842 
843 		alias Data = TaggedAlgebraic!Payload;
844 
845 		Data[] data;
846 		RelativeMeasurer v;
847 		typeof(makeModel(data)) model;
848 	}
849 
850 	class Test1 : Fixture
851 	{
852 		override void test()
853 		{
854 			v.position = 0;
855 			model.visitForward(data, v);
856 			model.size.should.be == 180;
857 			v.output.should.be == [
858 				TreePosition([ ],         0),
859 				TreePosition([0],        10),
860 				TreePosition([1],        20),
861 				TreePosition([2],        30),
862 				TreePosition([3],        40),
863 				TreePosition([3, 0],     50),
864 				TreePosition([3, 1],     60),
865 				TreePosition([4],        70),
866 				TreePosition([4, 0],     80),
867 				TreePosition([4, 1],     90),
868 				TreePosition([4, 2],    100),
869 				TreePosition([4, 2, 0], 110),
870 				TreePosition([4, 2, 1], 120),
871 				TreePosition([4, 3],    130),
872 				TreePosition([4, 3, 0], 140),
873 				TreePosition([4, 3, 1], 150),
874 				TreePosition([4, 3, 2], 160),
875 				TreePosition([5],       170),
876 			];
877 			v.position.should.be == 170;
878 
879 			v.position = 0;
880 			v.path.value = [4,2,1];
881 			model.visitForward(data, v);
882 			v.output.should.be == [
883 				TreePosition([4, 2, 1],  0),
884 				TreePosition([4, 3],    10),
885 				TreePosition([4, 3, 0], 20),
886 				TreePosition([4, 3, 1], 30),
887 				TreePosition([4, 3, 2], 40),
888 				TreePosition([5],       50)
889 			];
890 		}
891 	}
892 
893 	class Test2 : Fixture
894 	{
895 		override void test()
896 		{
897 			// default
898 			{
899 				v.path.clear;
900 				v.position = 0;
901 				v.destination = v.destination.nan;
902 				model.visitForward(data, v);
903 
904 				v.position.should.be == 170;
905 				v.path.value[].should.be == [];
906 			}
907 
908 			// next position is between two elements
909 			{
910 				v.path.clear;
911 				v.position = 0;
912 				v.destination = 15;
913 				model.visitForward(data, v);
914 
915 				v.position.should.be == 10;
916 				v.destination.should.be == 15;
917 				v.path.value[].should.be == [0];
918 			}
919 
920 			// next position is equal to start of an element
921 			{
922 				v.path.clear;
923 				v.position = 0;
924 				v.destination = 30;
925 				model.visitForward(data, v);
926 
927 				v.position.should.be == 30;
928 				v.destination.should.be == 30;
929 				v.path.value[].should.be == [2];
930 			}
931 
932 			// start path is not null
933 			{
934 				v.path.value = [3, 0];
935 				v.position = 0;
936 				v.destination = 55;
937 				model.visitForward(data, v);
938 
939 				v.position.should.be == 50;
940 				v.destination.should.be == 55;
941 				v.path.value[].should.be == [4, 2];
942 			}
943 
944 			// reverse order, start path is not null
945 			{
946 				v.path.value = [4, 1];
947 				v.position = 90;
948 				v.destination = 41;
949 
950 				model.visitBackward(data, v);
951 
952 				v.position.should.be == 40;
953 				v.destination.should.be == 41;
954 				v.path.value[].should.be == [3];
955 
956 				// bubble to the next element
957 				v.destination = 19;
958 
959 				model.visitBackward(data, v);
960 
961 				v.path.value[].should.be == [0];
962 				v.position.should.be == 10;
963 				v.destination.should.be == 19;
964 				v.output.should.be == [
965 					TreePosition([3], 40),
966 					TreePosition([2], 30),
967 					TreePosition([1], 20),
968 					TreePosition([0], 10),
969 				];
970 			}
971 		}
972 	}
973 
974 	class ScrollingTest : Fixture
975 	{
976 		override void test()
977 		{
978 			v.path.clear;
979 			v.position = 0;
980 
981 			// the element height is 10 px
982 
983 			// scroll 7 px forward
984 			visit(model, data, v, 7);
985 			// current element is the root one
986 			v.path.value[].should.be == [];
987 			// position of the current element is 0 px
988 			v.position.should.be == 0;
989 			// the window starts from 7th px
990 			v.destination.should.be == 7;
991 
992 			// scroll the next 7th px forward
993 			visit(model, data, v, 14);
994 			// the current element is the first child element
995 			v.path.value[].should.be == [0];
996 			// position of the current element is 10 px
997 			v.position.should.be == 10;
998 			// the window starts from 14th px
999 			v.destination.should.be == 14;
1000 
1001 			// scroll the next 7th px forward
1002 			visit(model, data, v, 21);
1003 			// the current element is the second child element
1004 			v.path.value[].should.be == [1];
1005 			// position of the current element is 20 px
1006 			v.position.should.be == 20;
1007 			// the window starts from 21th px
1008 			v.destination.should.be == 21;
1009 
1010 			// scroll the next 7th px forward
1011 			visit(model, data, v, 28);
1012 			// the current element is the second child element
1013 			v.path.value[].should.be == [1];
1014 			// position of the current element is 20 px
1015 			v.position.should.be == 20;
1016 			// the window starts from 28th px
1017 			v.destination.should.be == 28;
1018 
1019 			// scroll the next 7th px forward
1020 			visit(model, data, v, 35);
1021 			// the current element is the third child element
1022 			v.path.value[].should.be == [2];
1023 			// position of the current element is 30 px
1024 			v.position.should.be == 30;
1025 			// the window starts from 35th px
1026 			v.destination.should.be == 35;
1027 
1028 			// scroll 7th px backward
1029 			visit(model, data, v, 27);
1030 			// the current element is the second child element
1031 			v.path.value[].should.be == [1];
1032 			// position of the current element is 20 px
1033 			v.position.should.be == 20;
1034 			// the window starts from 27th px
1035 			v.destination.should.be == 27;
1036 
1037 			// scroll the next 9th px backward
1038 			visit(model, data, v, 18);
1039 			// the current element is the first child element
1040 			v.path.value[].should.be == [0];
1041 			// position of the current element is 10 px
1042 			v.position.should.be == 10;
1043 			// the window starts from 18th px
1044 			v.destination.should.be == 18;
1045 
1046 			// scroll the next 6th px backward
1047 			visit(model, data, v, 12);
1048 			// the current element is the first child element
1049 			v.path.value[].should.be == [0];
1050 			// position of the current element is 10 px
1051 			v.position.should.be == 10;
1052 			// the window starts from 12th px
1053 			v.destination.should.be == 12;
1054 
1055 			// scroll the next 5th px backward
1056 			visit(model, data, v, 7);
1057 			// the current element is the root element
1058 			v.path.value[].should.be == [];
1059 			// position of the current element is 0 px
1060 			v.position.should.be == 0;
1061 			// the window starts from 7th px
1062 			v.destination.should.be == 7;
1063 
1064 			// scroll 76 px forward
1065 			visit(model, data, v, 83);
1066 			// // the current element is the second child element
1067 			// v.path.value[].should.be == [4, 0];
1068 			// // position of the current element is 20 px
1069 			// v.position.should.be == 80;
1070 			// the window starts from 27th px
1071 			v.destination.should.be == 83;
1072 
1073 			visit(model, data, v, 81);
1074 			v.path.value[].should.be == [4, 0];
1075 			v.position.should.be == 80;
1076 			v.destination.should.be == 81;
1077 
1078 			visit(model, data, v, 80);
1079 			v.path.value[].should.be == [4, 0];
1080 			v.position.should.be == 80;
1081 			v.destination.should.be == 80;
1082 
1083 			visit(model, data, v, 79.1);
1084 			v.path.value[].should.be == [4];
1085 			v.position.should.be == 70;
1086 			v.destination.should.be ~ 79.1;
1087 
1088 			visit(model, data, v, 133.4);
1089 			v.path.value[].should.be == [4, 3];
1090 			v.position.should.be == 130;
1091 			v.destination.should.be ~ 133.4;
1092 
1093 			visit(model, data, v, 0);
1094 			v.path.value[].should.be == [];
1095 			v.position.should.be == 0;
1096 			v.destination.should.be ~ 0.0;
1097 		}
1098 	}
1099 }
1100 
1101 version(unittest) @Name("reverse_dynamic_array")
1102 unittest
1103 {
1104 	import unit_threaded : should, be;
1105 
1106 	auto data = [0, 1, 2, 3];
1107 	auto model = makeModel(data);
1108 	auto visitor = RelativeMeasurer();
1109 
1110 	model.collapsed = false;
1111 	{
1112 		auto mv = MeasuringVisitor(9);
1113 		model.visitForward(data, mv);
1114 	}
1115 	visitor.position = 0;
1116 	model.visitForward(data, visitor);
1117 	visitor.output.should.be == [
1118 		TreePosition([ ],  0),
1119 		TreePosition([0], 10),
1120 		TreePosition([1], 20),
1121 		TreePosition([2], 30),
1122 		TreePosition([3], 40),
1123 	];
1124 
1125 	visitor.position.should.be == 40;
1126 
1127 	model.visitBackward(data, visitor);
1128 	visitor.output.should.be == [
1129 		TreePosition([3], 40),
1130 		TreePosition([2], 30),
1131 		TreePosition([1], 20),
1132 		TreePosition([0], 10),
1133 		TreePosition([ ],  0),
1134 	];
1135 
1136 	visitor.path.value = [1,];
1137 	visitor.position = 20;
1138 	model.visitForward(data, visitor);
1139 	visitor.output.should.be == [
1140 		TreePosition([1], 20),
1141 		TreePosition([2], 30),
1142 		TreePosition([3], 40),
1143 	];
1144 	visitor.position = 20;
1145 	model.visitBackward(data, visitor);
1146 	visitor.output.should.be == [
1147 		TreePosition([1], 20),
1148 		TreePosition([0], 10),
1149 		TreePosition([ ],  0),
1150 	];
1151 }
1152 
1153 version(unittest) @Name("aggregate")
1154 unittest
1155 {
1156 	import unit_threaded : should, be;
1157 
1158 	static struct NestedData1
1159 	{
1160 		long l;
1161 		char ch;
1162 	}
1163 
1164 	static struct NestedData2
1165 	{
1166 		short sh;
1167 		NestedData1 data1;
1168 		string str;
1169 	}
1170 
1171 	static struct Data
1172 	{
1173 		int i;
1174 		float f;
1175 		double d;
1176 		string s;
1177 		NestedData2 data2;
1178 	}
1179 
1180 	const data = Data(0, 1, 2, "3", NestedData2(ushort(2), NestedData1(1_000_000_000, 'z'), "text"));
1181 	auto model = makeModel(data);
1182 	auto visitor = RelativeMeasurer();
1183 
1184 	model.collapsed = false;
1185 	{
1186 		auto mv = MeasuringVisitor(9);
1187 		model.visitForward(data, mv);
1188 	}
1189 	visitor.position = 0;
1190 	model.visitForward(data, visitor);
1191 	visitor.output.should.be == [
1192 		TreePosition([], 0),
1193 		TreePosition([0], 10),
1194 		TreePosition([1], 20),
1195 		TreePosition([2], 30),
1196 		TreePosition([3], 40),
1197 		TreePosition([4], 50),
1198 	];
1199 	visitor.position.should.be == 50;
1200 
1201 	{
1202 		visitor.path.clear;
1203 		visitor.position = 0;
1204 		visitor.destination = 30;
1205 		model.visitForward(data, visitor);
1206 		visitor.output.should.be == [
1207 			TreePosition([], 0),
1208 			TreePosition([0], 10),
1209 			TreePosition([1], 20),
1210 			TreePosition([2], 30),
1211 		];
1212 		visitor.position.should.be == 30;
1213 	}
1214 
1215 	{
1216 		visitor.path.clear;
1217 		visitor.position = 30;
1218 		visitor.destination = visitor.position + 30;
1219 		model.visitForward(data, visitor);
1220 		visitor.output.should.be == [
1221 			TreePosition([], 30),
1222 			TreePosition([0], 40),
1223 			TreePosition([1], 50),
1224 			TreePosition([2], 60),
1225 		];
1226 		visitor.position.should.be == 60;
1227 	}
1228 
1229 	{
1230 		visitor.path.value = [0];
1231 		visitor.position = 130;
1232 		visitor.destination = visitor.position + 20;
1233 		model.visitForward(data, visitor);
1234 		visitor.output.should.be == [
1235 			TreePosition([0], 130),
1236 			TreePosition([1], 140),
1237 			TreePosition([2], 150),
1238 		];
1239 		visitor.position.should.be == 150;
1240 	}
1241 
1242 	visitor.path.value = [2];
1243 	visitor.position = 30;
1244 	visitor.destination = visitor.destination.nan;
1245 	model.visitForward(data, visitor);
1246 
1247 	visitor.output.should.be == [
1248 		TreePosition([2], 30),
1249 		TreePosition([3], 40),
1250 		TreePosition([4], 50),
1251 	];
1252 
1253 	visitor.path.clear;
1254 	visitor.destination = visitor.destination.nan;
1255 	model.visitBackward(data, visitor);
1256 	visitor.output.should.be == [
1257 		TreePosition([4], 50),
1258 		TreePosition([3], 40),
1259 		TreePosition([2], 30),
1260 		TreePosition([1], 20),
1261 		TreePosition([0], 10),
1262 		TreePosition([],   0),
1263 	];
1264 }
1265 
1266 version(unittest) @Name("new_paradigm")
1267 unittest
1268 {
1269 	static struct TrivialStruct
1270 	{
1271 		int i;
1272 		float f;
1273 	}
1274 
1275 	static struct StructNullable
1276 	{
1277 		import std.typecons : Nullable;
1278 		int i;
1279 		Nullable!float f;
1280 	}
1281 
1282 	auto d = StructNullable();
1283 	auto m = makeModel(d);
1284 	m.collapsed = false;
1285 	auto visitor = DefaultVisitor(19);
1286 	m.visitForward(d, visitor);
1287 	import std;
1288 	writeln(m);
1289 }