test-unit 3.1.5 → 3.6.1

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.
Files changed (87) hide show
  1. checksums.yaml +5 -5
  2. data/BSDL +24 -0
  3. data/COPYING +41 -41
  4. data/README.md +24 -17
  5. data/Rakefile +21 -24
  6. data/doc/text/getting-started.md +246 -0
  7. data/doc/text/news.md +797 -56
  8. data/lib/test/unit/assertion-failed-error.rb +35 -0
  9. data/lib/test/unit/assertions.rb +542 -220
  10. data/lib/test/unit/attribute.rb +78 -4
  11. data/lib/test/unit/auto-runner-loader.rb +17 -0
  12. data/lib/test/unit/autorunner.rb +175 -78
  13. data/lib/test/unit/code-snippet-fetcher.rb +7 -7
  14. data/lib/test/unit/collector/descendant.rb +1 -0
  15. data/lib/test/unit/collector/dir.rb +4 -2
  16. data/lib/test/unit/collector/load.rb +25 -15
  17. data/lib/test/unit/collector/objectspace.rb +1 -0
  18. data/lib/test/unit/collector.rb +31 -0
  19. data/lib/test/unit/color-scheme.rb +29 -2
  20. data/lib/test/unit/data-sets.rb +127 -0
  21. data/lib/test/unit/data.rb +121 -12
  22. data/lib/test/unit/diff.rb +10 -11
  23. data/lib/test/unit/fixture.rb +77 -27
  24. data/lib/test/unit/notification.rb +9 -7
  25. data/lib/test/unit/omission.rb +34 -31
  26. data/lib/test/unit/pending.rb +12 -11
  27. data/lib/test/unit/priority.rb +7 -5
  28. data/lib/test/unit/runner/console.rb +20 -1
  29. data/lib/test/unit/test-suite-creator.rb +30 -9
  30. data/lib/test/unit/testcase.rb +349 -196
  31. data/lib/test/unit/testresult.rb +7 -0
  32. data/lib/test/unit/testsuite.rb +1 -1
  33. data/lib/test/unit/ui/console/testrunner.rb +171 -60
  34. data/lib/test/unit/ui/emacs/testrunner.rb +5 -5
  35. data/lib/test/unit/ui/testrunnermediator.rb +9 -7
  36. data/lib/test/unit/util/backtracefilter.rb +17 -5
  37. data/lib/test/unit/util/memory-usage.rb +47 -0
  38. data/lib/test/unit/util/observable.rb +2 -2
  39. data/lib/test/unit/util/output.rb +5 -4
  40. data/lib/test/unit/util/procwrapper.rb +4 -4
  41. data/lib/test/unit/version.rb +1 -1
  42. data/lib/test/unit/warning.rb +3 -0
  43. data/lib/test/unit.rb +177 -161
  44. data/lib/test-unit.rb +2 -17
  45. metadata +20 -94
  46. data/GPL +0 -339
  47. data/LGPL +0 -502
  48. data/test/collector/test-descendant.rb +0 -178
  49. data/test/collector/test-load.rb +0 -442
  50. data/test/collector/test_dir.rb +0 -406
  51. data/test/collector/test_objectspace.rb +0 -100
  52. data/test/fixtures/header-label.csv +0 -3
  53. data/test/fixtures/header-label.tsv +0 -3
  54. data/test/fixtures/header.csv +0 -3
  55. data/test/fixtures/header.tsv +0 -3
  56. data/test/fixtures/no-header.csv +0 -2
  57. data/test/fixtures/no-header.tsv +0 -2
  58. data/test/fixtures/plus.csv +0 -3
  59. data/test/run-test.rb +0 -22
  60. data/test/test-assertions.rb +0 -2157
  61. data/test/test-attribute-matcher.rb +0 -38
  62. data/test/test-attribute.rb +0 -123
  63. data/test/test-code-snippet.rb +0 -37
  64. data/test/test-color-scheme.rb +0 -82
  65. data/test/test-color.rb +0 -47
  66. data/test/test-data.rb +0 -281
  67. data/test/test-diff.rb +0 -518
  68. data/test/test-emacs-runner.rb +0 -60
  69. data/test/test-error.rb +0 -26
  70. data/test/test-failure.rb +0 -33
  71. data/test/test-fault-location-detector.rb +0 -163
  72. data/test/test-fixture.rb +0 -659
  73. data/test/test-notification.rb +0 -33
  74. data/test/test-omission.rb +0 -81
  75. data/test/test-pending.rb +0 -70
  76. data/test/test-priority.rb +0 -173
  77. data/test/test-test-case.rb +0 -1171
  78. data/test/test-test-result.rb +0 -113
  79. data/test/test-test-suite-creator.rb +0 -97
  80. data/test/test-test-suite.rb +0 -150
  81. data/test/testunit-test-util.rb +0 -31
  82. data/test/ui/test_testrunmediator.rb +0 -20
  83. data/test/util/test-method-owner-finder.rb +0 -38
  84. data/test/util/test-output.rb +0 -11
  85. data/test/util/test_backtracefilter.rb +0 -41
  86. data/test/util/test_observable.rb +0 -102
  87. data/test/util/test_procwrapper.rb +0 -36
