tui-td 0.2.10 → 0.2.12

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.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockNesting, Metrics/ParameterLists, Metrics/ClassLength, Metrics/CollectionLiteralLength
4
+
3
5
  require "chunky_png"
4
6
  require_relative "ansi_utils"
5
7
  require_relative "cairo_renderer"
@@ -200,7 +202,7 @@ module TUITD
200
202
  "╧" => [true, false, true, true, :double],
201
203
  "╨" => [true, false, true, true, :double],
202
204
  "╪" => [true, true, true, true, :double],
203
- "╫" => [true, true, true, true, :double]
205
+ "╫" => [true, true, true, true, :double],
204
206
  }.freeze
205
207
 
206
208
  private_constant :FONT
@@ -219,8 +221,10 @@ module TUITD
219
221
 
220
222
  @grid.each_with_index do |row, ri|
221
223
  next unless row
224
+
222
225
  row.each_with_index do |cell, ci|
223
226
  next unless cell
227
+
224
228
  render_cell(image, ri, ci, cell)
225
229
  end
226
230
  end
@@ -256,15 +260,15 @@ module TUITD
256
260
  end
257
261
 
258
262
  char_ord = char.ord
259
- if char_ord == 10095 # '❯'
263
+ if char_ord == 10_095 # '❯'
260
264
  draw_chevron(image, px, py, fg_rgb)
261
265
  draw_underline(image, px, py, CELL_W, fg_rgb) if underline
262
266
  return
263
- elsif char_ord == 9210 || char_ord == 9679 # '⏺' or '●'
267
+ elsif [9210, 9679].include?(char_ord) # '⏺' or '●'
264
268
  draw_circle(image, px, py, fg_rgb)
265
269
  draw_underline(image, px, py, CELL_W, fg_rgb) if underline
266
270
  return
267
- elsif char_ord >= 0x2800 && char_ord <= 0x28ff # Braille spinner
271
+ elsif char_ord.between?(0x2800, 0x28ff) # Braille spinner
268
272
  draw_braille(image, px, py, char, fg_rgb)
269
273
  draw_underline(image, px, py, CELL_W, fg_rgb) if underline
270
274
  return
@@ -399,7 +403,7 @@ module TUITD
399
403
 
400
404
  def glyph_rows(char)
401
405
  idx = (char.ord - 32) * 16
402
- return nil if idx < 0 || idx + 15 >= FONT.length
406
+ return nil if idx.negative? || idx + 15 >= FONT.length
403
407
 
404
408
  FONT[idx, 16]
405
409
  end
@@ -408,7 +412,7 @@ module TUITD
408
412
  color = ChunkyPNG::Color.rgb(*fg_rgb)
409
413
 
410
414
  rows.each_with_index do |byte, dy|
411
- next if byte == 0
415
+ next if byte.zero?
412
416
 
413
417
  slant = italic ? dy / 8 : 0
414
418
 
@@ -429,7 +433,7 @@ module TUITD
429
433
 
430
434
  def box_drawing?(char)
431
435
  char_ord = char.ord
432
- char_ord >= 0x2500 && char_ord <= 0x257F
436
+ char_ord.between?(0x2500, 0x257F)
433
437
  end
434
438
 
435
439
  def draw_box_character(image, px, py, char, fg_rgb)
@@ -438,10 +442,18 @@ module TUITD
438
442
  unless config
439
443
  char_ord = char.ord
440
444
  if [0x2500, 0x2501, 0x2504, 0x2505, 0x2508, 0x2509, 0x254c, 0x254d, 0x2550].include?(char_ord)
441
- style = [0x2501, 0x2505, 0x2509, 0x254d].include?(char_ord) ? :heavy : (char_ord == 0x2550 ? :double : :light)
445
+ style = if [0x2501, 0x2505, 0x2509, 0x254d].include?(char_ord)
446
+ :heavy
447
+ else
448
+ (char_ord == 0x2550 ? :double : :light)
449
+ end
442
450
  config = [false, false, true, true, style]
443
451
  elsif [0x2502, 0x2503, 0x2506, 0x2507, 0x250a, 0x250b, 0x254e, 0x254f, 0x2551].include?(char_ord)
