1 module auxil.test;
2 
3 version(unittest) import unit_threaded : Name, should, be;
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(SizeType 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 	m.size.should.be == 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 unittest
123 {
124 	version(Windows) {}
125 	else {
126 		import core.stdc.locale;
127 		setlocale(LC_NUMERIC, "C");
128 	}
129 	float[3] d = [1.1f, 2.2f, 3.3f];
130 	auto m = Model!d();
131 
132 	auto visitor = PrettyPrintingVisitor(9);
133 	visitor.destination = 100;
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 	visitor.output[].should.be == "
147 Caption: float[3]
148 	1.100000
149 	2.200000
150 	3.300000
151 \0";
152 }
153 
154 version(unittest) @Name("dynamic_array")
155 unittest
156 {
157 	auto visitor = PrettyPrintingVisitor(9);
158 
159 	float[] d = [1.1f, 2.2f, 3.3f];
160 	auto m = Model!d();
161 
162 	visitor.processItem;
163 	visitor.destination = visitor.destination.max;
164 	m.collapsed = false;
165 	m.model.length = d.length;
166 	m.visitForward(d, visitor);
167 
168 	d ~= [4.4f, 5.5f];
169 	m.model.length = d.length;
170 	m.visitForward(d, visitor);
171 
172 	d = d[2..3];
173 	m.model.length = d.length;
174 	m.visitForward(d, visitor);
175 
176 	visitor.output ~= '\0';
177 	version(none)
178 	{
179 		import core.stdc.stdio : printf;
180 		printf("%s\nlength: %ld\n", visitor.output[].ptr, visitor.output.length);
181 	}
182 
183 	import std.algorithm : equal;
184 	visitor.output[].should.be == "
185 Caption: float[]
186 	1.100000
187 	2.200000
188 	3.300000
189 Caption: float[]
190 	1.100000
191 	2.200000
192 	3.300000
193 	4.400000
194 	5.500000
195 Caption: float[]
196 	3.300000
197 \0";
198 }
199 
200 version(none)
201 version(unittest) @Name("aggregate_with_only_member")
202 @nogc unittest
203 {
204 	static struct OneMember
205 	{
206 		string one = "one";
207 	}
208 
209 	auto d = OneMember();
210 	auto m = Model!d();
211 	import std.traits : FieldNameTuple;
212 	import std.meta : AliasSeq;
213 	static assert(FieldNameTuple!(typeof(m)) == AliasSeq!("single_member_model"));
214 
215 	auto visitor = PrettyPrintingVisitor(9);
216 	visitor.processItem;
217 	m.visitForward(d, visitor);
218 
219 	visitor.output ~= '\0';
220 
221 	version(none)
222 	{
223 		import core.stdc.stdio : printf;
224 		printf("---\n%s\n---\nlength: %ld\n",
225 			visitor.output[].ptr, visitor.output.length);
226 	}
227 
228 	import std.algorithm : equal;
229 	visitor.output[].should.be == "
230 one
231 \0";
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, %lld", 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.destination = visitor.destination.max;
263 	visitor.processItem;
264 	m.visitForward(d, visitor);
265 
266 	visitor.output ~= '\0';
267 
268 	version(none)
269 	{
270 		import core.stdc.stdio : printf;
271 		printf("---\n%s\n---\nlength: %ld\n",
272 			visitor.output[].ptr, visitor.output.length);
273 	}
274 
275 	visitor.output[].should.be == "
276 Custom header nan, 0
277 	nan
278 	0
279 \0";
280 }
281 
282 version(unittest)
283 {
284 	import auxil.traits : renderedAs, renderedAsMember;
285 
286 	private struct Proxy
287 	{
288 		float f;
289 
290 		this(ref const(Test3) t3)
291 		{
292 			f = t3.f;
293 		}
294 	}
295 
296 	@renderedAs!Proxy
297 	private struct Test3
298 	{
299 		int i;
300 		float f;
301 	}
302 
303 	@renderedAs!int
304 	private struct Test4
305 	{
306 		int i;
307 		float f;
308 
309 		int opCast(T : int)() const
310 		{
311 			return i;
312 		}
313 	}
314 
315 	@renderedAsMember!"t4.i"
316 	private struct Test5
317 	{
318 		Test4 t4;
319 		float f;
320 	}
321 }
322 
323 version(none)
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 		visitor.output[].should.be == "
354 123.000000
355 \0";
356 	}
357 
358 	{
359 		auto d = Test3();
360 		auto m = makeModel(d);
361 		static assert(!m.Collapsable);
362 
363 		auto visitor = PrettyPrintingVisitor(9);
364 		visitor.processItem;
365 		m.visitForward(d, visitor);
366 
367 		visitor.output ~= '\0';
368 
369 		version(none)
370 		{
371 			import core.stdc.stdio : printf;
372 			printf("---\n%s\n---\nlength: %ld\n",
373 				visitor.output[].ptr, visitor.output.length);
374 		}
375 
376 		import std.algorithm : equal;
377 		visitor.output[].should.be == "
378 nan
379 \0";
380 	}
381 
382 	{
383 		Test4 test4;
384 		test4.i = 112;
385 		auto m = makeModel(test4);
386 
387 		static assert(is(m.Proxy));
388 		static assert(is(typeof(m.proxy) == int));
389 		m.proxy.should.be == 112;
390 	}
391 
392 	{
393 		auto d = Test5(Test4(11, 22.2));
394 		auto m = makeModel(d);
395 		static assert(!m.Collapsable);
396 
397 		auto visitor = PrettyPrintingVisitor(9);
398 		visitor.processItem;
399 		m.visitForward(d, visitor);
400 
401 		visitor.output ~= '\0';
402 
403 		version(none)
404 		{
405 			import core.stdc.stdio : printf;
406 			printf("---\n%s\n---\nlength: %ld\n",
407 				visitor.output[].ptr, visitor.output.length);
408 		}
409 
410 		import std.algorithm : equal;
411 		visitor.output[].should.be == "
412 11
413 \0";
414 	}
415 
416 	{
417 		@("wrongAttribute")
418 		@("renderedAsMember.f", "123")
419 		@("123")
420 		static struct Test
421 		{
422 			int i;
423 			float f;
424 		}
425 		auto d = Test(11, 1.0);
426 		auto m = makeModel(d);
427 		static assert(!m.Collapsable);
428 
429 		auto visitor = PrettyPrintingVisitor(9);
430 		visitor.processItem;
431 		m.visitForward(d, visitor);
432 
433 		visitor.output ~= '\0';
434 
435 		version(none)
436 		{
437 			import core.stdc.stdio : printf;
438 			printf("---\n%s\n---\nlength: %ld\n",
439 				visitor.output[].ptr, visitor.output.length);
440 		}
441 
442 		visitor.output[].should.be == "
443 1.000000
444 \0";
445 	}
446 }
447 
448 version(unittest) @Name("TaggedAlgebraic")
449 unittest
450 {
451 	import taggedalgebraic : TaggedAlgebraic, Void, get;
452 	import unit_threaded : should, be;
453 
454 	static struct Struct
455 	{
456 		double d;
457 		char c;
458 	}
459 
460 	struct Payload
461 	{
462 		Void v;
463 		float f;
464 		int i;
465 		string sg;
466 		Struct st;
467 		string[] sa;
468 		double[] da;
469 	}
470 
471 	alias Data = TaggedAlgebraic!Payload;
472 
473 	Data[] data = [
474 		Data(1.2f),
475 		Data(4),
476 		Data("string"),
477 		Data(Struct(100, 'd')),
478 		Data(["str0", "str1", "str2"]),
479 		Data([0.1, 0.2, 0.3]),
480 	];
481 
482 	auto model = makeModel(data);
483 	model.length.should.be == data.length;
484 	model[4].get!(Model!(string[])).length.should.be == data[4].length;
485 
486 	// default value
487 	model.collapsed.should.be == true;
488 
489 	// change it directly
490 	model.collapsed = false;
491 	model.collapsed.should.be == false;
492 
493 	// change the value of the root by tree path
494 	setPropertyByTreePath!"collapsed"(data, model, [], true);
495 	model.collapsed.should.be == true;
496 	{
497 		const r = getPropertyByTreePath!("collapsed", bool)(data, model, []);
498 		r.isNull.should.be == false;
499 		r.get.should.be == true;
500 	}
501 
502 	// change the root once again
503 	setPropertyByTreePath!"collapsed"(data, model, [], false);
504 	model.collapsed.should.be == false;
505 
506 	// the value of the child by tree path
507 	setPropertyByTreePath!"collapsed"(data, model, [3], false);
508 	model.model[3].collapsed.should.be == false;
509 
510 	foreach(ref e; model.model)
511 		e.collapsed = false;
512 
513 	auto visitor = PrettyPrintingVisitor(9);
514 	visitor.processItem;
515 	visitor.destination = visitor.destination.max;
516 	model.visitForward(data, visitor);
517 
518 	data[4] ~= "recently added 4th element";
519 	model[4].update(data[4]);
520 	model.visitForward(data, visitor);
521 
522 	data[4] = data[4].get!(string[])[3..$];
523 	data[4].get!(string[])[0] = "former 4th element, now the only one";
524 	model[4].update(data[4]);
525 	model.visitForward(data, visitor);
526 
527 	visitor.output ~= '\0';
528 	version(none)
529 	{
530 		import core.stdc.stdio : printf;
531 		printf("%s\nlength: %ld\n", visitor.output[].ptr, visitor.output.length);
532 	}
533 
534 	import std.algorithm : equal;
535 	visitor.output[].should.be == "
536 Caption: TaggedAlgebraic!(Payload)[]
537 	1.200000
538 	4
539 	string
540 	Caption: Struct
541 		100.000000
542 		d
543 	Caption: string[]
544 		str0
545 		str1
546 		str2
547 	Caption: double[]
548 		0.100000
549 		0.200000
550 		0.300000
551 Caption: TaggedAlgebraic!(Payload)[]
552 	1.200000
553 	4
554 	string
555 	Caption: Struct
556 		100.000000
557 		d
558 	Caption: string[]
559 		str0
560 		str1
561 		str2
562 		recently added 4th element
563 	Caption: double[]
564 		0.100000
565 		0.200000
566 		0.300000
567 Caption: TaggedAlgebraic!(Payload)[]
568 	1.200000
569 	4
570 	string
571 	Caption: Struct
572 		100.000000
573 		d
574 	Caption: string[]
575 		former 4th element, now the only one
576 	Caption: double[]
577 		0.100000
578 		0.200000
579 		0.300000
580 \0";
581 }
582 
583 version(unittest) @Name("nogc_dynamic_array")
584 unittest
585 {
586 	auto visitor = PrettyPrintingVisitor(14);
587 	() {
588 		import std.experimental.allocator.mallocator : Mallocator;
589 		import automem.vector : Vector;
590 		Vector!(float, Mallocator) data;
591 		data ~= 0.1f;
592 		data ~= 0.2f;
593 		data ~= 0.3f;
594 
595 		auto model = makeModel(data[]);
596 
597 		visitor.destination = visitor.destination.max;
598 		visitor.processItem;
599 		model.visitForward(data[], visitor);
600 		model.size.should.be == visitor.size + model.Spacing;
601 
602 		model.collapsed = false;
603 		model.visitForward(data[], visitor);
604 
605 		model.size.should.be == 4*(visitor.size + model.Spacing);
606 		foreach(e; model.model)
607 			e.size.should.be == (visitor.size + model.Spacing);
608 
609 		visitor.output ~= '\0';
610 		version(none)
611 		{
612 			import core.stdc.stdio : printf;
613 			printf("%s\nlength: %ld\n", visitor.output[].ptr, visitor.output.length);
614 		}
615 	} ();
616 
617 	visitor.output[].should.be == "
618 Caption: float[]
619 Caption: float[]
620 	0.100000
621 	0.200000
622 	0.300000
623 \0";
624 }
625 
626 version(unittest) @Name("size_measuring")
627 unittest
628 {
629 	import taggedalgebraic : TaggedAlgebraic, Void, get;
630 	import unit_threaded : should, be;
631 
632 	static struct Struct
633 	{
634 		double d;
635 		char c;
636 	}
637 
638 	struct Payload
639 	{
640 		Void v;
641 		float f;
642 		int i;
643 		string sg;
644 		Struct st;
645 		string[] sa;
646 		double[] da;
647 	}
648 
649 	alias Data = TaggedAlgebraic!Payload;
650 
651 	Data[] data = [
652 		Data(1.2f),
653 		Data(4),
654 		Data("string"),
655 		Data(Struct(100, 'd')),
656 		Data(["str0", "str1", "str2"]),
657 		Data([0.1, 0.2, 0.3]),
658 	];
659 
660 	auto model = makeModel(data);
661 	model.length.should.be == data.length;
662 	model[4].get!(Model!(string[])).length.should.be == data[4].length;
663 
664 	model.size.should.be == 0;
665 	auto visitor = PrettyPrintingVisitor(17);
666 	model.visitForward(data, visitor);
667 
668 	model.collapsed.should.be == true;
669 	model.size.should.be == (visitor.size + model.Spacing);
670 	model.size.should.be == 18;
671 	visitor.position.should.be == 0;
672 
673 	setPropertyByTreePath!"collapsed"(data, model, [], false);
674 	visitor.destination = visitor.destination.max;
675 	model.visitForward(data, visitor);
676 	model.size.should.be == (visitor.size + model.Spacing)*7;
677 	model.size.should.be == 18*7;
678 	visitor.position.should.be == 6*18;
679 
680 	setPropertyByTreePath!"collapsed"(data, model, [3], false);
681 	visitor.destination = visitor.destination.max;
682 	model.visitForward(data, visitor);
683 	model.size.should.be == (visitor.size + model.Spacing)*9;
684 	model.size.should.be == 18*9;
685 	visitor.position.should.be == (6+2)*18;
686 
687 	setPropertyByTreePath!"collapsed"(data, model, [4], false);
688 	visitor.destination = visitor.destination.max;
689 	model.visitForward(data, visitor);
690 	model.size.should.be == (visitor.size + model.Spacing)*12;
691 	model.size.should.be == 18*12;
692 	visitor.position.should.be == (6+2+3)*18;
693 
694 	setPropertyByTreePath!"collapsed"(data, model, [5], false);
695 	visitor.destination = visitor.destination.max;
696 	model.visitForward(data, visitor);
697 	model.size.should.be == (visitor.size + model.Spacing)*15;
698 	model.size.should.be == 18*15;
699 	visitor.position.should.be == (6+2+3+3)*18;
700 
701 	visitor.destination = visitor.destination.max;
702 	model.visitForward(data, visitor);
703 	model.size.should.be == 270;
704 	visitor.position.should.be == 252;
705 
706 	visitor.position = 0;
707 	visitor.destination = 100;
708 	model.visitForward(data, visitor);
709 	model.size.should.be == 126;
710 	visitor.position.should.be == 90;
711 }
712 
713 struct RelativeMeasurer
714 {
715 	DefaultVisitorImpl!(SizeEnabled.no, TreePathEnabled.yes) default_visitor;
716 	alias default_visitor this;
717 
718 	TreePosition[] output;
719 
720 	void enterTree(Order order, Data, Model)(ref const(Data) data, ref Model model)
721 	{
722 		output = null;
723 	}
724 
725 	void enterNode(Order order, Data, Model)(ref const(Data) data, ref Model model)
726 	{
727 		static if (order == Order.Sinking)
728 			output ~= TreePosition(tree_path.value, position);
729 	}
730 
731 	void leaveNode(Order order, Data, Model)(ref const(Data) data, ref Model model)
732 	{
733 		static if (order == Order.Bubbling)
734 			output ~= TreePosition(tree_path.value, position);
735 	}
736 
737 	void processLeaf(Order order, Data, Model)(ref const(Data) data, ref Model model)
738 	{
739 		output ~= TreePosition(tree_path.value, position);
740 	}
741 }
742 
743 struct TreePosition
744 {
745 	import std.experimental.allocator.mallocator : Mallocator;
746 	import automem : Vector;
747 	Vector!(int, Mallocator) path;
748 	double size;
749 
750 	@disable this();
751 
752 	this(P, S)(P p, S s)
753 		if (!is(P == void[]))
754 	{
755 		path = p;
756 		size = s;
757 	}
758 
759 	this(S)(void[] p, S s)
760 	{
761 		size = s;
762 	}
763 
764 	import std.range : isOutputRange;
765 	import std.format : FormatSpec;
766 
767 	void toString(Writer) (ref Writer w, scope const ref FormatSpec!char fmt) const
768 		if (isOutputRange!(Writer, char))
769 	{
770 		import std.algorithm : copy;
771 		import std.conv : text;
772 
773 		copy(typeof(this).stringof, w);
774 		w.put('(');
775 		copy(text(path[], ", ", size), w);
776 		w.put(')');
777 	}
778 }
779 
780 version(unittest) @Name("reverse_dynamic_array")
781 unittest
782 {
783 	import unit_threaded : should, be;
784 
785 	auto data = [0, 1, 2, 3];
786 	auto model = makeModel(data);
787 	auto visitor = RelativeMeasurer();
788 
789 	model.collapsed = false;
790 	{
791 		auto mv = MeasuringVisitor(9);
792 		model.visitForward(data, mv);
793 	}
794 	visitor.position = 0;
795 	visitor.destination = visitor.destination.max;
796 	model.visitForward(data, visitor);
797 	visitor.output.should.be == [
798 		TreePosition([ ],  0),
799 		TreePosition([0], 10),
800 		TreePosition([1], 20),
801 		TreePosition([2], 30),
802 		TreePosition([3], 40),
803 	];
804 
805 	visitor.position.should.be == 40;
806 
807 	visitor.destination = -visitor.destination.max;
808 	model.visitBackward(data, visitor);
809 	visitor.output.should.be == [
810 		TreePosition([3], 40),
811 		TreePosition([2], 30),
812 		TreePosition([1], 20),
813 		TreePosition([0], 10),
814 		TreePosition([ ],  0),
815 	];
816 
817 	visitor.path.value = [1,];
818 	visitor.position = 20;
819 	visitor.destination = visitor.destination.max;
820 	model.visitForward(data, visitor);
821 	visitor.output.should.be == [
822 		TreePosition([1], 20),
823 		TreePosition([2], 30),
824 		TreePosition([3], 40),
825 	];
826 	visitor.position = 20;
827 	visitor.destination = -visitor.destination.max;
828 	model.visitBackward(data, visitor);
829 	visitor.output.should.be == [
830 		TreePosition([1], 20),
831 		TreePosition([0], 10),
832 		TreePosition([ ],  0),
833 	];
834 }
835 
836 version(unittest) @Name("aggregate")
837 unittest
838 {
839 	import unit_threaded : should, be;
840 
841 	static struct NestedData1
842 	{
843 		long l;
844 		char ch;
845 	}
846 
847 	static struct NestedData2
848 	{
849 		short sh;
850 		NestedData1 data1;
851 		string str;
852 	}
853 
854 	static struct Data
855 	{
856 		int i;
857 		float f;
858 		double d;
859 		string s;
860 		NestedData2 data2;
861 	}
862 
863 	const data = Data(0, 1, 2, "3", NestedData2(ushort(2), NestedData1(1_000_000_000, 'z'), "text"));
864 	auto model = makeModel(data);
865 	auto visitor = RelativeMeasurer();
866 
867 	model.collapsed = false;
868 	{
869 		auto mv = MeasuringVisitor(9);
870 		model.visitForward(data, mv);
871 	}
872 	visitor.position = 0;
873 	visitor.destination = visitor.destination.max;
874 	model.visitForward(data, visitor);
875 	visitor.output.should.be == [
876 		TreePosition([], 0),
877 		TreePosition([0], 10),
878 		TreePosition([1], 20),
879 		TreePosition([2], 30),
880 		TreePosition([3], 40),
881 		TreePosition([4], 50),
882 	];
883 	visitor.position.should.be == 50;
884 
885 	{
886 		visitor.path.clear;
887 		visitor.position = 0;
888 		visitor.destination = 30;
889 		model.visitForward(data, visitor);
890 		visitor.output.should.be == [
891 			TreePosition([], 0),
892 			TreePosition([0], 10),
893 			TreePosition([1], 20),
894 			TreePosition([2], 30),
895 		];
896 		visitor.position.should.be == 30;
897 	}
898 
899 	{
900 		visitor.path.clear;
901 		visitor.position = 30;
902 		visitor.destination = visitor.position + 30;
903 		model.visitForward(data, visitor);
904 		visitor.output.should.be == [
905 			TreePosition([], 30),
906 			TreePosition([0], 40),
907 			TreePosition([1], 50),
908 			TreePosition([2], 60),
909 		];
910 		visitor.position.should.be == 60;
911 	}
912 
913 	{
914 		visitor.path.value = [0];
915 		visitor.position = 130;
916 		visitor.destination = visitor.position + 20;
917 		model.visitForward(data, visitor);
918 		visitor.output.should.be == [
919 			TreePosition([0], 130),
920 			TreePosition([1], 140),
921 			TreePosition([2], 150),
922 		];
923 		visitor.position.should.be == 150;
924 	}
925 
926 	visitor.path.value = [2];
927 	visitor.position = 30;
928 	visitor.destination = visitor.destination.max;
929 	model.visitForward(data, visitor);
930 
931 	visitor.output.should.be == [
932 		TreePosition([2], 30),
933 		TreePosition([3], 40),
934 		TreePosition([4], 50),
935 	];
936 
937 	visitor.path.clear;
938 	visitor.destination = -visitor.destination.max;
939 	model.visitBackward(data, visitor);
940 	visitor.output.should.be == [
941 		TreePosition([4], 50),
942 		TreePosition([3], 40),
943 		TreePosition([2], 30),
944 		TreePosition([1], 20),
945 		TreePosition([0], 10),
946 		TreePosition([],   0),
947 	];
948 }
949 
950 version(unittest) @Name("new_paradigm")
951 unittest
952 {
953 	static struct TrivialStruct
954 	{
955 		int i;
956 		float f;
957 	}
958 
959 	static struct StructNullable
960 	{
961 		import std.typecons : Nullable;
962 		int i;
963 		Nullable!float f;
964 	}
965 
966 	auto d = StructNullable();
967 	auto m = makeModel(d);
968 	m.collapsed = false;
969 	auto visitor = DefaultVisitor(19);
970 	m.visitForward(d, visitor);
971 	import std;
972 	writeln(m);
973 }