@@ -25,9 +25,9 @@ module Test
25
25
  basedir = @base
26
26
  $:.push(basedir) if basedir
27
27
  if(from.empty?)
28
- recursive_collect('.', find_test_cases)
28
+ suite = recursive_collect('.', find_test_cases)
29
29
  elsif(from.size == 1)
30
- recursive_collect(from.first, find_test_cases)
30
+ suite = recursive_collect(from.first, find_test_cases)
31
31
  else
32
32
  suites = []
33
33
  from.each do |f|
@@ -38,6 +38,8 @@ module Test
38
38
  sort(suites).each{|s| suite << s}
39
39
  suite
40
40
  end
41
+ adjust_ractor_tests(suite)
42
+ suite
41
43
  ensure
42
44
  $:.delete_at($:.rindex(basedir)) if basedir
43
45
  end
@@ -10,6 +10,7 @@ module Test
10
10
  include Collector
11
11
 
12
12
  attr_reader :patterns, :excludes, :base
13
+ attr_reader :default_test_paths
13
14
 
14
15
  def initialize
15
16
  super
@@ -18,6 +19,7 @@ module Test
18
19
  @patterns = [/\Atest[_\-].+\.rb\z/m, /[_\-]test\.rb\z/]
19
20
  @excludes = []
20
21
  @base = nil
22
+ @default_test_paths = []
21
23
  @require_failed_infos = []
22
24
  end
23
25
 
@@ -26,11 +28,19 @@ module Test
26
28
  @base = base
27
29
  end
28
30
 
31
+ def default_test_paths=(paths)
32
+ @default_test_paths = paths.collect do |path|
33
+ Pathname(path)
34
+ end
35
+ end
36
+
29
37
  def collect(*froms)
30
38
  add_load_path(@base) do
39
+ froms = @default_test_paths if froms.empty?
31
40
  froms = ["."] if froms.empty?
32
41
  test_suites = []
33
- already_gathered = find_test_cases
42
+ already_gathered = {}
43
+ find_test_cases(already_gathered)
34
44
  froms.each do |from|
35
45
  from = resolve_path(from)
36
46
  if from.directory?
@@ -51,16 +61,19 @@ module Test
51
61
  test_suite = test_suites.first
52
62
  end
53
63
 
64
+ adjust_ractor_tests(test_suite)
65
+
54
66
  test_suite
55
67
  end
56
68
  end
57
69
 
58
- def find_test_cases(ignore=[])
70
+ def find_test_cases(already_gathered)
59
71
  test_cases = []
60
72
  TestCase::DESCENDANTS.each do |test_case|
61
- test_cases << test_case unless ignore.include?(test_case)
73
+ next if already_gathered.key?(test_case)
74
+ test_cases << test_case
75
+ already_gathered[test_case] = true
62
76
  end
63
- ignore.concat(test_cases)
64
77
  test_cases
65
78
  end
66
79
 
@@ -102,7 +115,7 @@ module Test
102
115
  return if @program_file == expanded_path.to_s
103
116
  add_load_path(expanded_path.dirname) do
104
117
  begin
105
- require(path.basename.to_s)
118
+ require(expanded_path.to_s)
106
119
  rescue LoadError
107
120
  @require_failed_infos << {:path => expanded_path, :exception => $!}
108
121
  end
@@ -155,11 +168,11 @@ module Test
155
168
  return if @require_failed_infos.empty?
156
169
 
157
170
  require_failed_infos = @require_failed_infos
158
- require_failed_omissions = Class.new(Test::Unit::TestCase)
159
- require_failed_omissions.class_eval do
171
+ require_failed_errors = Class.new(Test::Unit::TestCase)
172
+ require_failed_errors.class_eval do
160
173
  class << self