444
- style = [0x2503, 0x2507, 0x250b, 0x254f].include?(char_ord) ? :heavy : (char_ord == 0x2551 ? :double : :light)
452
+ style = if [0x2503, 0x2507, 0x250b, 0x254f].include?(char_ord)
453
+ :heavy
454
+ else
455
+ (char_ord == 0x2551 ? :double : :light)
456
+ end
445
457
  config = [true, true, false, false, style]
446
458
  else
447
459
  config = [true, true, true, true, :light]
@@ -454,24 +466,31 @@ module TUITD
454
466
 
455
467
  color = ChunkyPNG::Color.rgb(*fg_rgb)
456
468
 
457
- if style == :double
469
+ case style
470
+ when :double
458
471
  if left
459
- (px..(cx + 2)).each { |x| image[x, py + 6] = color }
460
- (px..(cx + 2)).each { |x| image[x, py + 10] = color }
472
+ (px..(cx + 2)).each do |x|
473
+ image[x, py + 6] = color
474
+ image[x, py + 10] = color
475
+ end
461
476
  end
462
477
  if right
463
478
  ((cx - 2)..(px + 7)).each { |x| image[x, py + 6] = color }
464
479
  ((cx - 2)..(px + 10)).each { |x| image[x, py + 10] = color }
465
480
  end
466
481
  if up
467
- (py..(cy + 2)).each { |y| image[px + 2, y] = color }
468
- (py..(cy + 2)).each { |y| image[px + 6, y] = color }
482
+ (py..(cy + 2)).each do |y|
483
+ image[px + 2, y] = color
484
+ image[px + 6, y] = color
485
+ end
469
486
  end
470
487
  if down
471
- ((cy - 2)..(py + 15)).each { |y| image[px + 2, y] = color }
472
- ((cy - 2)..(py + 15)).each { |y| image[px + 6, y] = color }
488
+ ((cy - 2)..(py + 15)).each do |y|
489
+ image[px + 2, y] = color
490
+ image[px + 6, y] = color
491
+ end
473
492
  end
474
- elsif style == :heavy
493
+ when :heavy
475
494
  if left
476
495
  (px..cx).each do |x|
477
496
  image[x, cy - 1] = color
@@ -500,42 +519,34 @@ module TUITD
500
519
  image[cx + 1, y] = color
501
520
  end
502
521
  end
503
- elsif style == :light_rounded
522
+ when :light_rounded
504
523
  case char
505
524
  when "╭"
506
- (px + 5..px + 7).each { |x| image[x, py + 8] = color }
507
- (py + 10..py + 15).each { |y| image[px + 4, y] = color }
525
+ ((px + 5)..(px + 7)).each { |x| image[x, py + 8] = color }
526
+ ((py + 10)..(py + 15)).each { |y| image[px + 4, y] = color }
508
527
  image[px + 4, py + 9] = color
509
528
  image[px + 5, py + 9] = color
510
529
  when "╮"
511
- (px..px + 3).each { |x| image[x, py + 8] = color }
512
- (py + 10..py + 15).each { |y| image[px + 4, y] = color }
530
+ (px..(px + 3)).each { |x| image[x, py + 8] = color }
531
+ ((py + 10)..(py + 15)).each { |y| image[px + 4, y] = color }
513
532
  image[px + 4, py + 9] = color
514
533
  image[px + 3, py + 9] = color
515
534
  when "╯"
516
- (px..px + 3).each { |x| image[x, py + 8] = color }
517
- (py..py + 6).each { |y| image[px + 4, y] = color }
535
+ (px..(px + 3)).each { |x| image[x, py + 8] = color }
536
+ (py..(py + 6)).each { |y| image[px + 4, y] = color }
518
537
  image[px + 4, py + 7] = color
519
538
  image[px + 3, py + 7] = color
520
539
  when "╰"
521
- (px + 5..px + 7).each { |x| image[x, py + 8] = color }
522
- (py..py + 6).each { |y| image[px + 4, y] = color }
540
+ ((px + 5)..(px + 7)).each { |x| image[x, py + 8] = color }
541
+ (py..(py + 6)).each { |y| image[px + 4, y] = color }
523
542
  image[px + 4, py + 7] = color
