rich-ruby 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,604 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Comprehensive stress tests for Rich library
4
+ # These tests push every component to its limits
5
+
6
+ require_relative "../lib/rich"
7
+
8
+ class StressTest
9
+ attr_reader :name, :passed, :error, :duration
10
+
11
+ def initialize(name, &block)
12
+ @name = name
13
+ @block = block
14
+ @passed = false
15
+ @error = nil
16
+ @duration = 0
17
+ end
18
+
19
+ def run
20
+ start = Time.now
21
+ begin
22
+ @block.call
23
+ @passed = true
24
+ rescue StandardError => e
25
+ @error = e
26
+ @passed = false
27
+ end
28
+ @duration = Time.now - start
29
+ self
30
+ end
31
+ end
32
+
33
+ class StressTestSuite
34
+ def initialize
35
+ @tests = []
36
+ @console = Rich::Console.new
37
+ end
38
+
39
+ def test(name, &block)
40
+ @tests << StressTest.new(name, &block)
41
+ end
42
+
43
+ def run_all
44
+ puts "=" * 70
45
+ puts "Rich Library Stress Test Suite"
46
+ puts "=" * 70
47
+ puts ""
48
+
49
+ @tests.each do |t|
50
+ print " #{t.name.ljust(50)}... "
51
+ t.run
52
+ if t.passed
53
+ puts "✓ PASS (#{format('%.3f', t.duration)}s)"
54
+ else
55
+ puts "✗ FAIL"
56
+ puts " Error: #{t.error.message}"
57
+ puts " #{t.error.backtrace.first}"
58
+ end
59
+ end
60
+
61
+ passed = @tests.count(&:passed)
62
+ total = @tests.length
63
+ total_time = @tests.sum(&:duration)
64
+
65
+ puts ""
66
+ puts "=" * 70
67
+ puts "Results: #{passed}/#{total} tests passed in #{format('%.2f', total_time)}s"
68
+ puts "=" * 70
69
+
70
+ passed == total
71
+ end
72
+ end
73
+
74
+ suite = StressTestSuite.new
75
+
76
+ # =============================================================================
77
+ # PART 1: COLOR SYSTEM STRESS TESTS
78
+ # =============================================================================
79
+
80
+ suite.test("Parse all 256 ANSI color names") do
81
+ Rich::ANSI_COLOR_NAMES.each do |name, number|
82
+ color = Rich::Color.parse(name)
83
+ raise "#{name} parsed wrong" unless color.number == number
84
+ end
85
+ end
86
+
87
+ suite.test("Parse 10,000 random hex colors") do
88
+ 10_000.times do
89
+ r = rand(256)
90
+ g = rand(256)
91
+ b = rand(256)
92
+ hex = format("#%02x%02x%02x", r, g, b)
93
+ color = Rich::Color.parse(hex)
94
+ raise "Hex parse failed" unless color.triplet.red == r
95
+ end
96
+ end
97
+
98
+ suite.test("Color downgrade from truecolor to 256") do
99
+ 1000.times do
100
+ triplet = Rich::ColorTriplet.new(rand(256), rand(256), rand(256))
101
+ color = Rich::Color.from_triplet(triplet)
102
+ downgraded = color.downgrade(Rich::ColorSystem::EIGHT_BIT)
103
+ raise "Downgrade failed" unless downgraded.type == Rich::ColorType::EIGHT_BIT
104
+ end
105
+ end
106
+
107
+ suite.test("Color downgrade from truecolor to 16") do
108
+ 1000.times do
109
+ triplet = Rich::ColorTriplet.new(rand(256), rand(256), rand(256))
110
+ color = Rich::Color.from_triplet(triplet)
111
+ downgraded = color.downgrade(Rich::ColorSystem::STANDARD)
112
+ raise "Downgrade failed" unless downgraded.number.between?(0, 15)
113
+ end
114
+ end
115
+
116
+ suite.test("ColorTriplet HSL roundtrip") do
117
+ 100.times do
118
+ h = rand(360)
119
+ s = rand(100)
120
+ l = rand(100)
121
+ triplet = Rich::ColorTriplet.from_hsl(h, s, l)
122
+ raise "HSL failed" unless triplet.red.between?(0, 255)
123
+ end
124
+ end
125
+
126
+ suite.test("Color parse caching performance") do
127
+ # Parse same colors many times - should be cached
128
+ colors = %w[red green blue yellow magenta cyan white black]
129
+ 10_000.times do
130
+ colors.each { |c| Rich::Color.parse(c) }
131
+ end
132
+ end
133
+
134
+ # =============================================================================
135
+ # PART 2: STYLE SYSTEM STRESS TESTS
136
+ # =============================================================================
137
+
138
+ suite.test("Parse complex style definitions") do
139
+ styles = [
140
+ "bold italic underline red on blue",
141
+ "dim reverse strike conceal",
142
+ "not bold underline2 frame encircle overline",
143
+ "bright_magenta on bright_cyan blink",
144
+ "#ff5500 on #00ff55 bold italic"
145
+ ]
146
+ styles.each do |s|
147
+ style = Rich::Style.parse(s)
148
+ raise "Parse failed for: #{s}" if style.nil?
149
+ end
150
+ end
151
+
152
+ suite.test("Style combination chain (1000 styles)") do
153
+ base = Rich::Style.parse("bold")
154
+ 1000.times do |i|
155
+ color_style = Rich::Style.parse("color(#{i % 256})")
156
+ base = base + color_style
157
+ end
158
+ raise "Combination failed" unless base.bold?
159
+ end
160
+
161
+ suite.test("Style attribute bitmask integrity") do
162
+ Rich::StyleAttribute::NAMES.each do |attr|
163
+ style = Rich::Style.new(**{ attr => true })
164
+ raise "#{attr} not set" unless style.send("#{attr}?")
165
+ end
166
+ end
167
+
168
+ suite.test("Style render with all attributes") do
169
+ style = Rich::Style.new(
170
+ color: "red",
171
+ bgcolor: "blue",
172
+ bold: true,
173
+ italic: true,
174
+ underline: true,
175
+ strike: true
176
+ )
177
+ rendered = style.render
178
+ raise "Render empty" if rendered.empty?
179
+ raise "No escape" unless rendered.include?("\e[")
180
+ end
181
+
182
+ # =============================================================================
183
+ # PART 3: UNICODE AND CELL WIDTH STRESS TESTS
184
+ # =============================================================================
185
+
186
+ suite.test("CJK character width calculation") do
187
+ cjk_strings = [
188
+ "你好世界", # Chinese
189
+ "こんにちは", # Japanese Hiragana
190
+ "안녕하세요", # Korean
191
+ "漢字カタカナ한글", # Mixed
192
+ "🎉🎊🎁🎄🎅" # Emoji
193
+ ]
194
+ cjk_strings.each do |s|
195
+ width = Rich::Cells.cell_len(s)
196
+ # CJK and emoji are generally 2 cells wide
197
+ raise "Width wrong for: #{s}" unless width >= s.length
198
+ end
199
+ end
200
+
201
+ suite.test("Zero-width combining characters") do
202
+ # Combining marks should have zero width
203
+ combining = "é" # e + combining acute
204
+ base_width = Rich::Cells.cell_len("e")
205
+ # The combined char width should account for combining marks
206
+ combined_width = Rich::Cells.cell_len(combining)
207
+ raise "Combining failed" if combined_width < 0
208
+ end
209
+
210
+ suite.test("Mixed ASCII and Unicode") do
211
+ text = "Hello 你好 World 世界 123"
212
+ width = Rich::Cells.cell_len(text)
213
+ # ASCII=14, CJK=4chars*2=8, total should be 22+
214
+ raise "Mixed width failed" unless width >= 20
215
+ end
216
+
217
+ suite.test("Large Unicode string (10KB)") do
218
+ # Generate 10KB of mixed Unicode
219
+ chars = "ABCあいう你好世界🎉".chars
220
+ text = (0...10_000).map { chars.sample }.join
221
+ width = Rich::Cells.cell_len(text)
222
+ raise "Large string failed" unless width > 0
223
+ end
224
+
225
+ suite.test("Empty and whitespace strings") do
226
+ raise "Empty failed" unless Rich::Cells.cell_len("") == 0
227
+ raise "Space failed" unless Rich::Cells.cell_len(" ") == 1
228
+ raise "Tab failed" unless Rich::Cells.cell_len("\t") == 1
229
+ raise "Newline failed" unless Rich::Cells.cell_len("\n") == 1
230
+ end
231
+
232
+ # =============================================================================
233
+ # PART 4: SEGMENT SYSTEM STRESS TESTS
234
+ # =============================================================================
235
+
236
+ suite.test("Segment split at every position") do
237
+ text = "Hello World"
238
+ style = Rich::Style.parse("bold red")
239
+ segment = Rich::Segment.new(text, style: style)
240
+
241
+ (0..text.length).each do |pos|
242
+ before, after = segment.split_cells(pos)
243
+ combined = before.text + after.text
244
+ raise "Split failed at #{pos}" unless combined == text
245
+ end
246
+ end
247
+
248
+ suite.test("Segment line splitting with many newlines") do
249
+ segments = [
250
+ Rich::Segment.new("Line1\nLine2\nLine3\n\nLine5"),
251
+ Rich::Segment.new("More\nLines\nHere")
252
+ ]
253
+ lines = Rich::Segment.split_lines(segments)
254
+ raise "Line count wrong" unless lines.length >= 6
255
+ end
256
+
257
+ suite.test("Segment simplification (1000 segments)") do
258
+ style = Rich::Style.parse("bold")
259
+ segments = 1000.times.map { Rich::Segment.new("x", style: style) }
260
+ simplified = Rich::Segment.simplify(segments)
261
+ raise "Not simplified" unless simplified.length < segments.length
262
+ end
263
+
264
+ suite.test("Segment rendering with control codes") do
265
+ segments = [
266
+ Rich::Segment.control([[Rich::Control::ControlType::HIDE_CURSOR]]),
267
+ Rich::Segment.new("Content", style: Rich::Style.parse("green")),
268
+ Rich::Segment.control([[Rich::Control::ControlType::SHOW_CURSOR]])
269
+ ]
270
+ output = Rich::Segment.render(segments)
271
+ raise "Control not rendered" unless output.include?("\e[?25l")
272
+ end
273
+
274
+ # =============================================================================
275
+ # PART 5: TEXT AND MARKUP STRESS TESTS
276
+ # =============================================================================
277
+
278
+ suite.test("Text with 1000 overlapping spans") do
279
+ text = Rich::Text.new("A" * 1000)
280
+ 1000.times do |i|
281
+ text.stylize("bold", i, i + 50)
282
+ end
283
+ segments = text.to_segments
284
+ raise "No segments" if segments.empty?
285
+ end
286
+
287
+ suite.test("Deeply nested markup") do
288
+ nested = "[bold][italic][underline][red][on blue]Deep[/on blue][/red][/underline][/italic][/bold]"
289
+ text = Rich::Markup.parse(nested)
290
+ raise "Parse failed" if text.plain != "Deep"
291
+ end
292
+
293
+ suite.test("Markup validation with errors") do
294
+ invalid = "[bold]unclosed"
295
+ errors = Rich::Markup.validate(invalid)
296
+ raise "Should have errors" if errors.empty?
297
+
298
+ valid = "[bold]closed[/bold]"
299
+ errors = Rich::Markup.validate(valid)
300
+ raise "Should be valid" unless errors.empty?
301
+ end
302
+
303
+ suite.test("Text wrapping at various widths") do
304
+ text = Rich::Text.new("Lorem ipsum dolor sit amet, consectetur adipiscing elit. " * 10)
305
+ [10, 20, 40, 80, 120].each do |width|
306
+ wrapped = text.wrap(width)
307
+ raise "Wrap failed at #{width}" if wrapped.empty?
308
+ end
309
+ end
310
+
311
+ suite.test("Text with special characters") do
312
+ specials = "Tab:\tNewline:\nCarriage:\rBackslash:\\"
313
+ text = Rich::Text.new(specials)
314
+ text.stylize("bold", 0, specials.length)
315
+ segments = text.to_segments
316
+ raise "Special chars failed" if segments.empty?
317
+ end
318
+
319
+ # =============================================================================
320
+ # PART 6: LAYOUT COMPONENT STRESS TESTS
321
+ # =============================================================================
322
+
323
+ suite.test("Panel with very long content") do
324
+ content = "X" * 1000
325
+ panel = Rich::Panel.new(content, title: "Long Content")
326
+ output = panel.render(max_width: 80)
327
+ raise "Panel failed" if output.empty?
328
+ end
329
+
330
+ suite.test("Panel with Unicode borders and content") do
331
+ content = "こんにちは世界"
332
+ panel = Rich::Panel.new(content, title: "日本語", box: Rich::Box::DOUBLE)
333
+ output = panel.render(max_width: 40)
334
+ raise "Unicode panel failed" if output.empty?
335
+ end
336
+
337
+ suite.test("Table with 100 rows") do
338
+ table = Rich::Table.new(title: "Large Table")
339
+ table.add_column("ID", justify: :right)
340
+ table.add_column("Name")
341
+ table.add_column("Value", justify: :center)
342
+
343
+ 100.times do |i|
344
+ table.add_row(i.to_s, "Item #{i}", format("%.2f", rand * 1000))
345
+ end
346
+
347
+ output = table.render(max_width: 80)
348
+ raise "Large table failed" if output.empty?
349
+ end
350
+
351
+ suite.test("Table with Unicode content") do
352
+ table = Rich::Table.new(box: Rich::Box::HEAVY)
353
+ table.add_column("Language")
354
+ table.add_column("Greeting")
355
+ table.add_row("日本語", "こんにちは")
356
+ table.add_row("中文", "你好")
357
+ table.add_row("한국어", "안녕하세요")
358
+ table.add_row("Emoji", "👋🌍🎉")
359
+
360
+ output = table.render(max_width: 60)
361
+ raise "Unicode table failed" if output.empty?
362
+ end
363
+
364
+ suite.test("Tree with deep nesting (10 levels)") do
365
+ tree = Rich::Tree.new("Root")
366
+ current = tree.root
367
+ 10.times do |i|
368
+ current = current.add("Level #{i + 1}")
369
+ end
370
+ output = tree.render
371
+ raise "Deep tree failed" unless output.include?("Level 10")
372
+ end
373
+
374
+ suite.test("Tree with many siblings (100)") do
375
+ tree = Rich::Tree.new("Root")
376
+ 100.times do |i|
377
+ tree.add("Child #{i}")
378
+ end
379
+ output = tree.render
380
+ raise "Wide tree failed" unless output.lines.length >= 100
381
+ end
382
+
383
+ suite.test("All box styles render correctly") do
384
+ boxes = [
385
+ Rich::Box::ASCII,
386
+ Rich::Box::SQUARE,
387
+ Rich::Box::ROUNDED,
388
+ Rich::Box::HEAVY,
389
+ Rich::Box::DOUBLE,
390
+ Rich::Box::MINIMAL,
391
+ Rich::Box::SIMPLE
392
+ ]
393
+ boxes.each do |box|
394
+ panel = Rich::Panel.new("Content", box: box)
395
+ output = panel.render(max_width: 30)
396
+ raise "Box style failed" if output.empty?
397
+ end
398
+ end
399
+
400
+ # =============================================================================
401
+ # PART 7: PROGRESS AND ANIMATION STRESS TESTS
402
+ # =============================================================================
403
+
404
+ suite.test("Progress bar at every percentage") do
405
+ bar = Rich::ProgressBar.new(total: 100)
406
+ 101.times do |i|
407
+ bar.update(i)
408
+ raise "Percentage wrong" unless bar.percentage == i
409
+ end
410
+ end
411
+
412
+ suite.test("Progress bar with very large total") do
413
+ bar = Rich::ProgressBar.new(total: 1_000_000_000)
414
+ bar.update(500_000_000)
415
+ raise "Large total failed" unless bar.percentage == 50
416
+ end
417
+
418
+ suite.test("Spinner cycles through all frames") do
419
+ spinner = Rich::Spinner.new(frames: Rich::ProgressStyle::DOTS)
420
+ seen_frames = Set.new
421
+ 100.times do
422
+ seen_frames.add(spinner.frame)
423
+ spinner.advance
424
+ end
425
+ raise "Not all frames" unless seen_frames.length == Rich::ProgressStyle::DOTS.length
426
+ end
427
+
428
+ suite.test("Multiple spinner styles") do
429
+ styles = [
430
+ Rich::ProgressStyle::DOTS,
431
+ Rich::ProgressStyle::LINE,
432
+ Rich::ProgressStyle::CIRCLE,
433
+ Rich::ProgressStyle::BOUNCE
434
+ ]
435
+ styles.each do |frames|
436
+ spinner = Rich::Spinner.new(frames: frames)
437
+ 5.times { spinner.advance }
438
+ end
439
+ end
440
+
441
+ # =============================================================================
442
+ # PART 8: JSON AND PRETTY PRINTING STRESS TESTS
443
+ # =============================================================================
444
+
445
+ suite.test("JSON with deeply nested structure") do
446
+ data = { "level" => nil }
447
+ current = data
448
+ 20.times do |i|
449
+ current["nested"] = { "level" => i }
450
+ current = current["nested"]
451
+ end
452
+ output = Rich::JSON.to_s(data)
453
+ raise "Deep JSON failed" if output.empty?
454
+ end
455
+
456
+ suite.test("JSON with large array") do
457
+ data = (0...1000).to_a
458
+ output = Rich::JSON.to_s(data)
459
+ raise "Large array failed" if output.empty?
460
+ end
461
+
462
+ suite.test("JSON with special characters") do
463
+ data = {
464
+ "unicode" => "こんにちは 🎉",
465
+ "escapes" => "Line1\nLine2\tTabbed",
466
+ "quotes" => 'He said "hello"'
467
+ }
468
+ output = Rich::JSON.to_s(data)
469
+ raise "Special JSON failed" if output.empty?
470
+ end
471
+
472
+ suite.test("Pretty print complex Ruby object") do
473
+ data = {
474
+ string: "hello",
475
+ number: 42,
476
+ float: 3.14159,
477
+ bool: true,
478
+ nil_val: nil,
479
+ array: [1, 2, [3, 4]],
480
+ hash: { nested: { deep: "value" } },
481
+ symbol: :test
482
+ }
483
+ output = Rich::Pretty.to_s(data)
484
+ raise "Pretty print failed" if output.empty?
485
+ end
486
+
487
+ # =============================================================================
488
+ # PART 9: CONSOLE AND RENDERING STRESS TESTS
489
+ # =============================================================================
490
+
491
+ suite.test("Console size detection") do
492
+ console = Rich::Console.new
493
+ raise "No width" unless console.width > 0
494
+ raise "No height" unless console.height > 0
495
+ raise "No color system" unless Rich::ColorSystem::ALL.include?(console.color_system)
496
+ end
497
+
498
+ suite.test("Console options update") do
499
+ options = Rich::ConsoleOptions.new(max_width: 80)
500
+ updated = options.update(max_width: 120)
501
+ raise "Update failed" unless updated.max_width == 120
502
+ raise "Original changed" unless options.max_width == 80
503
+ end
504
+
505
+ suite.test("Control codes generate valid ANSI") do
506
+ codes = [
507
+ Rich::Control.clear_screen,
508
+ Rich::Control.cursor_up(5),
509
+ Rich::Control.cursor_down(5),
510
+ Rich::Control.cursor_forward(10),
511
+ Rich::Control.cursor_backward(10),
512
+ Rich::Control.cursor_move_to(10, 20),
513
+ Rich::Control.set_title("Test"),
514
+ Rich::Control.hide_cursor,
515
+ Rich::Control.show_cursor,
516
+ Rich::Control.hyperlink("https://example.com", "Link")
517
+ ]
518
+ codes.each do |code|
519
+ raise "Invalid control code" if code.nil?
520
+ end
521
+ end
522
+
523
+ suite.test("ANSI stripping") do
524
+ styled = "\e[1;31mHello\e[0m \e[32mWorld\e[0m"
525
+ stripped = Rich::Control.strip_ansi(styled)
526
+ raise "Strip failed" unless stripped == "Hello World"
527
+ end
528
+
529
+ # =============================================================================
530
+ # PART 10: WINDOWS CONSOLE API TESTS (Windows only)
531
+ # =============================================================================
532
+
533
+ if Gem.win_platform?
534
+ suite.test("Windows Console API functions available") do
535
+ methods = %i[
536
+ stdout_handle
537
+ get_console_mode
538
+ set_console_mode
539
+ supports_ansi?
540
+ get_size
541
+ get_cursor_position
542
+ ]
543
+ methods.each do |method|
544
+ raise "Missing #{method}" unless Rich::Win32Console.respond_to?(method)
545
+ end
546
+ end
547
+
548
+ suite.test("Windows ANSI support detection") do
549
+ result = Rich::Win32Console.supports_ansi?
550
+ raise "Must be boolean" unless [true, false].include?(result)
551
+ end
552
+
553
+ suite.test("Windows console size valid") do
554
+ size = Rich::Win32Console.get_size
555
+ raise "Size failed" unless size && size[0] > 0 && size[1] > 0
556
+ end
557
+ end
558
+
559
+ # =============================================================================
560
+ # PART 11: EDGE CASES AND BOUNDARY CONDITIONS
561
+ # =============================================================================
562
+
563
+ suite.test("Empty inputs handled gracefully") do
564
+ # These should not raise
565
+ Rich::Color.parse("default")
566
+ Rich::Style.parse("")
567
+ Rich::Text.new("")
568
+ Rich::Markup.parse("")
569
+ Rich::Panel.new("")
570
+ Rich::Table.new
571
+ Rich::Tree.new("")
572
+ end
573
+
574
+ suite.test("Nil inputs handled gracefully") do
575
+ Rich::Style.parse(nil)
576
+ Rich::Style.null + nil
577
+ Rich::Segment.new(nil.to_s)
578
+ end
579
+
580
+ suite.test("Very long single-line content") do
581
+ content = "X" * 10_000
582
+ text = Rich::Text.new(content)
583
+ text.stylize("bold", 0, 5000)
584
+ segments = text.to_segments
585
+ raise "Long content failed" if segments.empty?
586
+ end
587
+
588
+ suite.test("Content at exact width boundary") do
589
+ # Panel with content exactly at boundary
590
+ panel = Rich::Panel.new("X" * 18, padding: 0)
591
+ output = panel.render(max_width: 20)
592
+ raise "Boundary failed" if output.empty?
593
+ end
594
+
595
+ suite.test("Zero and negative values") do
596
+ Rich::ProgressBar.new(total: 0)
597
+ Rich::ProgressBar.new(total: 1, completed: -5) # Should clamp
598
+ text = Rich::Text.new("test")
599
+ text.wrap(0) # Should handle gracefully
600
+ end
601
+
602
+ # Run all tests
603
+ success = suite.run_all
604
+ exit(success ? 0 : 1)