161
174
  def name
162
- "RequireFailedOmissions"
175
+ "RequireFailedErrors"
163
176
  end
164
177
  end
165
178
 
@@ -169,21 +182,18 @@ module Test
169
182
  normalized_path = normalized_path.gsub(/\A_+/, '')
170
183
  exception = info[:exception]
171
184
  define_method("test_require_#{normalized_path}") do
172
- @require_failed_exception = exception
173
- omit("failed to load: <#{path}>: <#{exception.message}>")
185
+ raise(exception.class,
186
+ "failed to load <#{path}>: #{exception.message}",
187
+ exception.backtrace)
174
188
  end
175
189
  end
176
190
 
177
191
  def priority
178
192
  100
179
193
  end
180
-
181
- def filter_backtrace(location)
182
- super(@require_failed_exception.backtrace)
183
- end
184
194
  end
185
195
 
186
- add_suite(test_suites, require_failed_omissions.suite)
196
+ add_suite(test_suites, require_failed_errors.suite)
187
197
  end
188
198
  end
189
199
  end
@@ -26,6 +26,7 @@ module Test
26
26
  end
27
27
  end
28
28
  sort(sub_suites).each{|s| suite << s}
29
+ adjust_ractor_tests(suite)
29
30
  suite
30
31
  end
31
32
  end
@@ -68,6 +68,37 @@ module Test
68
68
  suite << sub_suite
69
69
  end
70
70
  end
71
+
72
+ def adjust_ractor_tests(suite)
73
+ return if suite.nil?
74
+ ractor_suites = extract_ractor_tests(suite)
75
+ ractor_suites.each do |ractor_suite|
76
+ suite << ractor_suite
77
+ end
78
+ end
79
+
80
+ def extract_ractor_tests(suite)
81
+ ractor_suites = []
82
+ ractor_tests = []
83
+ suite.tests.each do |test|
84
+ case test
85
+ when TestSuite
86
+ ractor_suites.concat(extract_ractor_tests(test))
87
+ else
88
+ next unless test[:ractor]
89
+ ractor_tests << test
90
+ end
91
+ end
92
+ unless ractor_tests.empty?
93
+ suite.delete_tests(ractor_tests)
94
+ ractor_suite = TestSuite.new(suite.name, suite.test_case)
95
+ ractor_tests.each do |ractor_test|
96
+ ractor_suite << ractor_test
97
+ end
98
+ ractor_suites << ractor_suite
99
+ end
100
+ ractor_suites
101
+ end
71
102
  end
72
103
  end
73
104
  end
@@ -5,9 +5,25 @@ module Test
5
5
  class ColorScheme
6
6
  include Enumerable
7
7
 
8
+ TERM_256 = /
9
+ [+-]256color|
10
+ \A(?:
11
+ alacritty|
12
+ iTerm\s?\d*\.app|
13
+ kitty|
14
+ mintty|
15
+ ms-terminal|
16
+ nsterm-build\d+|
17
+ nsterm|
18
+ terminator|
19
+ terminology(?:-[0-9.]+)?|
20
+ termite|
21
+ vscode
22
+ )\z/x
23
+
8
24
  class << self
9
25
  def default
10
- if available_colors == 256
26
+ if available_colors >= 256
11
27
  default_for_256_colors
12
28
  else
13
29
  default_for_8_colors
@@ -110,6 +126,7 @@ module Test
110
126
  guess_available_colors_from_vte_version_env ||
111
127
  guess_available_colors_from_colorterm_env ||
112
128
  guess_available_colors_from_term_env ||
129
+ guess_available_colors_from_github_actions_env ||
113
130
  8
114
131
  end
115
132
 
@@ -140,12 +157,22 @@ module Test
140
157
 
141
158
  def guess_available_colors_from_term_env
142
159
  case ENV["TERM"]
143
- when /-256color\z/
160
+ when /[+-]direct/
161
+ 2 ** 24
162
+ when TERM_256
144
163
  256
145
164
  else
146
165
  nil
147
166
  end
148
167
  end
168
+
169
+ def guess_available_colors_from_github_actions_env
170
+ if ENV["GITHUB_ACTIONS"] == "true"
171
+ 2 ** 24
172
+ else
173
+ nil
174
+ end
175
+ end
149
176
  end
150
177
 
151
178
  def initialize(scheme_spec)