524
543
  image[px + 5, py + 7] = color
525
544
  end
526
545
  else # :light
527
- if left
528
- (px..cx).each { |x| image[x, cy] = color }
529
- end
530
- if right
531
- (cx..(px + 7)).each { |x| image[x, cy] = color }
532
- end
533
- if up
534
- (py..cy).each { |y| image[cx, y] = color }
535
- end
536
- if down
537
- (cy..(py + 15)).each { |y| image[cx, y] = color }
538
- end
546
+ (px..cx).each { |x| image[x, cy] = color } if left
547
+ (cx..(px + 7)).each { |x| image[x, cy] = color } if right
548
+ (py..cy).each { |y| image[cx, y] = color } if up
549
+ (cy..(py + 15)).each { |y| image[cx, y] = color } if down
539
550
  end
540
551
  end
541
552
 
@@ -551,7 +562,7 @@ module TUITD
551
562
  ri = cursor_info[:row] || cursor_info["row"] || 0
552
563
  ci = cursor_info[:col] || cursor_info["col"] || 0
553
564
 
554
- return if ri < 0 || ri >= @rows || ci < 0 || ci >= @cols
565
+ return if ri.negative? || ri >= @rows || ci.negative? || ci >= @cols
555
566
 
556
567
  style_val = @state[:cursor_style] || cursor_info[:style] || cursor_info["style"] || 1
557
568
 
@@ -568,6 +579,7 @@ module TUITD
568
579
  x = px + dx
569
580
  y = py + dy
570
581
  next if x >= image.width || y >= image.height
582
+
571
583
  original_color = image[x, y]
572
584
  r = 255 - ChunkyPNG::Color.r(original_color)
573
585
  g = 255 - ChunkyPNG::Color.g(original_color)
@@ -579,9 +591,11 @@ module TUITD
579
591
  2.times do |h_offset|
580
592
  y = py + CELL_H - 1 - h_offset
581
593
  next if y >= image.height
594
+
582
595
  CELL_W.times do |dx|
583
596
  x = px + dx
584
597
  next if x >= image.width
598
+
585
599
  image[x, y] = color
586
600
  end
587
601
  end
@@ -589,9 +603,11 @@ module TUITD
589
603
  2.times do |w_offset|
590
604
  x = px + w_offset
591
605
  next if x >= image.width
606
+
592
607
  CELL_H.times do |dy|
593
608
  y = py + dy
594
609
  next if y >= image.height
610
+
595
611
  image[x, y] = color
596
612
  end
597
613
  end
@@ -603,7 +619,7 @@ module TUITD
603
619
  (0..3).each do |i|
604
620
  image[px + 2 + i, py + 4 + i] = color
605
621
  image[px + 3 + i, py + 4 + i] = color # bold/thick chevron
606
-
622
+
607
623
  image[px + 5 - i, py + 8 + i] = color
608
624
  image[px + 6 - i, py + 8 + i] = color # bold/thick chevron
609
625
  end
@@ -623,6 +639,7 @@ module TUITD
623
639
  x = cx + dx
624
640
  y = cy + dy
625
641
  next if x < px || x >= px + CELL_W || y < py || y >= py + CELL_H
642
+
626
643
  image[x, y] = color
627
644
  end
628
645
  end
@@ -639,17 +656,18 @@ module TUITD
639
656
  [5, 6], # Dot 5
640
657
  [5, 9], # Dot 6
641
658
  [2, 12], # Dot 7
642
- [5, 12] # Dot 8
659
+ [5, 12], # Dot 8
643
660
  ]
644
661
  dot_coords.each_with_index do |(dx, dy), idx|
645
- if (mask & (1 << idx)) != 0
646
- 2.times do |ddy|
647
- 2.times do |ddx|
648
- x = px + dx + ddx
649
- y = py + dy + ddy
650
- next if x >= image.width || y >= image.height
651
- image[x, y] = color
652
- end
662
+ next unless mask.anybits?(1 << idx)
663
+
664
+ 2.times do |ddy|
665
+ 2.times do |ddx|
666
+ x = px + dx + ddx
667
+ y = py + dy + ddy
668
+ next if x >= image.width || y >= image.height
669
+
670
+ image[x, y] = color
653
671
  end
