1
using System;
2
using System.Collections.Generic;
3
using System.Linq;
4
using Libplanet.Action;
5
using Libplanet.Blockchain.Renderers;
6
using Libplanet.Blocks;
7

8
namespace Libplanet.Tests.Common
9
{
10
    /// <summary>
11
    /// Validates if rendering events are in the correct order according to the documented automata
12
    /// (see also the docs for <see cref="IRenderer{T}"/> and <see cref="IActionRenderer{T}"/>)
13
    /// using profiling-guided analysis.
14
    /// </summary>
15
    /// <typeparam name="T">An <see cref="IAction"/> type.  It should match to
16
    /// <see cref="Libplanet.Blockchain.BlockChain{T}"/>'s type parameter.</typeparam>
17
    public class ValidatingActionRenderer<T> : RecordingRenderer<T>
18
        where T : IAction, new()
19
    {
20
        private enum RenderState
21
        {
22
            Ready,
23
            Reorg,
24
            Block,
25
            BlockEnd,
26
        }
27

28
        public override void RenderReorg(Block<T> oldTip, Block<T> newTip, Block<T> branchpoint)
29 1
        {
30 1
            base.RenderReorg(oldTip, newTip, branchpoint);
31 1
            Validate();
32 1
        }
33

34
        public override void UnrenderAction(
35
            IAction action,
36
            IActionContext context,
37
            IAccountStateDelta nextStates
38
        )
39 1
        {
40 1
            base.UnrenderAction(action, context, nextStates);
41 1
            Validate();
42 1
        }
43

44
        public override void UnrenderActionError(
45
            IAction action,
46
            IActionContext context,
47
            Exception exception
48
        )
49 0
        {
50 0
            base.UnrenderActionError(action, context, exception);
51 0
            Validate();
52 0
        }
53

54
        public override void RenderBlock(Block<T> oldTip, Block<T> newTip)
55 2
        {
56 2
            base.RenderBlock(oldTip, newTip);
57 2
            Validate();
58 2
        }
59

60
        public override void RenderAction(
61
            IAction action,
62
            IActionContext context,
63
            IAccountStateDelta nextStates
64
        )
65 2
        {
66 2
            base.RenderAction(action, context, nextStates);
67 2
            Validate();
68 2
        }
69

70
        public override void RenderActionError(
71
            IAction action,
72
            IActionContext context,
73
            Exception exception
74
        )
75 1
        {
76 1
            base.RenderActionError(action, context, exception);
77 1
            Validate();
78 1
        }
79

80
        public override void RenderBlockEnd(Block<T> oldTip, Block<T> newTip)
81 2
        {
82 2
            base.RenderBlockEnd(oldTip, newTip);
83 2
            Validate();
84 2
        }
85

86
        public override void RenderReorgEnd(
87
            Block<T> oldTip,
88
            Block<T> newTip,
89
            Block<T> branchpoint
90
        )
91 1
        {
92 1
            base.RenderReorgEnd(oldTip, newTip, branchpoint);
93 1
            Validate();
94 1
        }
95

96
        private void Validate()
97 2
        {
98 2
            var state = RenderState.Ready;
99 2
            RenderRecord<T>.Reorg reorgState = null;
100 2
            RenderRecord<T>.Block blockState = null;
101 2
            long previousActionBlockIndex = -1L;
102 2
            var records = new List<RenderRecord<T>>(Records.Count);
103

104 0
            Exception BadRenderExc(string message) => new InvalidRenderException(records, message);
105

106
#pragma warning disable S2589
107 2
            foreach (RenderRecord<T> record in Records)
108 2
            {
109 2
                records.Add(record);
110 2
                switch (state)
111
                {
112
                    case RenderState.Ready:
113 2
                    {
114 2
                        if (!(reorgState is null && blockState is null))
115 0
                        {
116 0
                            throw BadRenderExc(
117 0
                                $"Unexpected reorg/block states: {reorgState}/{blockState}."
118 0
                            );
119
                        }
120 2
                        else if (record is RenderRecord<T>.BlockBase blockBase && blockBase.Begin)
121 2
                        {
122 2
                            if (blockBase is RenderRecord<T>.Reorg reorg)
123 1
                            {
124 1
                                reorgState = reorg;
125 1
                                state = RenderState.Reorg;
126 1
                                break;
127
                            }
128 2
                            else if (blockBase is RenderRecord<T>.Block block)
129 2
                            {
130 2
                                blockState = block;
131 2
                                state = RenderState.Block;
132 2
                                break;
133
                            }
134 0
                        }
135

136 0
                        throw BadRenderExc(
137 0
                            $"Expected {nameof(IRenderer<T>.RenderReorg)} or " +
138 0
                            $"{nameof(IRenderer<T>.RenderBlock)}."
139 0
                        );
140
                    }
141

142
                    case RenderState.Reorg:
143 1
                    {
144 1
                        if (reorgState is null || !(blockState is null))
145 0
                        {
146 0
                            throw BadRenderExc(
147 0
                                $"Unexpected reorg/block states: {reorgState}/{blockState}."
148 0
                            );
149
                        }
150 1
                        else if (record is RenderRecord<T>.Block block && block.Begin)
151 1
                        {
152 1
                            if (block.OldTip != reorgState.OldTip ||
153 1
                                block.NewTip != reorgState.NewTip)
154 0
                            {
155 0
                                throw BadRenderExc(
156 0
                                    $"{nameof(IRenderer<T>.RenderReorg)} and " +
157 0
                                    $"{nameof(IRenderer<T>.RenderBlock)} which follows it should " +
158 0
                                    "have the same oldTip and newTip."
159 0
                                );
160
                            }
161

162 1
                            blockState = block;
163 1
                            state = RenderState.Block;
164 1
                            break;
165
                        }
166 1
                        else if (record is RenderRecord<T>.ActionBase actionBase &&
167 1
                                 actionBase.Unrender)
168 1
                        {
169 1
                            long idx = actionBase.Context.BlockIndex;
170 1
                            long minIdx = reorgState.Branchpoint.Index + 1;
171 1
                            long maxIdx = previousActionBlockIndex < 0
172 1
                                ? reorgState.OldTip.Index
173 1
                                : previousActionBlockIndex;
174 1
                            if (idx < minIdx || idx > maxIdx)
175 0
                            {
176 0
                                throw BadRenderExc(
177 0
                                    "An action is from a block which has an unexpected index " +
178 0
                                    $"#{idx} (expected min: #{minIdx}; max: #{maxIdx})."
179 0
                                );
180
                            }
181

182 1
                            previousActionBlockIndex = idx;
183 1
                            break;
184
                        }
185

186 0
                        throw BadRenderExc(
187 0
                            $"Expected {nameof(IRenderer<T>.RenderBlock)} or " +
188 0
                            $"{nameof(IActionRenderer<T>.UnrenderAction)} or " +
189 0
                            $"{nameof(IActionRenderer<T>.UnrenderActionError)}."
190 0
                        );
191
                    }
192

193
                    case RenderState.Block:
194 2
                    {
195 2
                        if (blockState is null)
196 0
                        {
197 0
                            throw BadRenderExc("Unexpected block state: null.");
198
                        }
199 2
                        else if (record is RenderRecord<T>.Block block && block.End)
200 2
                        {
201 2
                            if (block.OldTip != blockState.OldTip ||
202 2
                                block.NewTip != blockState.NewTip)
203 0
                            {
204 0
                                throw BadRenderExc(
205 0
                                    $"{nameof(IRenderer<T>.RenderBlock)} and " +
206 0
                                    $"{nameof(IActionRenderer<T>.RenderBlockEnd)} which matches " +
207 0
                                    "to it should have the same oldTip and newTip."
208 0
                                );
209
                            }
210

211 2
                            state = reorgState is null ? RenderState.Ready : RenderState.BlockEnd;
212 2
                            blockState = null;
213 2
                            break;
214
                        }
215 2
                        else if (record is RenderRecord<T>.ActionBase actionBase &&
216 2
                                 actionBase.Render)
217 2
                        {
218 2
                            long idx = actionBase.Context.BlockIndex;
219 2
                            if (reorgState is RenderRecord<T>.Reorg reorg)
220 1
                            {
221 1
                                long minIdx = previousActionBlockIndex >= 0
222 1
                                    ? previousActionBlockIndex
223 1
                                    : reorg.Branchpoint.Index + 1;
224 1
                                long maxIdx = reorg.NewTip.Index;
225 1
                                if (idx < minIdx || idx > maxIdx)
226 0
                                {
227 0
                                    throw BadRenderExc(
228 0
                                        "An action is from a block which has an unexpected index " +
229 0
                                        $"#{idx} (expected min: #{minIdx}; max: #{maxIdx})."
230 0
                                    );
231
                                }
232 1
                            }
233 2
                            else if (idx != blockState.NewTip.Index)
234 0
                            {
235 0
                                throw BadRenderExc(
236 0
                                    "An action is from a block which has an unexpected index " +
237 0
                                    $"#{idx} (expected: #{blockState.NewTip.Index}."
238 0
                                );
239
                            }
240

241 2
                            previousActionBlockIndex = idx;
242 2
                            break;
243
                        }
244

245 0
                        throw BadRenderExc(
246 0
                            $"Expected {nameof(IActionRenderer<T>.RenderBlockEnd)} or " +
247 0
                            $"{nameof(IActionRenderer<T>.RenderAction)} or " +
248 0
                            $"{nameof(IActionRenderer<T>.RenderActionError)}"
249 0
                        );
250
                    }
251

252
                    case RenderState.BlockEnd:
253 1
                    {
254 1
                        if (reorgState is null || !(blockState is null))
255 0
                        {
256 0
                            throw BadRenderExc(
257 0
                                $"Unexpected reorg/block states: {reorgState}/{blockState}."
258 0
                            );
259
                        }
260 1
                        else if (record is RenderRecord<T>.Reorg reorg && reorg.End)
261 1
                        {
262 1
                            if (reorg.OldTip != reorgState.OldTip ||
263 1
                                reorg.NewTip != reorgState.NewTip ||
264 1
                                reorg.Branchpoint != reorgState.Branchpoint)
265 0
                            {
266 0
                                throw BadRenderExc(
267 0
                                    $"{nameof(IRenderer<T>.RenderReorgEnd)} should match to " +
268 0
                                    $"{nameof(IActionRenderer<T>.RenderReorg)}; they should have " +
269 0
                                    "the same oldTip, newTip, and branchpoint."
270 0
                                );
271
                            }
272

273 1
                            state = RenderState.Ready;
274 1
                            reorgState = null;
275 1
                            break;
276
                        }
277

278 0
                        throw BadRenderExc(
279 0
                            $"Expected {nameof(IActionRenderer<T>.RenderReorgEnd)}."
280 0
                        );
281
                    }
282
                }
283 2
            }
284
#pragma warning restore S2589
285 2
        }
286

287
        public class InvalidRenderException : Exception
288
        {
289
            public InvalidRenderException(
290
                IReadOnlyList<RenderRecord<T>> records,
291
                string message
292
            )
293 0
                : base(message)
294 0
            {
295 0
                Records = records;
296 0
            }
297

298 0
            public IReadOnlyList<RenderRecord<T>> Records { get; }
299

300
            public override string Message
301
            {
302
                get
303 0
                {
304
                    string MakeCompact(string stackTrace)
305 0
                    {
306 0
                        int pos = 0;
307 0
                        for (int i = 0; pos >= 0 && i < 10; i++)
308 0
                        {
309 0
                            pos = stackTrace.IndexOf('\n', pos + 1);
310 0
                        }
311

312 0
                        return pos < 0
313 0
                            ? stackTrace
314 0
                            : stackTrace.Substring(0, pos);
315 0
                    }
316

317 0
                    string pre = base.Message;
318 0
                    if (Records.Count < 1)
319 0
                    {
320 0
                        return pre + "\n(0 records.)";
321
                    }
322

323 0
                    RenderRecord<T> first = Records[Records.Count - 1];
324 0
                    var firstLine = $"{pre}\n{first}";
325 0
                    if (Records.Count < 2)
326 0
                    {
327 0
                        return $"{firstLine}\n{MakeCompact(first.StackTrace)}\n(1 record.)";
328
                    }
329

330
                    // Find common postfix
331 0
                    string firstTrace = first.StackTrace;
332 0
                    int commonPostfix = 0;
333 0
                    for (int i = 0, end = Records.Min(r => r.StackTrace.Length); i < end; i++)
334 0
                    {
335 0
                        char charInFirst = firstTrace[StackTrace.Length - (i + 1)];
336 0
                        bool allEqual = Records.Skip(1).All(r =>
337 0
                        {
338 0
                            string stackTrace = r.StackTrace;
339 0
                            char charFromEnd = stackTrace[stackTrace.Length - (i + 1)];
340 0
                            return charFromEnd.Equals(charInFirst);
341 0
                        });
342

343 0
                        if (!allEqual)
344 0
                        {
345 0
                            commonPostfix = i;
346 0
                            break;
347
                        }
348 0
                    }
349

350 0
                    firstTrace =
351 0
                        MakeCompact(firstTrace.Substring(0, firstTrace.Length - commonPostfix));
352 0
                    firstLine += $"\n{firstTrace}";
353 0
                    RenderRecord<T> second = Records[Records.Count - 2];
354 0
                    IEnumerable<RenderRecord<T>> rest = Records.Reverse().Skip(2);
355 0
                    string secondTrace = second.StackTrace;
356 0
                    string secondCompactTrace =
357 0
                        MakeCompact(secondTrace.Substring(0, secondTrace.Length - commonPostfix));
358 0
                    return $"{firstLine}\n{second}\n{secondCompactTrace}\n" +
359 0
                           string.Join("\n", rest) + $"\n({Records.Count} records.)";
360 0
                }
361
            }
362
        }
363
    }
364
}

Read our documentation on viewing source code .

Loading