@@ -0,0 +1,127 @@
1
+ module Test
2
+ module Unit
3
+ class DataSets
4
+ def initialize
5
+ @variables = []
6
+ @procs = []
7
+ @value_sets = []
8
+ end
9
+
10
+ def add(data_set, options=nil)
11
+ options ||= {}
12
+ if data_set.respond_to?(:call)
13
+ @procs << [data_set, options]
14
+ elsif data_set.is_a?(Array)
15
+ @variables << [data_set, options]
16
+ else
17
+ @value_sets << [data_set, options]
18
+ end
19
+ end
20
+
21
+ def <<(data_set)
22
+ add(data_set)
23
+ end
24
+
25
+ def have_keep?
26
+ each_data_set do |_, options|
27
+ return true if options[:keep]
28
+ end
29
+ false
30
+ end
31
+
32
+ def keep
33
+ new_data_sets = self.class.new
34
+ each_data_set do |data_set, options|
35
+ next if options.nil?
36
+ next unless options[:keep]
37
+ new_data_sets.add(data_set, options)
38
+ end
39
+ new_data_sets
40
+ end
41
+
42
+ def each
43
+ variables = @variables
44
+ value_sets = @value_sets
45
+ @procs.each do |proc, options|
46
+ data_set = proc.call
47
+ case data_set
48
+ when Array
49
+ variables += [[data_set, options]]
50
+ else
51
+ value_sets += [[data_set, options]]
52
+ end
53
+ end
54
+
55
+ value_sets.each do |values, _options|
56
+ values.each do |label, data|
57
+ yield(label, data)
58
+ end
59
+ end
60
+
61
+ each_pattern(variables) do |label, data|
62
+ yield(label, data)
63
+ end
64
+ end
65
+
66
+ def ==(other)
67
+ @variables == other.instance_variable_get(:@variables) and
68
+ @procs == other.instance_variable_get(:@procs) and
69
+ @value_sets == other.instance_variable_get(:@value_sets)
70
+ end
71
+
72
+ def eql?(other)
73
+ self == other
74
+ end
75
+
76
+ def hash
77
+ [@variables, @procs, @value_sets].hash
78
+ end
79
+
80
+ private
81
+ def each_data_set(&block)
82
+ @procs.each(&block)
83
+ @variables.each(&block)
84
+ @value_sets.each(&block)
85
+ end
86
+
87
+ def each_pattern(variables)
88
+ grouped_variables = variables.group_by do |_, options|
89
+ options[:group]
90
+ end
91
+ grouped_variables.each do |group, group_variables|
92
+ each_raw_pattern(group_variables) do |cell|
93
+ label = String.new
94
+ label << "group: #{group.inspect}" unless group.nil?
95
+ data = {}
96
+ cell.each do |variable, pattern, pattern_label|
97
+ label << ", " unless label.empty?
98
+ label << "#{variable}: #{pattern_label}"
99
+ data[variable] = pattern
100
+ end
101
+ yield(label, data)
102
+ end
103
+ end
104
+ end
105
+
106
+ def each_raw_pattern(variables, &block)
107
+ return if variables.empty?
108
+
109
+ sorted_variables = variables.sort_by do |(variable, _), _|
110
+ variable
111
+ end
112
+ all_patterns = sorted_variables.collect do |(variable, patterns), _|
113
+ if patterns.is_a?(Hash)
114
+ patterns.collect do |pattern_label, pattern|
115
+ [variable, pattern, pattern_label]
116
+ end
117
+ else
118
+ patterns.collect do |pattern|
119
+ [variable, pattern, pattern.inspect]
120
+ end
121
+ end
122
+ end
123
+ all_patterns[0].product(*all_patterns[1..-1], &block)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -1,3 +1,5 @@
1
+ require "test/unit/data-sets"
2
+
1
3
  module Test
2
4
  module Unit
3
5
  module Data
@@ -12,9 +14,12 @@ module Test
12
14
  #
13
15
  # Define test data in the test code.
14
16
  #
15
- # @overload data(label, data)
17
+ # @overload data(label, data, options={})
16
18
  # @param [String] label specify test case name.
17
19
  # @param data specify test data.
20
+ # @param [Hash] options specify options.
21
+ # @option options [Boolean] :keep whether or not to use
22
+ # this data in the following test methods
18
23
  #
19
24
  # @example data(label, data)
20
25
  # data("empty string", [true, ""])