654
672
  end
655
673
  end
@@ -659,7 +677,7 @@ module TUITD
659
677
  color = ChunkyPNG::Color.rgb(*fg_rgb)
660
678
  (5..8).each do |dy|
661
679
  width = dy - 5
662
- (4 - width..4 + width).each do |dx|
680
+ ((4 - width)..(4 + width)).each do |dx|
663
681
  image[px + dx, py + dy] = color
664
682
  end
665
683
  end
@@ -669,7 +687,7 @@ module TUITD
669
687
  color = ChunkyPNG::Color.rgb(*fg_rgb)
670
688
  (5..8).each do |dy|
671
689
  width = 8 - dy
672
- (4 - width..4 + width).each do |dx|
690
+ ((4 - width)..(4 + width)).each do |dx|
673
691
  image[px + dx, py + dy] = color
674
692
  end
675
693
  end
@@ -874,6 +892,6 @@ module TUITD
874
892
  image[px + 3, py + 8] = color
875
893
  image[px + 4, py + 8] = color
876
894
  end
877
-
878
895
  end
879
896
  end
897
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockNesting, Metrics/ParameterLists, Metrics/ClassLength, Metrics/CollectionLiteralLength
data/lib/tui_td/state.rb CHANGED
@@ -1,121 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module TUITD
4
- # Represents the parsed state of a terminal screen.
5
- # Provides high-level query methods for AI consumption.
6
- class State
7
- attr_reader :rows, :cols, :grid, :cursor, :cursor_visible, :cursor_style, :mouse_mode, :mouse_format
8
-
9
- def initialize(data)
10
- raise ArgumentError, "State data must include :size key" unless data[:size]
11
- raise ArgumentError, "State data must include :rows key" unless data[:rows]
12
-
13
- @rows = data[:size][:rows]
14
- @cols = data[:size][:cols]
15
- @grid = data[:rows]
16
- @cursor = data[:cursor]
17
-
18
- cursor_info = data[:cursor].is_a?(Hash) ? data[:cursor] : {}
19
- @cursor_visible = data.key?(:cursor_visible) ? data[:cursor_visible] : (cursor_info[:visible] != false)
20
- @cursor_style = data.key?(:cursor_style) ? data[:cursor_style] : (cursor_info[:style] || 1)
21
-
22
- @mouse_mode = data[:mouse_mode] || :none
23
- @mouse_format = data[:mouse_format] || :normal
24
- end
25
-
26
- # Get plain text of the entire terminal (no ANSI)
27
- def plain_text
28
- @grid.map { |row| row.map { |c| c[:char] }.join.rstrip }.join("\n")
29
- end
30
-
31
- # Get text at a specific position
32
- def text_at(row, col, length = @cols - col)
33
- return "" if row >= @rows || col >= @cols
34
- @grid[row][col, length].map { |c| c[:char] }.join
35
- end
36
-
37
- # Search for text across the entire terminal
38
- def find_text(pattern)
39
- results = []
40
- @grid.each_with_index do |row, ri|
41
- text = row.map { |c| c[:char] }.join
42
- pos = 0
43
- while (match = text.index(pattern, pos))
44
- results << { row: ri, col: match, text: pattern, full_line: text }
45
- pos = match + 1
46
- end
47
- end
48
- results
49
- end
50
-
51
- # Get the color at a specific cell
52
- def foreground_at(row, col)
53
- return nil if row >= @rows || col >= @cols
54
- @grid[row][col][:fg]
55
- end
56
-
57
- def background_at(row, col)
58
- return nil if row >= @rows || col >= @cols
59
- @grid[row][col][:bg]
60
- end
3
+ require "tans-parser"
61
4
 
