1 /******************************************************************************
2 
3     Enhancement to VersionDecorator that allows converting through multiple
4     struct versions at once. It is kept separate from core implementation
5     because additional overhead may be not suitable for real-time apps
6 
7     Copyright:
8         Copyright (c) 2009-2016 dunnhumby Germany GmbH.
9         All rights reserved.
10 
11     License:
12         Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
13         Alternatively, this file may be distributed under the terms of the Tango
14         3-Clause BSD License (see LICENSE_BSD.txt for details).
15 
16 *******************************************************************************/
17 
18 module ocean.util.serialize.contiguous.MultiVersionDecorator;
19 
20 import ocean.transition;
21 
22 import ocean.core.Buffer;
23 import ocean.core.Verify;
24 import ocean.core.Exception;
25 import ocean.core.Enforce;
26 import ocean.core.StructConverter : structConvert;
27 
28 import ocean.stdc.string : memmove;
29 import ocean.math.Math : abs;
30 import ocean.util.container.ConcatBuffer;
31 
32 import ocean.util.serialize.Version;
33 import ocean.util.serialize.contiguous.Serializer;
34 import ocean.util.serialize.contiguous.Deserializer;
35 import ocean.util.serialize.contiguous.Contiguous;
36 
37 version (UnitTest)
38 {
39     import ocean.core.array.Search : rfind;
40     import ocean.core.Test;
41     import ocean.text.convert.Formatter;
42 }
43 
44 /*******************************************************************************
45 
46     Alternative contiguous version decorator implementation for usage in less
47     performance critical applications. Is capable of converting through
48     multiple struct versions in one go for added convenience.
49 
50     Amount of allowed conversions for single call is set via constructor
51     argument, 10 by default
52 
53 *******************************************************************************/
54 
55 class VersionDecorator
56 {
57     /// Convenience shortcut
58     public alias VersionDecorator This;
59 
60     /// Reused exception instance
61     protected VersionHandlingException e;
62 
63     /// Allowed difference between struct versions to be converted in one go
64     private size_t conversion_limit;
65 
66     /// Persistent buffer reused for temporary allocations needed for struct
67     /// conversions between different versions
68     private ConcatBuffer!(void) convert_buffer;
69 
70     /// Struct buffer for copy of deserialized data needed for in-place
71     /// deserialization with conversion
72     private Buffer!(void) struct_buffer;
73 
74     /***************************************************************************
75 
76         Constructor
77 
78         Params:
79             limit = maximum allowed difference between struct versions
80             buffer_size = starting this of convert_buffer, does not really
81                 matter much in practice because it will quickly grow to the
82                 maximum required size and stay there
83 
84     ***************************************************************************/
85 
86     public this ( size_t limit = 10, size_t buffer_size = 512 )
87     {
88         this.e = new VersionHandlingException;
89         this.conversion_limit =limit;
90         this.convert_buffer = new ConcatBuffer!(void)(buffer_size);
91     }
92 
93     /***************************************************************************
94 
95         Serializes `input` with This.Serializer and prepends version number
96         before struct data in the buffer.
97 
98         Params:
99             input  = struct instance to serialize
100             buffer = destination buffer for serialized data
101 
102         Returns:
103             full slice of `buffer`
104 
105     ***************************************************************************/
106 
107     public static void[] store ( S ) ( S input, ref Buffer!(void) buffer )
108     {
109         alias Version.Info!(S) VInfo;
110 
111         static assert (
112             VInfo.exists,
113             "Trying to use " ~ This.stringof ~ " with unversioned struct "
114                 ~ S.stringof
115         );
116 
117         buffer.length = .Serializer.countRequiredSize(input)
118             + Version.Type.sizeof;
119         auto unversioned = Version.inject(buffer, VInfo.number);
120         .Serializer.serialize(input, unversioned);
121 
122         verify(unversioned.ptr is (buffer[].ptr + Version.Type.sizeof));
123 
124         return buffer[];
125     }
126 
127     /// ditto
128     public static void[] store ( S, D ) ( S input, ref D[] buffer )
129     {
130         static assert (D.sizeof == 1,
131             "buffer can't be interpreted as void[]");
132         return store!(S)(input, *cast(Buffer!(void)*) &buffer);
133     }
134 
135     /***************************************************************************
136 
137         Loads versioned struct from `buffer` in-place
138 
139         If deserialized struct is of different version than requested one,
140         converts it iteratively, one version increment/decrement at time.
141 
142         Params:
143             buffer = data previously generated by `store` method, contains both
144                 version data and serialized struct. Will be extended if needed
145                 and modified in-place, version bytes removed
146 
147         Returns:
148             part of `buffer` after deserialization and version stripping, may be
149             wrapped in deserializer-specific struct
150 
151     ***************************************************************************/
152 
153     public Contiguous!(S) load ( S ) ( ref Buffer!(void) buffer )
154     {
155         static assert (
156             Version.Info!(S).exists,
157             "Trying to use " ~ This.stringof ~ " with unversioned struct "
158                 ~ S.stringof
159         );
160 
161         this.e.enforceInputLength!(S)(buffer.length);
162 
163         Version.Type input_version;
164         auto unversioned = Version.extract(buffer[], input_version);
165         // can't just do `buffer = unversioned` because it will create new
166         // gc root and slowly leak memory with each load
167         memmove(buffer[].ptr, unversioned.ptr, unversioned.length);
168 
169         return this.handleVersion!(S)(buffer, input_version);
170     }
171 
172     /// ditto
173     public Contiguous!(S) load(S)(ref void[] buffer)
174     {
175         return this.load!(S)(* cast(Buffer!(void)*) &buffer);
176     }
177 
178     /***************************************************************************
179 
180         Loads versioned struct from `buffer` and stores resulting data
181         in `copy_buffer`, leaving `buffer` untouched.
182 
183         If deserialized struct is of different version than requested one,
184         converts it iteratively, one version increment/decrement at time.
185 
186         Params:
187             buffer = data previously generated by `store` method, contains both
188                 version data and serialized struct. Effectively const
189             copy_buffer = buffer where deserialized struct data will be stored.
190                 Will be extended if needed and won't contain version bytes
191 
192         Returns:
193             slice of `buffer` after deserialization and version stripping
194 
195     ***************************************************************************/
196 
197     public Contiguous!(S) loadCopy ( S ) ( in void[] buffer,
198         ref Contiguous!(S) copy_buffer )
199     {
200         static assert (
201             Version.Info!(S).exists,
202             "Trying to use " ~ This.stringof ~ " with unversioned struct "
203                 ~ S.stringof
204         );
205 
206         this.e.enforceInputLength!(S)(buffer.length);
207 
208         Version.Type input_version;
209         auto unversioned = Version.extract(buffer, input_version);
210         copy_buffer.data.length = unversioned.length;
211         enableStomping(copy_buffer.data);
212         copy_buffer.data[0 .. unversioned.length] = unversioned[];
213 
214         return this.handleVersion!(S)(copy_buffer.data, input_version);
215     }
216 
217     /***************************************************************************
218 
219         Utility method to convert struct contained in input buffer to needed
220         struct version. Converted struct will be stored in the same buffer
221         replacing old data.
222 
223         You can override this method to change version converting logic.
224 
225         Params:
226             S = final struct version to get
227             buffer = input buffer after version bytes have been stripped off,
228                 will contain resulting struct data after this method exits
229             input_version = version that was extracted from buffer
230 
231         Returns:
232             deserialize() result for the last struct conversion
233 
234         Throws:
235             VersionHandlingException if can't convert between provided versions
236 
237     ***************************************************************************/
238 
239     protected Contiguous!(S) handleVersion ( S )
240         ( ref Buffer!(void) buffer, Version.Type input_version )
241     {
242         alias Version.Info!(S) VInfo;
243 
244         if (abs(input_version - VInfo.number) >= this.conversion_limit)
245         {
246             this.e.throwCantConvert!(S)(input_version);
247         }
248 
249         if (input_version == VInfo.number)
250         {
251             // no conversion is necessary
252             return .Deserializer.deserialize!(S)(buffer);
253         }
254 
255         if (input_version > VInfo.number)
256         {
257             // input is of higher version, need to convert down
258             static if (VInfo.next.exists)
259             {
260                 this.handleVersion!(VInfo.next.type)(buffer, input_version);
261                 return this.convert!(S, VInfo.next.type)(buffer);
262             }
263             else
264             {
265                 this.e.throwCantConvert!(S)(input_version);
266             }
267         }
268 
269         if (input_version < VInfo.number)
270         {
271             // input is of lower version, need to convert up
272             static if (VInfo.prev.exists)
273             {
274                 this.handleVersion!(VInfo.prev.type)(buffer, input_version);
275                 return this.convert!(S, VInfo.prev.type)(buffer);
276             }
277             else
278             {
279                 this.e.throwCantConvert!(S)(input_version);
280             }
281         }
282 
283         assert(0);
284     }
285 
286     /// ditto
287     public Contiguous!(S) handleVersion ( S )
288         ( ref void[] buffer, Version.Type input_version )
289     {
290         return handleVersion!(S)(*cast(Buffer!(void)*) &buffer,
291             input_version);
292     }
293 
294     /***************************************************************************
295 
296         Helper method that takes care of actual conversion routine between
297         two struct types (those are assumed to be of compatible versions)
298 
299         Uses this.convert_buffer for temporary allocations
300 
301         Template_Params:
302             S = needed struct type
303             Source = struct type seralized into buffer
304 
305         Params:
306             buffer = contains serialized Source instance, will be modified to
307                 store deserialized S instance instead.
308 
309     ***************************************************************************/
310 
311     public Contiguous!(S) convert ( S, Source ) ( ref Buffer!(void) buffer )
312     {
313         scope(exit)
314         {
315             this.convert_buffer.clear();
316         }
317 
318         if (this.struct_buffer.length < buffer.length)
319             this.struct_buffer.length = buffer.length;
320         this.struct_buffer[0 .. buffer.length] = buffer[];
321 
322         auto tmp_struct = .Deserializer.deserialize!(Source)(this.struct_buffer);
323         S result_struct;
324         structConvert!(Source, S)(
325             *tmp_struct.ptr,
326             result_struct,
327             &this.convert_buffer.add
328         );
329         .Serializer.serialize(result_struct, buffer);
330         return .Deserializer.deserialize!(S)(buffer);
331     }
332 
333     /// ditto
334     public Contiguous!(S) convert ( S, Source ) ( ref void[] buffer )
335     {
336         return convert!(S, Source)(*cast(Buffer!(void)*) &buffer);
337     }
338 }
339 
340 /***************************************************************************
341 
342     Exception thrown when the loaded encounters any issues with version
343     support
344 
345 ***************************************************************************/
346 
347 public class VersionHandlingException : Exception
348 {
349     mixin ReusableExceptionImplementation;
350 
351     /***************************************************************************
352 
353         Used to enforce that input is large enough to store version
354         bytes and some offset.
355 
356         Params:
357             input_length = size of input buffer
358             file = inferred
359             line = inferred
360 
361         Template_Params:
362             S = struct type that was attempted to be loaded
363 
364     ***************************************************************************/
365 
366     void enforceInputLength ( S ) ( size_t input_length,
367         istring file = __FILE__, int line = __LINE__ )
368     {
369         if (input_length <= Version.Type.sizeof)
370         {
371             this.set("Loading ")
372                 .append(S.stringof)
373                 .append(" has failed, input buffer too short (length ")
374                 .append(input_length)
375                 .append(", need ")
376                 .append(Version.Type.sizeof)
377                 .append(")");
378             this.line = line;
379             this.file = file;
380 
381             throw this;
382         }
383     }
384 
385     /***************************************************************************
386 
387         Used in case of version mismatch between requested struct and incoming
388         buffer
389 
390         Params:
391             input_version = version found in input buffer
392             file = inferred
393             line = inferred
394 
395         Template_Params:
396             S = struct type that was attempted to be loaded
397 
398     ***************************************************************************/
399 
400     void throwCantConvert ( S ) ( Version.Type input_version,
401         istring file = __FILE__, int line = __LINE__ )
402     {
403         this.set("Got version ")
404             .append(input_version)
405             .append(" for struct ")
406             .append(S.stringof)
407             .append(", expected ")
408             .append(Version.Info!(S).number)
409             .append(". Can't convert between these");
410         this.line = line;
411         this.file = file;
412 
413         throw this;
414     }
415 }
416 
417 version(UnitTest)
418 {
419     struct Multi_Test1
420     {
421         static struct Version0
422         {
423             enum StructVersion = 0;
424             alias Version1 StructNext;
425 
426             int a, b;
427 
428             mstring[] strarr;
429         }
430 
431         static struct Version1
432         {
433             enum StructVersion = 1;
434             alias Version0 StructPrevious;
435             alias Version2 StructNext;
436 
437             int b, a;
438 
439             mstring[] strarr;
440         }
441 
442         static struct Version2
443         {
444             enum StructVersion = 2;
445             alias Version1 StructPrevious;
446 
447             int a, b, c;
448 
449             mstring[] strarr;
450 
451             static void convert_c(ref Version1 s, ref Version2 dst)
452             {
453                 dst.c = s.a + s.b;
454             }
455         }
456     }
457 }
458 
459 unittest
460 {
461     // loadCopy
462 
463     auto loader = new VersionDecorator();
464     auto ver0 = Multi_Test1.Version0(42, 43, ["version0".dup]);
465     void[] serialized;
466     Contiguous!(Multi_Test1.Version2) buffer;
467 
468     loader.store(ver0, serialized);
469     auto ver2 = loader.loadCopy(serialized, buffer);
470 
471     testNoAlloc({
472         auto ver2 = loader.loadCopy(serialized, buffer);
473     } ());
474 
475     test!("==")(ver2.ptr.a, ver0.a);
476     test!("==")(ver2.ptr.b, ver0.b);
477     test!("==")(ver2.ptr.c, ver0.a + ver0.b);
478     test!("==")(ver2.ptr.strarr, ver0.strarr);
479 }
480 
481 unittest
482 {
483     // in-place load
484 
485     auto loader = new VersionDecorator();
486 
487     auto ver0 = Multi_Test1.Version0(42, 43, ["version0".dup]);
488     void[] buffer;
489 
490     loader.store(ver0, buffer);
491     auto ver2 = loader.load!(Multi_Test1.Version2)(buffer);
492 
493     test!("==")(ver2.ptr.a, ver0.a);
494     test!("==")(ver2.ptr.b, ver0.b);
495     test!("==")(ver2.ptr.c, ver0.a + ver0.b);
496     test!("==")(ver2.ptr.strarr, ver0.strarr);
497 
498     void[] buffer2;
499     loader.store(*ver2.ptr, buffer2);
500     auto ver0_again = loader.load!(Multi_Test1.Version0)(buffer2);
501 
502 }
503 
504 // error handling
505 
506 version (UnitTest)
507 {
508     struct Multi_Test2
509     {
510         static struct Version3
511         {
512             int a, b;
513             enum StructVersion = 3;
514         }
515 
516         static struct VersionHuge
517         {
518             enum StructVersion = 100;
519         }
520     }
521 }
522 
523 unittest
524 {
525     auto loader = new VersionDecorator();
526 
527     auto ver0 = Multi_Test1.Version0(42, 43, ["version0".dup]);
528     void[] buffer;
529 
530     // version number difference too big
531     loader.store(ver0, buffer);
532     testThrown!(VersionHandlingException)(
533         loader.load!(Multi_Test2.VersionHuge)(buffer)
534     );
535 
536     // "next" alias is not defined
537     loader.store(ver0, buffer);
538     testThrown!(VersionHandlingException)(
539         loader.load!(Multi_Test2.Version3)(buffer)
540     );
541 
542     // "prev" alias is not defined
543     loader.store(Multi_Test2.Version3.init, buffer);
544     testThrown!(VersionHandlingException)(
545         loader.load!(Multi_Test1.Version2)(buffer)
546     );
547 }
548 
549 
550 version (UnitTest):
551 
552 /*******************************************************************************
553 
554     No conversion. More extensively covered by (de)serializer base tests in
555     package_test.d
556 
557 *******************************************************************************/
558 
559 unittest
560 {
561     struct S
562     {
563         enum StructVersion = 1;
564 
565         int    a = 42;
566         double b = 2.0;
567     }
568 
569     auto loader = new VersionDecorator;
570 
571     void[] buffer;
572     S t;
573     loader.store(t, buffer);
574 
575     Contiguous!(S) dst;
576     loader.loadCopy!(S)(buffer, dst);
577 
578     test!("==")(dst.ptr.a, t.a);
579     test!("==")(dst.ptr.b, t.b);
580 
581     dst = loader.load!(S)(buffer);
582 
583     test!("==")(dst.ptr.a, t.a);
584     test!("==")(dst.ptr.b, t.b);
585 }
586 
587 /*******************************************************************************
588 
589     No conversion. Check non void[] API.
590 
591 *******************************************************************************/
592 
593 unittest
594 {
595     struct S
596     {
597         enum StructVersion = 1;
598         int a = 42;
599     }
600 
601     auto loader = new VersionDecorator;
602 
603     ubyte[] buffer;
604     S t;
605     loader.store(t, buffer);
606 
607     Contiguous!(S) dst;
608     loader.loadCopy!(S)(buffer, dst);
609 
610     test!("==")(dst.ptr.a, t.a);
611 }
612 
613 /*******************************************************************************
614 
615     Error handling
616 
617 *******************************************************************************/
618 
619 unittest
620 {
621     auto loader = new VersionDecorator;
622     void[] buffer = null;
623 
624     // must not accept non-versioned
625 
626     struct NoVersion { }
627     static assert (!is(typeof(loader.load!(NoVersion)(buffer))));
628 
629     // must detect if input size is too small
630 
631     struct Dummy { enum StructVersion = 1; }
632 
633     testThrown!(VersionHandlingException)(loader.load!(Dummy)(buffer));
634 
635     Contiguous!(Dummy) dst;
636     testThrown!(VersionHandlingException)(
637         loader.loadCopy!(Dummy)(buffer, dst));
638 
639     // must detect if conversion is not defined
640 
641     struct Dummy2 { enum StructVersion = 2; }
642 
643     loader.store(Dummy2.init, buffer);
644     testThrown!(VersionHandlingException)(loader.load!(Dummy)(buffer));
645 
646     loader.store(Dummy.init, buffer);
647     testThrown!(VersionHandlingException)(loader.load!(Dummy2)(buffer));
648 }
649 
650 /*******************************************************************************
651 
652     Conversion from higher version, trivial struct
653 
654 *******************************************************************************/
655 
656 struct Test1
657 {
658     struct Version1
659     {
660         enum StructVersion = 1;
661 
662         alias Version2 StructNext;
663 
664         static void convert_a(ref Version2 src, ref Version1 dst)
665         {
666             dst.a = src.a + 1;
667         }
668 
669         int a;
670     }
671 
672     struct Version2
673     {
674         enum StructVersion = 2;
675 
676         int a = 42;
677     }
678 }
679 
680 unittest
681 {
682     auto loader = new VersionDecorator;
683 
684     with (Test1)
685     {
686         void[] buffer;
687         Version2 t;
688         loader.store(t, buffer);
689 
690         Contiguous!(Version1) dst;
691         loader.loadCopy(buffer, dst);
692         test!("==")(dst.ptr.a, t.a + 1);
693 
694         auto result = loader.load!(Version1)(buffer);
695         test!("==")(result.ptr.a, t.a + 1);
696     }
697 }
698 
699 /*******************************************************************************
700 
701     Conversion from lower version, trivial struct
702 
703 *******************************************************************************/
704 
705 struct Test2
706 {
707     struct Version1
708     {
709         enum StructVersion = 1;
710 
711         int a;
712     }
713 
714     struct Version2
715     {
716         enum StructVersion = 2;
717 
718         alias Version1 StructPrevious;
719 
720         static void convert_a(ref Version1 src, ref Version2 dst)
721         {
722             dst.a = src.a + 1;
723         }
724 
725         int a = 42;
726     }
727 }
728 
729 unittest
730 {
731     auto loader = new VersionDecorator;
732 
733     with (Test2)
734     {
735         Buffer!(void) buffer;
736         Version1 t;
737         loader.store(t, buffer);
738 
739         Contiguous!(Version2) dst;
740         loader.loadCopy(buffer[], dst);
741         test!("==")(dst.ptr.a, t.a + 1);
742 
743         auto result = loader.load!(Version2)(buffer);
744         test!("==")(result.ptr.a, t.a + 1);
745     }
746 }
747 
748 /*******************************************************************************
749 
750     Chained bi-directional conversions
751 
752 *******************************************************************************/
753 
754 struct Test3
755 {
756     struct Version0
757     {
758         enum ubyte StructVersion = 0;
759         alias Version1 StructNext;
760 
761         struct Nested0
762         {
763             int a;
764         }
765 
766         int a;
767         int b;
768 
769         Nested0[] nested_arr;
770         char[][]  string_arr;
771 
772         static Version0 create ()
773         {
774             Version0 t;
775 
776             t.a = 100;
777             t.b = -100;
778             t.nested_arr = [ Nested0(42), Nested0(43), Nested0(44) ];
779             t.string_arr = [ "This".dup, "Is".dup,
780                 "A".dup, "Freaking".dup, "String!".dup ];
781 
782             return t;
783         }
784 
785         void compare ( NamedTest t, Version0 other )
786         {
787             foreach (index, ref element; (&this).tupleof)
788             {
789                 t.test!("==")(element, other.tupleof[index]);
790             }
791         }
792 
793         void compare ( NamedTest t, Version1 other )
794         {
795             foreach (index, ref element; (&this).tupleof)
796             {
797                 enum name = this.tupleof[index].stringof[
798                     rfind(this.tupleof[index].stringof, "."[]) + 1 .. $
799                 ];
800 
801                 static if (name == "nested_arr")
802                 {
803                     foreach (i, elem; (&this).nested_arr)
804                     {
805                         t.test!("==")(elem.a, other.nested_arr[i].a);
806                     }
807                 }
808                 else
809                     mixin(`test!("==")(element, other.` ~ name ~ `);`);
810             }
811         }
812     }
813 
814     struct Version1
815     {
816         enum ubyte StructVersion = 1;
817         alias Version0 StructPrevious;
818         alias Version2 StructNext;
819 
820         struct Nested1
821         {
822             int a;
823             int b;
824 
825             static void convert_b ( ref Version0.Nested0 s, ref Nested1 dst )
826             {
827                 dst.b = s.a + 1;
828             }
829             static void convert_b ( ref Version2.Nested2 s, ref Nested1 dst )
830             {
831                 dst.b = s.a / 2;
832             }
833         }
834 
835         int a;
836         int b;
837         int c;
838 
839         Nested1[] nested_arr;
840         char[][]  string_arr;
841 
842         static void convert_c ( ref Version0 s, ref Version1 dst )
843         {
844             dst.c = s.b - s.a;
845         }
846 
847         static void convert_c ( ref Version2 s, ref Version1 dst )
848         {
849             dst.c = s.d;
850         }
851 
852         void compare ( NamedTest t, Version0 other )
853         {
854             foreach (index, ref member; (&this).tupleof)
855             {
856                 enum name = this.tupleof[index].stringof[
857                     rfind(this.tupleof[index].stringof, "."[]) + 1 .. $
858                 ];
859 
860                 static if (name == "nested_arr")
861                 {
862                     foreach (i, ref nested; (&this).nested_arr)
863                     {
864                         test!("==")(nested.a, other.nested_arr[i].a);
865                         test!("==")(nested.b, other.nested_arr[i].a + 1);
866                     }
867                 }
868                 else static if (name == "c")
869                 {
870                     test!("==")((&this).c, other.b - other.a);
871                 }
872                 else
873                 {
874                     mixin(`test!("==")(member, other.` ~ name ~ ");");
875                 }
876             }
877         }
878 
879         void compare ( NamedTest t, Version1 other )
880         {
881             foreach (index, ref element; (&this).tupleof)
882             {
883                 t.test!("==")(element, other.tupleof[index]);
884             }
885         }
886 
887         void compare ( NamedTest t, Version2 other )
888         {
889             foreach (index, ref member; (&this).tupleof)
890             {
891                 enum name = this.tupleof[index].stringof[
892                     rfind(this.tupleof[index].stringof, "."[]) + 1 .. $
893                 ];
894 
895                 static if (name == "nested_arr")
896                 {
897                     foreach (i, ref nested; (&this).nested_arr)
898                     {
899                         test!("==")(nested.a, other.nested_arr[i].a);
900                         test!("==")(nested.b, other.nested_arr[i].a / 2);
901                     }
902                 }
903                 else static if (name == "c")
904                 {
905                     test!("==")((&this).c, other.d);
906                 }
907                 else
908                 {
909                     mixin(`test!("==")(member, other.` ~ name ~ ");");
910                 }
911             }
912         }
913     }
914 
915     struct Version2
916     {
917         enum ubyte StructVersion = 2;
918 
919         alias Version1 StructPrevious;
920 
921         struct Nested2
922         {
923             int a;
924 
925             static void convert_a ( ref Version1.Nested1 s, ref Nested2 dst )
926             {
927                 dst.a = s.b * 2;
928             }
929         }
930 
931         Nested2[] nested_arr;
932 
933         int b;
934         int a;
935         int d;
936 
937         char[][] string_arr;
938 
939         static void convert_d ( ref Version1 s, ref Version2 dst )
940         {
941             dst.d = s.c;
942         }
943 
944         void compare ( NamedTest t, ref Version0 other )
945         {
946             assert (false);
947         }
948 
949         void compare ( NamedTest t, ref Version1 other )
950         {
951             foreach (index, ref member; (&this).tupleof)
952             {
953                 enum name = this.tupleof[index].stringof[
954                     rfind(this.tupleof[index].stringof, "."[]) + 1 .. $
955                 ];
956 
957                 static if (name == "nested_arr")
958                 {
959                     foreach (i, ref nested; (&this).nested_arr)
960                     {
961                         test!("==")(nested.a, other.nested_arr[i].b * 2);
962                     }
963                 }
964                 else static if (name == "d")
965                 {
966                     test!("==")((&this).d, other.c);
967                 }
968                 else
969                 {
970                     mixin(`test!("==")(member, other.` ~ name ~ ");");
971                 }
972             }
973         }
974 
975         void compare ( NamedTest t, ref Version2 other )
976         {
977             foreach (index, member; (&this).tupleof)
978             {
979                 t.test!("==")(member, other.tupleof[index]);
980             }
981         }
982     }
983 }
984 
985 Dst testConv(Src, Dst)(Src src, size_t limit = 10)
986 {
987     auto test = new NamedTest(Src.stringof ~ " -> " ~ Dst.stringof);
988 
989     try
990     {
991         auto loader = new VersionDecorator(limit);
992         void[] buffer;
993 
994         loader.store(src, buffer);
995         auto dst = loader.load!(Dst)(buffer);
996         dst.ptr.compare(test, src);
997 
998         return *dst.ptr;
999     }
1000     catch (Exception e)
1001     {
1002         if (e.classinfo == TestException.classinfo)
1003             throw e;
1004 
1005         test.msg = format(
1006             "Unhandled exception of type {} from {}:{} - '{}'",
1007             e.classinfo.name,
1008             e.file,
1009             e.line,
1010             e.message()
1011         );
1012         test.file = __FILE__;
1013         test.line = __LINE__;
1014         throw test;
1015     }
1016 }
1017 
1018 unittest
1019 {
1020     with (Test3)
1021     {
1022         // internal sanity : exceptions must propagate as NamedTest exceptions
1023         testThrown!(NamedTest)(
1024             testConv!(Version0, Version2)(Version0.create(), 1)
1025         );
1026 
1027         auto ver0 = testConv!(Version0, Version0)(Version0.create());
1028         auto ver1 = testConv!(Version0, Version1)(ver0);
1029         auto ver2 = testConv!(Version1, Version2)(ver1);
1030         auto ver1_r = testConv!(Version2, Version1)(ver2);
1031         auto ver0_r = testConv!(Version1, Version0)(ver1_r);
1032 
1033         testConv!(Version1, Version1)(ver1);
1034         testConv!(Version2, Version2)(ver2);
1035     }
1036 }
1037 
1038 Dst testConvMemory(Src, Dst)(Src src, size_t limit = 10)
1039 {
1040     auto test = new NamedTest(Src.stringof ~ " -> " ~ Dst.stringof);
1041 
1042     Contiguous!(Dst) result;
1043     auto loader = new VersionDecorator(limit);
1044     void[] buffer;
1045 
1046     static immutable iterations = 10_000;
1047 
1048     static void storeThenLoad (ref NamedTest test, ref VersionDecorator loader,
1049                                ref Src src, ref void[] buffer,
1050                                ref Contiguous!(Dst) result)
1051     {
1052         try
1053         {
1054             loader.store(src, buffer);
1055             result = loader.load!(Dst)(buffer);
1056             //    result.ptr.compare(test, src);
1057         }
1058         catch (Exception e)
1059         {
1060             if (e.classinfo == TestException.classinfo)
1061                 throw e;
1062 
1063             test.msg = format(
1064                 "Unhandled exception of type {} from {}:{} - '{}'",
1065                 e.classinfo.name,
1066                 e.file,
1067                 e.line,
1068                 e.message()
1069                 );
1070             test.file = __FILE__;
1071             test.line = __LINE__;
1072             throw test;
1073         }
1074     }
1075 
1076     // After 1% of the iterations, memory usage shouldn't grow anymore
1077     for ( size_t i = 0; i < (iterations / 100); ++i )
1078     {
1079         storeThenLoad(test, loader, src, buffer, result);
1080     }
1081 
1082     // Do the other 99%
1083     testNoAlloc(
1084         {
1085             for ( size_t i = 0; i < iterations - (iterations / 100); ++i )
1086             {
1087                 storeThenLoad(test, loader, src, buffer, result);
1088             }
1089         }());
1090 
1091     return *result.ptr;
1092 }
1093 
1094 unittest
1095 {
1096     with (Test3)
1097     {
1098         // internal sanity : exceptions must propagate as NamedTest exceptions
1099         testThrown!(NamedTest)(
1100             testConvMemory!(Version0, Version2)(Version0.create(), 1)
1101         );
1102 
1103         auto ver0 = testConvMemory!(Version0, Version0)(Version0.create());
1104         auto ver1 = testConvMemory!(Version0, Version1)(ver0);
1105         auto ver2 = testConvMemory!(Version1, Version2)(ver1);
1106         auto ver1_r = testConvMemory!(Version2, Version1)(ver2);
1107         auto ver0_r = testConvMemory!(Version1, Version0)(ver1_r);
1108 
1109         testConvMemory!(Version1, Version1)(ver1);
1110         testConvMemory!(Version2, Version2)(ver2);
1111     }
1112 }
1113 
1114 /******************************************************************************
1115 
1116     Conversion which replaces struct fields completely
1117 
1118 ******************************************************************************/
1119 
1120 struct Test4
1121 {
1122     struct Ver0
1123     {
1124         enum ubyte StructVersion = 0;
1125         int a;
1126     }
1127 
1128     struct Ver1
1129     {
1130         enum ubyte StructVersion = 1;
1131         alias Test4.Ver0 StructPrevious;
1132 
1133         long b;
1134         static void convert_b(ref Ver0 rhs, ref Ver1 dst) { dst.b = 42; }
1135     }
1136 }
1137 
1138 unittest
1139 {
1140     auto loader = new VersionDecorator;
1141     void[] buffer;
1142 
1143     auto src = Test4.Ver0(20);
1144     loader.store(src, buffer);
1145     auto dst = loader.load!(Test4.Ver1)(buffer);
1146     test!("==")(dst.ptr.b, 42);
1147 }