@@ -24,9 +29,39 @@ module Test
24
29
  # assert_equal(expected, target.empty?)
25
30
  # end
26
31
  #
27
- # @overload data(data_set)
32
+ # @overload data(variable, patterns, options={})
33
+ # @param [Symbol] variable specify test pattern variable name.
34
+ # @param [Array] patterns specify test patterns for the variable.
35
+ # @param [Hash] options specify options.
36
+ # @option options [Boolean] :keep whether or not to use
37
+ # this data in the following test methods
38
+ # @option options [Object] :group the test pattern group.
39
+ # Test matrix is generated for each test pattern group separately.
40
+ #
41
+ # @example data(variable, patterns)
42
+ # data(:x, [1, 2, 3])
43
+ # data(:y, ["a", "b"])
44
+ # def test_patterns(data)
45
+ # # 3 * 2 times executed
46
+ # # 3: the number of patterns of :x
47
+ # # 2: the number of patterns of :y
48
+ # p data
49
+ # # => {:x => 1, :y => "a"}
50
+ # # => {:x => 1, :y => "b"}
51
+ # # => {:x => 2, :y => "a"}
52
+ # # => {:x => 2, :y => "b"}
53
+ # # => {:x => 3, :y => "a"}
54
+ # # => {:x => 3, :y => "b"}
55
+ # end
56
+ #
57
+ # Generates test matrix from variable and patterns pairs.
58
+ #
59
+ # @overload data(data_set, options={})
28
60
  # @param [Hash] data_set specify test data as a Hash that
29
61
  # key is test label and value is test data.
62
+ # @param [Hash] options specify options.
63
+ # @option options [Boolean] :keep whether or not to use
64
+ # this data in the following test methods
30
65
  #
31
66
  # @example data(data_set)
32
67
  # data("empty string" => [true, ""],
@@ -36,9 +71,12 @@ module Test
36
71
  # assert_equal(expected, target.empty?)
37
72
  # end
38
73
  #
39
- # @overload data(&block)
40
- # @yieldreturn [Hash] return test data set as a Hash that
41
- # key is test label and value is test data.
74
+ # @overload data(options={}, &block)
75
+ # @param [Hash] options specify options.
76
+ # @option options [Boolean] :keep whether or not to use
77
+ # this data in the following test methods
78
+ # @yieldreturn [Hash<String, Object>] return test data set
79
+ # as a Hash that key is test label and value is test data.
42
80
  #
43
81
  # @example data(&block)
44
82
  # data do
@@ -52,22 +90,93 @@ module Test
52
90
  # assert_equal(expected, target.empty?)
53
91
  # end
54
92
  #
93
+ # @overload data(options={}, &block)
94
+ # @param [Hash] options specify options.
95
+ # @option options [Boolean] :keep whether or not to use
96
+ # this data in the following test methods
97
+ # @yieldreturn [Array<Symbol, Array>] return test data set
98
+ # as an Array of variable and patterns.
99
+ #
100
+ # @example data(&block)
101
+ # data do
102
+ # patterns = 3.times.to_a
103
+ # [:x, patterns]
104
+ # end
105
+ # data do
106
+ # patterns = []
107
+ # character = "a"
108
+ # 2.times.each do
109
+ # patterns << character
110
+ # character = character.succ
111
+ # end
112
+ # [:y, patterns]
113
+ # end
114
+ # def test_patterns(data)
115
+ # # 3 * 2 times executed
116
+ # # 3: the number of patterns of :x
117
+ # # 2: the number of patterns of :y
118
+ # p data
119
+ # # => {:x => 0, :y => "a"}
120
+ # # => {:x => 0, :y => "b"}
121
+ # # => {:x => 1, :y => "a"}
122
+ # # => {:x => 1, :y => "b"}
123
+ # # => {:x => 2, :y => "a"}
124
+ # # => {:x => 2, :y => "b"}
125
+ # end
126
+ #
127
+ # Generates test matrix from variable and patterns pairs.
128
+ #
55
129
  def data(*arguments, &block)
130
+ options = nil
56
131
  n_arguments = arguments.size
57
132
  case n_arguments
58
133
  when 0
59
134
  raise ArgumentError, "no block is given" unless block_given?
60
135
  data_set = block
61
136
  when 1
62
- data_set = arguments[0]
137
+ if block_given?
138
+ data_set = block
139
+ options = arguments[0]
140
+ else
141
+ data_set = arguments[0]
142
+ end
63
143
  when 2
