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