62
- def style_at(row, col)
63
- return nil if row >= @rows || col >= @cols
64
- cell = @grid[row][col]
65
- { bold: cell[:bold], italic: cell[:italic], underline: cell[:underline] }
66
- end
67
-
68
- def to_ai_json
69
- h = extract_highlights
70
- cursor_info = @cursor.is_a?(Hash) ? @cursor : {}
71
- r = cursor_info[:row] || cursor_info["row"] || 0
72
- c = cursor_info[:col] || cursor_info["col"] || 0
73
- styled_count = h.count { |hl| hl[:bold] || hl[:italic] || hl[:underline] || hl[:fg] || hl[:bg] }
74
-
75
- summary = +"Cursor at [#{r},#{c}]. "
76
- summary << "#{styled_count} styled row#{styled_count == 1 ? '' : 's'}"
77
- fgs = h.flat_map { |hl| hl[:fg] }.compact.uniq
78
- bgs = h.flat_map { |hl| hl[:bg] }.compact.uniq
79
- summary << ", colors: fg=#{fgs.sort.join(',')}" unless fgs.empty?
80
- summary << ", bg=#{bgs.sort.join(',')}" unless bgs.empty?
81
- summary << "."
82
-
83
- {
84
- size: { rows: @rows, cols: @cols },
85
- cursor: cursor_info,
86
- text: plain_text,
87
- highlights: h,
88
- summary: summary,
89
- }
90
- end
91
-
92
- private
93
-
94
- def extract_highlights
95
- highlights = []
96
- @grid.each_with_index do |row, ri|
97
- row_text = row.map { |c| c[:char] }.join
98
- next if row_text.strip.empty?
99
-
100
- fgs = row.map { |c| c[:fg] || c["fg"] || "default" }
101
- .uniq.reject { |c| c == "default" }
102
- bgs = row.map { |c| c[:bg] || c["bg"] || "default" }
103
- .uniq.reject { |c| c == "default" }
104
- bold = row.any? { |c| c[:bold] || c["bold"] }
105
- italic = row.any? { |c| c[:italic] || c["italic"] }
106
- underline = row.any? { |c| c[:underline] || c["underline"] }
107
-
108
- next if fgs.empty? && bgs.empty? && !bold && !italic && !underline
109
-
110
- h = { row: ri, text: row_text }
111
- h[:bold] = true if bold
112
- h[:italic] = true if italic
113
- h[:underline] = true if underline
114
- h[:fg] = fgs.size == 1 ? fgs.first : fgs unless fgs.empty?
115
- h[:bg] = bgs.size == 1 ? bgs.first : bgs unless bgs.empty?
116
- highlights << h
117
- end
118
- highlights
119
- end
120
- end
5
+ module TUITD
6
+ State = TansParser::State
121
7
  end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength, Metrics/ClassLength
4
+
3
5
  require "json"
6
+ require "shellwords"
4
7
 
5
8
  module TUITD
6
9
  # Executes TUI tests defined in JSON format.
@@ -39,10 +42,6 @@ module TUITD
39
42
  @on_step = on_step
40
43
  rescue JSON::ParserError => e
41
44
  raise Error, "Invalid JSON: #{e.message}"
42
- @plan[:steps] = @plan[:steps].map { |s| s.transform_keys(&:to_sym) }
43
- @plan[:before_all] = @plan[:before_all]&.map { |s| s.transform_keys(&:to_sym) }
44
- @plan[:after_all] = @plan[:after_all]&.map { |s| s.transform_keys(&:to_sym) }
45
- @on_step = on_step
46
45
  end
47
46
 
48
47
  def run
@@ -55,7 +54,7 @@ module TUITD
55
54
  hooks = [
56
55
  { label: :before_all, steps: @plan[:before_all] || [] },
57
56
  { label: :main, steps: @plan[:steps] },
58
- { label: :after_all, steps: @plan[:after_all] || [] }
57
+ { label: :after_all, steps: @plan[:after_all] || [] },
59
58
  ]
60
59
 
61
60
  all_results = []
@@ -120,18 +119,19 @@ module TUITD
120
119
  if match
121
120
  Result.new(step: action, passed: true, message: "Style at [#{row},#{col}] matches #{expected}")
122
121
  else
123
- Result.new(step: action, passed: false, message: "Style at [#{row},#{col}] is #{actual}, expected #{expected}")
122
+ Result.new(step: action, passed: false,
123
+ message: "Style at [#{row},#{col}] is #{actual}, expected #{expected}",)
124
124
  end