64
- data_set = {arguments[0] => arguments[1]}
144
+ case arguments[0]
145
+ when String
146
+ data_set = {arguments[0] => arguments[1]}
147
+ when Hash
148
+ data_set = arguments[0]
149
+ options = arguments[1]
150
+ else
151
+ variable = arguments[0]
152
+ patterns = arguments[1]
153
+ data_set = [variable, patterns]
154
+ end
155
+ when 3
156
+ case arguments[0]
157
+ when String
158
+ data_set = {arguments[0] => arguments[1]}
159
+ options = arguments[2]
160
+ else
161
+ variable = arguments[0]
162
+ patterns = arguments[1]
163
+ data_set = [variable, patterns]
164
+ options = arguments[2]
165
+ end
65
166
  else
66
- message = "wrong number arguments(#{n_arguments} for 1..2)"
167
+ message = "wrong number arguments(#{n_arguments} for 0..3)"
67
168
  raise ArgumentError, message
68
169
  end
69
- current_data = current_attribute(:data)[:value] || []
70
- attribute(:data, current_data + [data_set])
170
+ options ||= {}
171
+ data_sets = current_attribute(:data)[:value] || DataSets.new
172
+ data_sets.add(data_set, options)
173
+ if options[:keep] or data_sets.have_keep?
174
+ keep_hook = lambda do |attr|
175
+ attr.merge(value: attr[:value].keep)
176
+ end
177
+ options = options.merge(keep: true, keep_hook: keep_hook)
178
+ end
179
+ attribute(:data, data_sets, options)
71
180
  end
72
181
 
73
182
  # This method provides Data-Driven-Test functionality.
@@ -78,7 +187,7 @@ module Test
78
187
  #
79
188
  # @param [String] file_name full path to test data file.
80
189
  # File format is automatically detected from filename extension.
81
- # @raise [ArgumentError] if +file_name+ is not supported file format.
190
+ # @raise [ArgumentError] if `file_name` is not supported file format.
82
191
  # @see Loader#load
83
192
  #
84
193
  # @example Load data from CSV file
@@ -102,7 +211,7 @@ module Test
102
211
  #
103
212
  # @param [String] file_name full path to test data file.
104
213
  # File format is automatically detected from filename extension.
105
- # @raise [ArgumentError] if +file_name+ is not supported file format.
214
+ # @raise [ArgumentError] if `file_name` is not supported file format.
106
215
  # @see #load_csv
107
216
  # @see #load_tsv
108
217
  # @api private
@@ -3,9 +3,8 @@
3
3
  # Copyright (c) 2001-2008 Python Software Foundation; All Rights Reserved
4
4
  # Copyright (c) 2008-2011 Kouhei Sutou; All Rights Reserved
5
5
  #
6
- # It is free software, and is distributed under the Ruby
7
- # license and/or the PSF license. See the COPYING file and
8
- # PSFL file.
6
+ # It is free software, and is distributed under (the new Ruby license
7
+ # or BSDL) and the PSF license.
9
8
 
10
9
  module Test
11
10
  module Unit
@@ -269,7 +268,7 @@ module Test
269
268
 
270
269
  private
271
270
  def tag(mark, contents)
272
- contents.collect {|content| "#{mark}#{content}"}
271
+ contents.collect {|content| mark + content}
273
272
  end
274
273
  end
275
274
 
@@ -450,7 +449,7 @@ module Test
450
449
 
451
450
  def tag(mark, contents)
452
451
  contents.each do |content|
453
- @result << "#{mark}#{content}"
452
+ @result << (mark + content)
454
453
  end
455
454
  end
456
455
 
@@ -577,15 +576,15 @@ module Test
577
576
  to_width = compute_width(to_line, to_start, to_end)
578
577
  case tag
579
578
  when :replace
580
- from_tags << "^" * from_width
581
- to_tags << "^" * to_width
579
+ from_tags += "^" * from_width
580
+ to_tags += "^" * to_width
582
581
  when :delete
583
- from_tags << "-" * from_width
582
+ from_tags += "-" * from_width
584
583
  when :insert
585
- to_tags << "+" * to_width
584
+ to_tags += "+" * to_width
586
585
  when :equal
587
- from_tags << " " * from_width
588
- to_tags << " " * to_width
586
+ from_tags += " " * from_width
587
+ to_tags += " " * to_width
589
588
  else
590
589
  raise "unknown tag: #{tag}"
591
590
  end