• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zorbathut / dec / 9307326907

30 May 2024 06:18PM UTC coverage: 91.303% (-0.1%) from 91.401%
9307326907

Pull #7

github

zorbathut
Decs can now include `class` tags, if you need to specify a type that doesn't work as an XML node name.
Pull Request #7: Decs can now include class tags, if you need to specify a type that doesn't work as an XML node name.

4336 of 4749 relevant lines covered (91.3%)

183722.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.01
/src/WriterXml.cs
1
namespace Dec
2
{
3
    using System;
4
    using System.Collections;
5
    using System.Collections.Generic;
6
    using System.ComponentModel;
7
    using System.Linq;
8
    using System.Threading;
9
    using System.Xml;
10
    using System.Xml.Linq;
11

12
    internal abstract class WriterXml
13
    {
14
        // A list of writes that still have to happen. This is used so we don't have to do deep recursive dives and potentially blow our stack.
15
        // I think this is only used for WriterXmlRecord, but right now this all goes through WriterNodeXml which is meant to work with both of these.
16
        // The inheritance tree is kind of messed up right now and should be fixed.
17
        private WriterUtil.PendingWriteCoordinator pendingWriteCoordinator = new WriterUtil.PendingWriteCoordinator();
6,145✔
18

19
        public abstract bool AllowReflection { get; }
20
        public abstract Recorder.IUserSettings UserSettings { get; }
21

22
        public abstract bool RegisterReference(object referenced, XElement element, Recorder.Context recContext);
23

24
        public void RegisterPendingWrite(Action action)
25
        {
2,985✔
26
            pendingWriteCoordinator.RegisterPendingWrite(action);
2,985✔
27
        }
2,985✔
28

29
        public void DequeuePendingWrites()
30
        {
6,145✔
31
            pendingWriteCoordinator.DequeuePendingWrites();
6,145✔
32
        }
6,145✔
33
    }
34

35
    internal class WriterXmlCompose : WriterXml
36
    {
37
        public override bool AllowReflection { get => true; }
37,226✔
38
        public override Recorder.IUserSettings UserSettings { get; }
20✔
39

40
        private XDocument doc;
41
        private XElement decs;
42

43
        public WriterXmlCompose(Recorder.IUserSettings userSettings)
3,895✔
44
        {
3,895✔
45
            this.UserSettings = userSettings;
3,895✔
46

47
            doc = new XDocument();
3,895✔
48

49
            decs = new XElement("Decs");
3,895✔
50
            doc.Add(decs);
3,895✔
51
        }
3,895✔
52

53
        public override bool RegisterReference(object referenced, XElement element, Recorder.Context recContext)
54
        {
41,046✔
55
            // We never register references in Compose mode.
56
            return false;
41,046✔
57
        }
41,046✔
58

59
        public WriterNode StartDec(Type type, string decName)
60
        {
14,630✔
61
            string typeName;
62
            Type overrideClass = null;
14,630✔
63
            if (type.IsGenericType)
14,630✔
64
            {
10✔
65
                // okay, this just got more complicated
66
                // we need to find a dec subclass that doesn't have any generics, then attach an attribute
67
                // this is . . . not great design, honestly, I'm not sure I like this format right now
68
                // but, fine, it's ugly but it will work
69
                Type baseType = type.BaseType;
10✔
70
                while (baseType.IsGenericType)
10✔
71
                {
×
72
                    baseType = baseType.BaseType;
×
73
                }
×
74

75
                typeName = baseType.ComposeDecFormatted();
10✔
76
                overrideClass = type;
10✔
77
            }
10✔
78
            else
79
            {
14,620✔
80
                typeName = type.ComposeDecFormatted();
14,620✔
81
            }
14,620✔
82

83
            var nodeXml = WriterNodeXml.StartDec(this, decs, typeName, decName);
14,630✔
84

85
            if (overrideClass != null)
14,630✔
86
            {
10✔
87
                nodeXml.TagClass(overrideClass);
10✔
88
            }
10✔
89

90
            return nodeXml;
14,630✔
91
        }
14,630✔
92

93
        public string Finish(bool pretty)
94
        {
3,895✔
95
            DequeuePendingWrites();
3,895✔
96

97
            return doc.ToString(pretty ? SaveOptions.None : SaveOptions.DisableFormatting);
3,895✔
98
        }
3,895✔
99
    }
100

101
    internal class WriterXmlRecord : WriterXml
102
    {
103
        public override bool AllowReflection { get => false; }
318,070✔
104
        public override Recorder.IUserSettings UserSettings { get; }
15✔
105

106
        // Maps between object and the in-place element. This does *not* yet have the ref ID tagged, and will have to be extracted into a new Element later.
107
        private Dictionary<object, XElement> refToElement = new Dictionary<object, XElement>();
2,250✔
108
        private Dictionary<XElement, object> elementToRef = new Dictionary<XElement, object>();
2,250✔
109

110
        // A map from object to the string intended as a reference. This will be filled in only once a second reference to something is created.
111
        // This is cleared after we resolve references, then re-used for the depth capping code.
112
        private Dictionary<object, string> refToString = new Dictionary<object, string>();
2,250✔
113

114
        // Current reference ID that we're on.
115
        private int referenceId = 0;
2,250✔
116

117
        private XDocument doc;
118
        private XElement record;
119
        private XElement refs;
120
        private XElement rootElement;
121

122
        public WriterXmlRecord(Recorder.IUserSettings userSettings)
2,250✔
123
        {
2,250✔
124
            this.UserSettings = userSettings;
2,250✔
125

126
            doc = new XDocument();
2,250✔
127

128
            record = new XElement("Record");
2,250✔
129
            doc.Add(record);
2,250✔
130

131
            record.Add(new XElement("recordFormatVersion", 1));
2,250✔
132

133
            refs = new XElement("refs");
2,250✔
134
            record.Add(refs);
2,250✔
135
        }
2,250✔
136

137
        public override bool RegisterReference(object referenced, XElement element, Recorder.Context recContext)
138
        {
472,080✔
139
            bool forceProcess = false;
472,080✔
140

141
            if (!refToElement.TryGetValue(referenced, out var xelement))
472,080✔
142
            {
321,495✔
143
                if (recContext.shared != Recorder.Context.Shared.Deny)
321,495✔
144
                {
302,760✔
145
                    // Insert it into our refToElement mapping
146
                    refToElement[referenced] = element;
302,760✔
147
                    elementToRef[element] = referenced;
302,760✔
148
                }
302,760✔
149
                else
150
                {
18,735✔
151
                    // Cannot be referenced, so we insert a fake null entry
152
                    refToElement[referenced] = null;
18,735✔
153

154
                    // Note: It is important not to add an elementToRef entry because this is later used to split long hierarchies
155
                    // and if you split a long hierarchy around a non-referencable barrier, everything breaks!
156
                }
18,735✔
157

158
                if (Config.TestRefEverything && recContext.shared != Recorder.Context.Shared.Deny)
321,495✔
159
                {
100,740✔
160
                    // Test pathway that should only occur during testing.
161
                    xelement = element;
100,740✔
162
                    forceProcess = true;
100,740✔
163
                }
100,740✔
164
                else
165
                {
220,755✔
166
                    return false;
220,755✔
167
                }
168
            }
100,740✔
169

170
            if (xelement == null)
251,325✔
171
            {
40✔
172
                // This is an unreferencable object! We are in trouble.
173
                Dbg.Err("Attempted to create a new reference to an unshared object. This may result in an invalid serialization. If this is coming from a Recorder setup, perhaps you need a .Shared() decorator.");
40✔
174
                return true;
40✔
175
            }
176

177
            // We have a referenceable target, but do *we* allow a reference?
178
            if (recContext.shared == Recorder.Context.Shared.Deny)
251,285✔
179
            {
×
180
                Dbg.Err("Attempted to create a new unshared reference to a previously-seen object. This may result in an invalid serialization. If this is coming from a Recorder setup, it's likely you either need a .Shared() decorator, or you need to ensure that this object is not serialized elsewhere.");
×
181
                return true;
×
182
            }
183

184
            var refId = refToString.TryGetValue(referenced);
251,285✔
185
            if (refId == null)
251,285✔
186
            {
201,075✔
187
                // We already had a reference, but we don't have a string ID for it. We need one now though!
188
                refId = $"ref{referenceId++:D5}";
201,075✔
189
                refToString[referenced] = refId;
201,075✔
190
            }
201,075✔
191

192
            // Tag the XML element properly
193
            element.SetAttributeValue("ref", refId);
251,285✔
194

195
            // And we're done!
196
            // If we're forcing auto-ref'ing, then we allow processing this; otherwise, we tell it to skip because it's already done.
197
            return !forceProcess;
251,285✔
198
        }
472,080✔
199

200
        public IEnumerable<KeyValuePair<string, XElement>> StripAndOutputReferences()
201
        {
7,240✔
202
            // It is *vitally* important that we do this step *after* all references are generated, not inline as we add references.
203
            // This is because we have to move all the contents of the XML element, but if we do it during generation, a recursive-reference situation could result in us trying to move the XML element before its contents are fully generated.
204
            // So we do it now, when we know that everything is finished.
205
            foreach (var refblock in refToString)
433,850✔
206
            {
206,065✔
207
                var result = new XElement("Ref");
206,065✔
208
                result.SetAttributeValue("id", refblock.Value);
206,065✔
209

210
                var src = refToElement[refblock.Key];
206,065✔
211

212
                // gotta ToArray() because it does not like mutating things while iterating
213
                // And yes, you have to .Remove() also, otherwise you get copies in both places.
214
                foreach (var attribute in src.Attributes().ToArray())
819,745✔
215
                {
100,775✔
216
                    attribute.Remove();
100,775✔
217

218
                    // We will normally not have a ref attribute here, but if we're doing the ref-everything mode, we might.
219
                    if (attribute.Name != "ref")
100,775✔
220
                    {
35✔
221
                        result.Add(attribute);
35✔
222
                    }
35✔
223
                }
100,775✔
224

225
                foreach (var node in src.Nodes().ToArray())
1,442,545✔
226
                {
412,175✔
227
                    node.Remove();
412,175✔
228
                    result.Add(node);
412,175✔
229
                }
412,175✔
230

231
                // Patch in the ref link
232
                src.SetAttributeValue("ref", refblock.Value);
206,065✔
233

234
                // We may not have had a class to begin with, but we sure need one now!
235
                result.SetAttributeValue("class", refblock.Key.GetType().ComposeDecFormatted());
206,065✔
236

237
                yield return new KeyValuePair<string, XElement>(refblock.Value, result);
206,065✔
238
            }
206,065✔
239

240
            // We're now done processing this segment and can erase it; we don't want to try doing this a second time!
241
            refToString.Clear();
7,240✔
242
        }
7,240✔
243

244
        public bool ProcessDepthLimitedReferences(XElement node, int depthRemaining)
245
        {
846,845✔
246
            if (depthRemaining <= 0 && elementToRef.ContainsKey(node))
846,845✔
247
            {
4,990✔
248
                refToString[elementToRef[node]] = $"ref{referenceId++:D5}";
4,990✔
249
                // We don't continue recursively because then we're threatening a stack overflow; we'll get it on the next pass
250

251
                return true;
4,990✔
252
            }
253
            else if (depthRemaining <= -100)
841,855✔
254
            {
15✔
255
                Dbg.Err("Depth limiter ran into an unshareable node stack that's too deep. Recommend using more `.Shared()` calls to allow for stack splitting. Generated file may not be readable (ask on Discord if you need this) and is likely to be very inefficient.");
15✔
256
                return false;
15✔
257
            }
258
            else
259
            {
841,840✔
260
                bool found = false;
841,840✔
261
                foreach (var child in node.Elements())
3,802,580✔
262
                {
638,530✔
263
                    found |= ProcessDepthLimitedReferences(child, depthRemaining - 1);
638,530✔
264
                }
638,530✔
265

266
                return found;
841,840✔
267
            }
268
        }
846,845✔
269

270
        public WriterNodeXml StartData(Type type)
271
        {
2,250✔
272
            var node = WriterNodeXml.StartData(this, record, "data", type);
2,250✔
273
            rootElement = node.GetXElement();
2,250✔
274
            return node;
2,250✔
275
        }
2,250✔
276

277
        public string Finish(bool pretty)
278
        {
2,250✔
279
            // Handle all our pending writes
280
            DequeuePendingWrites();
2,250✔
281

282
            // We now have a giant XML tree, potentially many thousands of nodes deep, where some nodes are references and some *should* be in the reference bank but aren't.
283
            // We need to do two things:
284
            // * Make all of our tagged references into actual references in the Refs section
285
            // * Tag anything deeper than a certain depth as a reference, then move it into the Refs section
286
            var depthTestsPending = new List<XElement>();
2,250✔
287
            depthTestsPending.Add(rootElement);
2,250✔
288

289
            // This is a loop between "write references" and "tag everything below a certain depth as needing to be turned into a reference".
290
            // We do this in a loop so we don't have to worry about ironically blowing our stack while making a change required to not blow our stack.
291
            while (true)
7,240✔
292
            {
7,240✔
293
                // Canonical ordering to provide some stability and ease-of-reading.
294
                foreach (var reference in StripAndOutputReferences().OrderBy(kvp => kvp.Key))
639,915✔
295
                {
206,065✔
296
                    refs.Add(reference.Value);
206,065✔
297
                    depthTestsPending.Add(reference.Value);
206,065✔
298
                }
206,065✔
299

300
                bool found = false;
7,240✔
301
                for (int i = 0; i < depthTestsPending.Count; ++i)
431,110✔
302
                {
208,315✔
303
                    // Magic number should probably be configurable at some point
304
                    found |= ProcessDepthLimitedReferences(depthTestsPending[i], 20);
208,315✔
305
                }
208,315✔
306
                depthTestsPending.Clear();
7,240✔
307

308
                if (!found)
7,240✔
309
                {
2,250✔
310
                    // No new depth-clobbering references found, just move on
311
                    break;
2,250✔
312
                }
313
            }
4,990✔
314

315
            if (refs.IsEmpty)
2,250✔
316
            {
1,440✔
317
                // strip out the refs 'cause it looks better that way :V
318
                refs.Remove();
1,440✔
319
            }
1,440✔
320

321
            if (!pretty)
2,250✔
322
            {
1,650✔
323
                doc.AddFirst(new XComment("Pretty-print can be enabled as a parameter of the Recorder.Write() call."));
1,650✔
324
            }
1,650✔
325
            doc.AddFirst(new XComment("This file was written by Dec, a serialization library designed for game development. (https://github.com/zorbathut/dec)"));
2,250✔
326

327
            return doc.ToString(pretty ? SaveOptions.None : SaveOptions.DisableFormatting);
2,250✔
328
        }
2,250✔
329
    }
330

331
    internal sealed class WriterNodeXml : WriterNode
332
    {
333
        private WriterXml writer;
334
        private XElement node;
335

336
        // Represents only the *active* depth in the program stack.
337
        // This is kind of painfully hacky, because when it's created, we don't know if it's going to represent a new stack start.
338
        // So we just kinda adjust it as we go.
339
        private int depth;
340
        private const int MaxRecursionDepth = 100;
341

342
        public override bool AllowReflection { get => writer.AllowReflection; }
355,296✔
343
        public override Recorder.IUserSettings UserSettings { get => writer.UserSettings; }
35✔
344

345
        private WriterNodeXml(WriterXml writer, XElement parent, string label, int depth, Recorder.Context context) : base(context)
939,674✔
346
        {
939,674✔
347
            this.writer = writer;
939,674✔
348
            this.depth = depth;
939,674✔
349

350
            node = new XElement(label);
939,674✔
351
            parent.Add(node);
939,674✔
352
        }
939,674✔
353

354
        public static WriterNodeXml StartDec(WriterXmlCompose writer, XElement decRoot, string type, string decName)
355
        {
14,630✔
356
            var node = new WriterNodeXml(writer, decRoot, type, 0, new Recorder.Context());
14,630✔
357
            node.GetXElement().Add(new XAttribute("decName", decName));
14,630✔
358
            return node;
14,630✔
359
        }
14,630✔
360

361
        public static WriterNodeXml StartData(WriterXmlRecord writer, XElement decRoot, string name, Type type)
362
        {
2,250✔
363
            return new WriterNodeXml(writer, decRoot, name, 0, new Recorder.Context() { shared = Recorder.Context.Shared.Flexible });
2,250✔
364
        }
2,250✔
365

366
        internal WriterNodeXml CreateNamedChild(string label, Recorder.Context context)
367
        {
92,035✔
368
            return new WriterNodeXml(writer, node, label, depth + 1, context);
92,035✔
369
        }
92,035✔
370

371
        // this should be WriterNodeXml but this C# doesn't support that
372
        public override WriterNode CreateRecorderChild(string label, Recorder.Context context)
373
        {
633,235✔
374
            return new WriterNodeXml(writer, node, label, depth + 1, context);
633,235✔
375
        }
633,235✔
376

377
        // this should be WriterNodeXml but this C# doesn't support that
378
        public override WriterNode CreateReflectionChild(System.Reflection.FieldInfo field, Recorder.Context context)
379
        {
197,524✔
380
            return new WriterNodeXml(writer, node, field.Name, depth + 1, context);
197,524✔
381
        }
197,524✔
382

383
        public override void WritePrimitive(object value)
384
        {
162,936✔
385
            if (value.GetType() == typeof(double))
162,936✔
386
            {
1,870✔
387
                double val = (double)value;
1,870✔
388
                if (double.IsNaN(val) && BitConverter.DoubleToInt64Bits(val) != BitConverter.DoubleToInt64Bits(double.NaN))
1,870✔
389
                {
60✔
390
                    // oops, all nan boxing!
391
                    node.Add(new XText("NaNbox" + BitConverter.DoubleToInt64Bits(val).ToString("X16")));
60✔
392
                }
60✔
393
                else if (Compat.FloatRoundtripBroken)
1,810✔
394
                {
362✔
395
                    node.Add(new XText(val.ToString("G17")));
362✔
396
                }
362✔
397
                else
398
                {
1,448✔
399
                    node.Add(new XText(val.ToString()));
1,448✔
400
                }
1,448✔
401
            }
1,870✔
402
            else if (value.GetType() == typeof(float))
161,066✔
403
            {
17,265✔
404
                float val = (float)value;
17,265✔
405
                if (float.IsNaN(val) && BitConverter.SingleToInt32Bits(val) != BitConverter.SingleToInt32Bits(float.NaN))
17,265✔
406
                {
70✔
407
                    // oops, all nan boxing!
408
                    node.Add(new XText("NaNbox" + BitConverter.SingleToInt32Bits(val).ToString("X8")));
70✔
409
                }
70✔
410
                else if (Compat.FloatRoundtripBroken)
17,195✔
411
                {
3,439✔
412
                    node.Add(new XText(val.ToString("G9")));
3,439✔
413
                }
3,439✔
414
                else
415
                {
13,756✔
416
                    node.Add(new XText(val.ToString()));
13,756✔
417
                }
13,756✔
418
            }
17,265✔
419
            else
420
            {
143,801✔
421
                node.Add(new XText(value.ToString()));
143,801✔
422
            }
143,801✔
423
        }
162,936✔
424

425
        public override void WriteEnum(object value)
426
        {
200✔
427
            node.Add(new XText(value.ToString()));
200✔
428
        }
200✔
429

430
        public override void WriteString(string value)
431
        {
18,490✔
432
            node.Add(new XText(value));
18,490✔
433
        }
18,490✔
434

435
        public override void WriteType(Type value)
436
        {
130✔
437
            node.Add(new XText(value.ComposeDecFormatted()));
130✔
438
        }
130✔
439

440
        public override void WriteDec(Dec value)
441
        {
25,200✔
442
            // Get the dec name and be done with it.
443
            if (value == null)
25,200✔
444
            {
22,635✔
445
                // "No data" is defined as null for decs, so we just do that
446
            }
22,635✔
447
            else if (value.DecName == "" || value.DecName == null)
2,565✔
448
            {
×
449
                Dbg.Err($"Attempted to write a Dec that was dynamically created but never registered; this will be left as a null reference. In most cases you shouldn't be dynamically creating Decs anyway, this is likely a malfunctioning deep copy such as a misbehaving ICloneable");
×
450
            }
×
451
            else if (value != Database.Get(value.GetType(), value.DecName))
2,565✔
452
            {
50✔
453
                Dbg.Err($"Referenced dec `{value}` does not exist in the database; serializing an error value instead");
50✔
454
                node.Add(new XText($"{value.DecName}_DELETED"));
50✔
455

456
                // if you actually have a dec named SomePreviouslyExistingDec_DELETED then you need to sort out what you're doing with your life
457
            }
50✔
458
            else
459
            {
2,515✔
460
                node.Add(new XText(value.DecName));
2,515✔
461
            }
2,515✔
462
        }
25,200✔
463

464
        public override void TagClass(Type type)
465
        {
596✔
466
            // I guess we just keep going? what's likely to be less damaging here? this may at least be manually reconstructible I suppose?
467
            FlagAsClass();
596✔
468

469
            node.Add(new XAttribute("class", type.ComposeDecFormatted()));
596✔
470
        }
596✔
471

472
        public override void WriteExplicitNull()
473
        {
184,102✔
474
            node.SetAttributeValue("null", "true");
184,102✔
475
        }
184,102✔
476

477
        public override bool WriteReference(object value)
478
        {
513,126✔
479
            return writer.RegisterReference(value, node, RecorderContext);
513,126✔
480
        }
513,126✔
481

482
        private void WriteArrayRank(WriterNodeXml node, Array value, Type referencedType, int rank, int[] indices)
483
        {
765✔
484
            if (rank == value.Rank)
765✔
485
            {
450✔
486
                Serialization.ComposeElement(node, value.GetValue(indices), referencedType);
450✔
487
            }
450✔
488
            else
489
            {
315✔
490
                for (int i = 0; i < value.GetLength(rank); ++i)
2,070✔
491
                {
720✔
492
                    var child = node.CreateNamedChild("li", RecorderContext.CreateChild());
720✔
493

494
                    indices[rank] = i;
720✔
495
                    WriteArrayRank(child, value, referencedType, rank + 1, indices);
720✔
496
                }
720✔
497
            }
315✔
498
        }
765✔
499

500
        public override void WriteArray(Array value)
501
        {
445✔
502
            Type referencedType = value.GetType().GetElementType();
445✔
503

504
            if (value.Rank == 1)
445✔
505
            {
400✔
506
                // fast path
507
                for (int i = 0; i < value.Length; ++i)
4,720✔
508
                {
1,960✔
509
                    Serialization.ComposeElement(CreateNamedChild("li", RecorderContext.CreateChild()), value.GetValue(i), referencedType);
1,960✔
510
                }
1,960✔
511

512
                return;
400✔
513
            }
514
            else
515
            {
45✔
516
                // slow path
517
                int[] indices = new int[value.Rank];
45✔
518
                WriteArrayRank(this, value, referencedType, 0, indices);
45✔
519
            }
45✔
520
        }
445✔
521

522
        public override void WriteList(IList value)
523
        {
17,150✔
524
            Type referencedType = value.GetType().GetGenericArguments()[0];
17,150✔
525

526
            for (int i = 0; i < value.Count; ++i)
111,300✔
527
            {
38,500✔
528
                Serialization.ComposeElement(CreateNamedChild("li", RecorderContext.CreateChild()), value[i], referencedType);
38,500✔
529
            }
38,500✔
530
        }
17,150✔
531

532
        public override void WriteDictionary(IDictionary value)
533
        {
7,820✔
534
            Type keyType = value.GetType().GetGenericArguments()[0];
7,820✔
535
            Type valueType = value.GetType().GetGenericArguments()[1];
7,820✔
536

537
            // I really want some way to canonicalize this ordering
538
            IDictionaryEnumerator iterator = value.GetEnumerator();
7,820✔
539
            while (iterator.MoveNext())
23,925✔
540
            {
16,105✔
541
                // In theory, some dicts support inline format, not li format. Inline format is cleaner and smaller and we should be using it when possible.
542
                // In practice, it's hard and I'm lazy and this always works, and we're not providing any guarantees about cleanliness of serialized output.
543
                // Revisit this later when someone (possibly myself) really wants it improved.
544
                var li = CreateNamedChild("li", RecorderContext);
16,105✔
545

546
                Serialization.ComposeElement(li.CreateNamedChild("key", RecorderContext.CreateChild()), iterator.Key, keyType);
16,105✔
547
                Serialization.ComposeElement(li.CreateNamedChild("value", RecorderContext.CreateChild()), iterator.Value, valueType);
16,105✔
548
            }
16,105✔
549
        }
7,820✔
550

551
        public override void WriteHashSet(IEnumerable value)
552
        {
380✔
553
            Type keyType = value.GetType().GetGenericArguments()[0];
380✔
554

555
            // I really want some way to canonicalize this ordering
556
            IEnumerator iterator = value.GetEnumerator();
380✔
557
            while (iterator.MoveNext())
1,840✔
558
            {
1,460✔
559
                // In theory, some sets support inline format, not li format. Inline format is cleaner and smaller and we should be using it when possible.
560
                // In practice, it's hard and I'm lazy and this always works, and we're not providing any guarantees about cleanliness of serialized output.
561
                // Revisit this later when someone (possibly myself) really wants it improved.
562
                Serialization.ComposeElement(CreateNamedChild("li", RecorderContext.CreateChild()), iterator.Current, keyType);
1,460✔
563
            }
1,460✔
564
        }
380✔
565

566
        public override void WriteQueue(IEnumerable value)
567
        {
30✔
568
            // We actually just treat this like an array right now; it's the same behavior and it's easier
569
            Type keyType = value.GetType().GetGenericArguments()[0];
30✔
570
            var array = value.GetType().GetMethod("ToArray").Invoke(value, new object[] { }) as Array;
30✔
571

572
            WriteArray(array);
30✔
573
        }
30✔
574

575
        public override void WriteStack(IEnumerable value)
576
        {
30✔
577
            // We actually just treat this like an array right now; it's the same behavior and it's easier
578
            Type keyType = value.GetType().GetGenericArguments()[0];
30✔
579
            var array = value.GetType().GetMethod("ToArray").Invoke(value, new object[] { }) as Array;
30✔
580

581
            // For some reason this writes it out to an array in the reverse order than I'd expect
582
            // (and also the reverse order it inputs in!)
583
            // so, uh, time to munge
584
            Array.Reverse(array);
30✔
585

586
            WriteArray(array);
30✔
587
        }
30✔
588

589
        public override void WriteTuple(object value, System.Runtime.CompilerServices.TupleElementNamesAttribute names)
590
        {
140✔
591
            var args = value.GetType().GenericTypeArguments;
140✔
592
            var length = args.Length;
140✔
593

594
            var nameArray = names?.TransformNames;
140✔
595

596
            for (int i = 0; i < length; ++i)
1,140✔
597
            {
430✔
598
                Serialization.ComposeElement(CreateNamedChild(nameArray != null ? nameArray[i] : "li", RecorderContext.CreateChild()), value.GetType().GetProperty(UtilMisc.DefaultTupleNames[i]).GetValue(value), args[i]);
430✔
599
            }
430✔
600
        }
140✔
601

602
        public override void WriteValueTuple(object value, System.Runtime.CompilerServices.TupleElementNamesAttribute names)
603
        {
250✔
604
            var args = value.GetType().GenericTypeArguments;
250✔
605
            var length = args.Length;
250✔
606

607
            var nameArray = names?.TransformNames;
250✔
608

609
            for (int i = 0; i < length; ++i)
1,800✔
610
            {
650✔
611
                Serialization.ComposeElement(CreateNamedChild(nameArray != null ? nameArray[i] : "li", RecorderContext.CreateChild()), value.GetType().GetField(UtilMisc.DefaultTupleNames[i]).GetValue(value), args[i]);
650✔
612
            }
650✔
613
        }
250✔
614

615
        public override void WriteRecord(IRecordable value)
616
        {
321,270✔
617
            if (depth < MaxRecursionDepth)
321,270✔
618
            {
318,285✔
619
                // This is somewhat faster than a full pending write (5-10% faster in one test case, though with a lot of noise), so we do it whenever we can.
620
                value.Record(new RecorderWriter(this));
318,285✔
621
            }
318,285✔
622
            else
623
            {
2,985✔
624
                // Reset depth because this will be run only when the pending writes are ready.
625
                depth = 0;
2,985✔
626
                writer.RegisterPendingWrite(() => WriteRecord(value));
5,970✔
627
            }
2,985✔
628
        }
321,270✔
629

630
        public override void WriteConvertible(Converter converter, object value)
631
        {
435✔
632
            // Convertibles are kind of a wildcard, so right now we're just changing this to Flexible mode
633
            MakeRecorderContextChild();
435✔
634

635
            if (depth < MaxRecursionDepth)
435✔
636
            {
435✔
637
                try
638
                {
435✔
639
                    if (converter is ConverterString converterString)
435✔
640
                    {
150✔
641
                        WriteString(converterString.WriteObj(value));
150✔
642
                    }
150✔
643
                    else if (converter is ConverterRecord converterRecord)
285✔
644
                    {
150✔
645
                        converterRecord.RecordObj(value, new RecorderWriter(this));
150✔
646
                    }
150✔
647
                    else if (converter is ConverterFactory converterFactory)
135✔
648
                    {
135✔
649
                        converterFactory.WriteObj(value, new RecorderWriter(this));
135✔
650
                    }
135✔
651
                    else
652
                    {
×
653
                        Dbg.Err($"Somehow ended up with an unsupported converter {converter.GetType()}");
×
654
                    }
×
655
                }
435✔
656
                catch (Exception e)
×
657
                {
×
658
                    Dbg.Ex(e);
×
659
                }
×
660
            }
435✔
661
            else
662
            {
×
663
                // Reset depth because this will be run only when the pending writes are ready.
664
                depth = 0;
×
665
                writer.RegisterPendingWrite(() => WriteConvertible(converter, value));
×
666
            }
×
667
        }
435✔
668

669
        internal XElement GetXElement()
670
        {
16,880✔
671
            return node;
16,880✔
672
        }
16,880✔
673
    }
674
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc