|
19 | 19 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
20 | 20 | // SOFTWARE. |
21 | 21 |
|
| 22 | +using System; |
22 | 23 | using System.Collections.Generic; |
23 | 24 | using System.IO; |
| 25 | +using System.Text; |
| 26 | +using System.Threading; |
| 27 | +using System.Threading.Tasks; |
24 | 28 | using FluentAssertions; |
25 | 29 | using Xunit; |
26 | 30 | using YamlDotNet.Core; |
@@ -151,5 +155,134 @@ public void MergingParserWithNestedSequence_ShouldNotThrowException() |
151 | 155 |
|
152 | 156 | new SerializerBuilder().Build().Serialize(yamlObject!).NormalizeNewLines().Should().Be(etalonMergedYaml); |
153 | 157 | } |
| 158 | + |
| 159 | + [Fact] |
| 160 | + public async Task MergingParserWithMergeKeyBomb_ShouldThrowExceptionWhenTooManyEvents() |
| 161 | + { |
| 162 | + // Timebox this test to avoid infinite loops in case of bugs. |
| 163 | + // 30 seconds should be more than enough for this test to run even on a slow machine, and if it takes longer than that, |
| 164 | + // it's likely that the merging parser is not correctly counting events and enforcing the limit. |
| 165 | + var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); |
| 166 | + cancellationTokenSource.Token.Register(() => |
| 167 | + { |
| 168 | + throw new TimeoutException("The test took too long, likely due to an infinite loop in the merging parser."); |
| 169 | + }); |
| 170 | + |
| 171 | + await Task.Run(() => |
| 172 | + { |
| 173 | + var sb = new StringBuilder(); |
| 174 | + |
| 175 | + // Base anchor |
| 176 | + sb.AppendLine("a0: &a0"); |
| 177 | + sb.AppendLine(" x: 1"); |
| 178 | + sb.AppendLine(); |
| 179 | + |
| 180 | + // Each level merges the previous anchor TWICE (fanout=2), doubling event count |
| 181 | + for (int i = 1; i <= 25; i++) |
| 182 | + { |
| 183 | + sb.AppendLine($"a{i}: &a{i}"); |
| 184 | + sb.AppendLine($" <<: *a{i - 1}"); // first merge |
| 185 | + sb.AppendLine($" <<: *a{i - 1}"); // second merge |
| 186 | + sb.AppendLine(); |
| 187 | + } |
| 188 | + |
| 189 | + sb.AppendLine("final:"); |
| 190 | + sb.AppendLine(" <<: *a25"); |
| 191 | + |
| 192 | + var yaml = sb.ToString(); |
| 193 | + var parser = new Parser(new StringReader(yaml)); |
| 194 | + var mergingParser = new MergingParser(parser, 1000); |
| 195 | + try |
| 196 | + { |
| 197 | + while (mergingParser.MoveNext()) |
| 198 | + { |
| 199 | + //move through everything, we're in a timebox so if this takes too long, the cancellation token will trigger and fail the test |
| 200 | + } |
| 201 | + } |
| 202 | + catch (YamlException ex) when (ex.Message.Contains("Too many parsing events")) |
| 203 | + { |
| 204 | + // Expected exception, test passes |
| 205 | + return; |
| 206 | + } |
| 207 | + catch (Exception ex) |
| 208 | + { |
| 209 | + throw new Exception($"Unexpected exception", ex); |
| 210 | + } |
| 211 | + }, cancellationTokenSource.Token); |
| 212 | + } |
| 213 | + |
| 214 | + [Fact] |
| 215 | + public async Task MergingParserWithManySmallMerges_ShouldThrowExceptionWhenCumulativeEventsExceedLimit() |
| 216 | + { |
| 217 | + // Timebox this test to avoid infinite loops in case of bugs. |
| 218 | + // 30 seconds should be more than enough for this test to run even on a slow machine, and if it takes longer than that, |
| 219 | + // it's likely that the merging parser is not correctly counting events and enforcing the limit. |
| 220 | + var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); |
| 221 | + cancellationTokenSource.Token.Register(() => |
| 222 | + { |
| 223 | + throw new TimeoutException("The test took too long, likely due to an infinite loop in the merging parser."); |
| 224 | + }); |
| 225 | + |
| 226 | + await Task.Run(() => |
| 227 | + { |
| 228 | + var sb = new StringBuilder(); |
| 229 | + sb.AppendLine("base: &base"); |
| 230 | + for (var i = 0; i < 25; i++) |
| 231 | + { |
| 232 | + sb.AppendLine($" k{i}: v{i}"); |
| 233 | + } |
| 234 | + |
| 235 | + sb.AppendLine(); |
| 236 | + for (var i = 0; i < 35; i++) |
| 237 | + { |
| 238 | + sb.AppendLine($"entry{i}:"); |
| 239 | + sb.AppendLine(" <<: *base"); |
| 240 | + sb.AppendLine(); |
| 241 | + } |
| 242 | + |
| 243 | + var parser = new Parser(new StringReader(sb.ToString())); |
| 244 | + var mergingParser = new MergingParser(parser, 1000); |
| 245 | + |
| 246 | + Action parse = () => |
| 247 | + { |
| 248 | + while (mergingParser.MoveNext()) |
| 249 | + { |
| 250 | + } |
| 251 | + }; |
| 252 | + |
| 253 | + parse.Should().Throw<YamlException>() |
| 254 | + .Where(ex => ex.Message.Contains("Too many parsing events")); |
| 255 | + }, cancellationTokenSource.Token); |
| 256 | + } |
| 257 | + |
| 258 | + [Fact] |
| 259 | + public void MergingParserWithDeepSingleChain_ShouldParseWithinLimit() |
| 260 | + { |
| 261 | + const int depth = 200; |
| 262 | + var sb = new StringBuilder(); |
| 263 | + |
| 264 | + sb.AppendLine("a0: &a0"); |
| 265 | + sb.AppendLine(" root: value"); |
| 266 | + sb.AppendLine(); |
| 267 | + |
| 268 | + for (var i = 1; i <= depth; i++) |
| 269 | + { |
| 270 | + sb.AppendLine($"a{i}: &a{i}"); |
| 271 | + sb.AppendLine($" <<: *a{i - 1}"); |
| 272 | + sb.AppendLine($" level{i}: {i}"); |
| 273 | + sb.AppendLine(); |
| 274 | + } |
| 275 | + |
| 276 | + sb.AppendLine("final:"); |
| 277 | + sb.AppendLine($" <<: *a{depth}"); |
| 278 | + |
| 279 | + var parser = new Parser(new StringReader(sb.ToString())); |
| 280 | + var mergingParser = new MergingParser(parser, 50000); |
| 281 | + var deserializer = new DeserializerBuilder().Build(); |
| 282 | + |
| 283 | + var yamlObject = deserializer.Deserialize<Dictionary<string, object>>(mergingParser); |
| 284 | + |
| 285 | + yamlObject.Should().ContainKey("final"); |
| 286 | + } |
154 | 287 | } |
155 | 288 | } |
0 commit comments