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