125
125
 
126
126
  when "screenshot"
127
127
  ensure_driver!(driver)
128
- path = value.is_a?(String) ? value : "/tmp/tui_td_#{Time.now.to_i}.png"
128
+ path = safe_output_path(value, "png")
129
129
  driver.screenshot(path)
130
130
  Result.new(step: action, passed: true, message: "Saved: #{path}")
131
131
 
132
132
  when "html"
133
133
  ensure_driver!(driver)
134
- path = value.is_a?(String) ? value : "/tmp/tui_td_#{Time.now.to_i}.html"
134
+ path = safe_output_path(value, "html")
135
135
  HtmlRenderer.new(driver.state_data).render(path)
136
136
  Result.new(step: action, passed: true, message: "Saved: #{path}")
137
137
 
@@ -159,7 +159,6 @@ module TUITD
159
159
  else
160
160
  Result.new(step: action, passed: false, message: "Unknown action: #{action}")
161
161
  end
162
-
163
162
  rescue StandardError => e
164
163
  r = Result.new(step: action, passed: false, message: "#{e.class}: #{e.message}")
165
164
  end
@@ -167,23 +166,23 @@ module TUITD
167
166
  all_results << r
168
167
  all_passed &&= r.passed
169
168
 
170
- if @on_step
171
- state_data = nil
172
- begin
173
- state_data = driver.state_data if driver
174
- rescue StandardError
175
- # ignore — state retrieval is best-effort
176
- end
177
- @on_step.call(
178
- index: all_results.size - 1,
179
- total: total_steps,
180
- action: action,
181
- value: value,
182
- result: r,
183
- driver: driver,
184
- state_data: state_data
185
- )
169
+ next unless @on_step
170
+
171
+ state_data = nil
172
+ begin
173
+ state_data = driver.state_data if driver
174
+ rescue StandardError
175
+ # ignore — state retrieval is best-effort
186
176
  end
177
+ @on_step.call(
178
+ index: all_results.size - 1,
179
+ total: total_steps,
180
+ action: action,
181
+ value: value,
182
+ result: r,
183
+ driver: driver,
184
+ state_data: state_data,
185
+ )
187
186
  end
188
187
  end
189
188
 
@@ -192,12 +191,25 @@ module TUITD
192
191
  {
193
192
  name: @plan[:name] || "(unnamed)",
194
193
  passed: all_passed,
195
- results: all_results.map(&:to_h)
194
+ results: all_results.map(&:to_h),
196
195
  }
197
196
  end
198
197
 
198
+ ALLOWED_OUTPUT_DIRS = ["/tmp"].freeze
199
+
199
200
  private
200
201
 
202
+ def safe_output_path(value, ext)
203
+ default = File.join("/tmp", "tui_td_#{Time.now.to_i}.#{ext}")
204
+ resolved = File.expand_path(value.is_a?(String) ? value : default)
205
+
206
+ unless ALLOWED_OUTPUT_DIRS.any? { |dir| resolved.start_with?(File.expand_path(dir)) }
207
+ raise TUITD::Error, "Output path must be under one of: #{ALLOWED_OUTPUT_DIRS.join(", ")}"
208
+ end
209
+
210
+ resolved
211
+ end
212
+
201
213
  def ensure_driver!(driver)
202
214
  raise Error, "No session. Add a 'start' step first." if driver.nil?
203
215
  end
@@ -261,8 +273,10 @@ module TUITD
261
273
  if actual == expected
262
274
  Result.new(step: step.keys.first.to_s, passed: true, message: "#{label} at [#{row},#{col}] is #{expected}")
263
275
  else
264
- Result.new(step: step.keys.first.to_s, passed: false, message: "#{label} at [#{row},#{col}] is #{actual}, expected #{expected}")
276
+ Result.new(step: step.keys.first.to_s, passed: false,
277
+ message: "#{label} at [#{row},#{col}] is #{actual}, expected #{expected}",)
265
278
  end
266
279
  end
267
280
  end
268
281
  end
282
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength, Metrics/ClassLength