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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +546 -0
- data/examples/demo.rb +106 -0
- data/examples/showcase.rb +420 -0
- data/examples/smoke_test.rb +41 -0
- data/examples/stress_test.rb +604 -0
- data/examples/syntax_markdown_demo.rb +166 -0
- data/examples/verify.rb +215 -0
- data/examples/visual_demo.rb +145 -0
- data/lib/rich/_palettes.rb +148 -0
- data/lib/rich/box.rb +342 -0
- data/lib/rich/cells.rb +512 -0
- data/lib/rich/color.rb +628 -0
- data/lib/rich/color_triplet.rb +220 -0
- data/lib/rich/console.rb +549 -0
- data/lib/rich/control.rb +332 -0
- data/lib/rich/json.rb +254 -0
- data/lib/rich/layout.rb +314 -0
- data/lib/rich/markdown.rb +509 -0
- data/lib/rich/markup.rb +175 -0
- data/lib/rich/panel.rb +311 -0
- data/lib/rich/progress.rb +430 -0
- data/lib/rich/segment.rb +387 -0
- data/lib/rich/style.rb +433 -0
- data/lib/rich/syntax.rb +1145 -0
- data/lib/rich/table.rb +525 -0
- data/lib/rich/terminal_theme.rb +126 -0
- data/lib/rich/text.rb +433 -0
- data/lib/rich/tree.rb +220 -0
- data/lib/rich/version.rb +5 -0
- data/lib/rich/win32_console.rb +582 -0
- data/lib/rich.rb +108 -0
- metadata +106 -0
|
@@ -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)
|