pmdtester 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ci/build.sh +91 -0
- data/.ci/files/env.gpg +1 -0
- data/.github/workflows/build.yml +39 -0
- data/.gitignore +9 -0
- data/.hoerc +1 -1
- data/.rubocop.yml +6 -2
- data/.ruby-version +1 -0
- data/History.md +40 -0
- data/Manifest.txt +24 -9
- data/README.rdoc +45 -30
- data/Rakefile +5 -3
- data/config/all-java.xml +1 -1
- data/config/design.xml +1 -1
- data/config/projectlist_1_0_0.xsd +2 -1
- data/config/projectlist_1_1_0.xsd +31 -0
- data/lib/pmdtester.rb +8 -7
- data/lib/pmdtester/builders/liquid_renderer.rb +73 -0
- data/lib/pmdtester/builders/pmd_report_builder.rb +102 -78
- data/lib/pmdtester/builders/project_builder.rb +100 -0
- data/lib/pmdtester/builders/project_hasher.rb +126 -0
- data/lib/pmdtester/builders/rule_set_builder.rb +92 -47
- data/lib/pmdtester/builders/simple_progress_logger.rb +4 -4
- data/lib/pmdtester/builders/summary_report_builder.rb +62 -131
- data/lib/pmdtester/collection_by_file.rb +55 -0
- data/lib/pmdtester/parsers/options.rb +19 -0
- data/lib/pmdtester/parsers/pmd_report_document.rb +74 -29
- data/lib/pmdtester/parsers/projects_parser.rb +2 -4
- data/lib/pmdtester/pmd_branch_detail.rb +29 -19
- data/lib/pmdtester/pmd_configerror.rb +23 -24
- data/lib/pmdtester/pmd_error.rb +34 -34
- data/lib/pmdtester/pmd_report_detail.rb +9 -12
- data/lib/pmdtester/pmd_tester_utils.rb +55 -0
- data/lib/pmdtester/pmd_violation.rb +66 -28
- data/lib/pmdtester/project.rb +21 -48
- data/lib/pmdtester/report_diff.rb +179 -111
- data/lib/pmdtester/resource_locator.rb +4 -0
- data/lib/pmdtester/runner.rb +66 -64
- data/pmdtester.gemspec +27 -36
- data/resources/_includes/diff_pill_row.html +6 -0
- data/resources/css/bootstrap.min.css +7 -0
- data/resources/css/datatables.min.css +36 -0
- data/resources/css/pmd-tester.css +131 -0
- data/resources/js/bootstrap.min.js +7 -0
- data/resources/js/code-snippets.js +66 -0
- data/resources/js/datatables.min.js +726 -0
- data/resources/js/jquery-3.2.1.slim.min.js +4 -0
- data/resources/js/jquery.min.js +2 -0
- data/resources/js/popper.min.js +5 -0
- data/resources/js/project-report.js +136 -0
- data/resources/project_diff_report.html +205 -0
- data/resources/project_index.html +102 -0
- metadata +64 -20
- data/.travis.yml +0 -40
- data/lib/pmdtester/builders/diff_builder.rb +0 -31
- data/lib/pmdtester/builders/diff_report/configerrors.rb +0 -50
- data/lib/pmdtester/builders/diff_report/errors.rb +0 -71
- data/lib/pmdtester/builders/diff_report/violations.rb +0 -77
- data/lib/pmdtester/builders/diff_report_builder.rb +0 -99
- data/lib/pmdtester/builders/html_report_builder.rb +0 -56
- data/resources/css/maven-base.css +0 -155
- data/resources/css/maven-theme.css +0 -171
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PmdTester
|
4
|
+
# A collection of things, grouped by file.
|
5
|
+
#
|
6
|
+
# (Note: this replaces PmdErrors and PmdViolations)
|
7
|
+
class CollectionByFile
|
8
|
+
def initialize
|
9
|
+
# a hash of filename -> [list of items]
|
10
|
+
@hash = Hash.new([])
|
11
|
+
@total = 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_all(filename, values)
|
15
|
+
return if values.empty?
|
16
|
+
|
17
|
+
if @hash.key?(filename)
|
18
|
+
@hash[filename].concat(values)
|
19
|
+
else
|
20
|
+
@hash[filename] = values
|
21
|
+
end
|
22
|
+
@total += values.size
|
23
|
+
end
|
24
|
+
|
25
|
+
def total_size
|
26
|
+
@total
|
27
|
+
end
|
28
|
+
|
29
|
+
def all_files
|
30
|
+
@hash.keys
|
31
|
+
end
|
32
|
+
|
33
|
+
def num_files
|
34
|
+
@hash.size
|
35
|
+
end
|
36
|
+
|
37
|
+
def all_values
|
38
|
+
@hash.values.flatten
|
39
|
+
end
|
40
|
+
|
41
|
+
def each_value(&block)
|
42
|
+
@hash.each_value do |vs|
|
43
|
+
vs.each(&block)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def [](fname)
|
48
|
+
@hash[fname]
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_h
|
52
|
+
@hash
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -16,6 +16,7 @@ module PmdTester
|
|
16
16
|
SINGLE = 'single'
|
17
17
|
DEFAULT_CONFIG_PATH = ResourceLocator.locate('config/all-java.xml')
|
18
18
|
DEFAULT_LIST_PATH = ResourceLocator.locate('config/project-list.xml')
|
19
|
+
DEFAULT_BASELINE_URL_PREFIX = 'https://sourceforge.net/projects/pmd/files/pmd-regression-tester/'
|
19
20
|
|
20
21
|
attr_reader :local_git_repo
|
21
22
|
attr_reader :base_branch
|
@@ -30,6 +31,9 @@ module PmdTester
|
|
30
31
|
attr_reader :auto_config_flag
|
31
32
|
attr_reader :debug_flag
|
32
33
|
attr_accessor :filter_set
|
34
|
+
attr_reader :keep_reports
|
35
|
+
attr_reader :error_recovery
|
36
|
+
attr_reader :baseline_download_url_prefix
|
33
37
|
|
34
38
|
def initialize(argv)
|
35
39
|
options = parse(argv)
|
@@ -46,6 +50,14 @@ module PmdTester
|
|
46
50
|
@auto_config_flag = options[:a]
|
47
51
|
@debug_flag = options[:d]
|
48
52
|
@filter_set = nil
|
53
|
+
@keep_reports = options.keep_reports?
|
54
|
+
@error_recovery = options.error_recovery?
|
55
|
+
url = options[:baseline_download_url]
|
56
|
+
@baseline_download_url_prefix = if url[-1] == '/'
|
57
|
+
url
|
58
|
+
else
|
59
|
+
"#{url}/"
|
60
|
+
end
|
49
61
|
|
50
62
|
# if the 'config' option is selected then `config` overrides `base_config` and `patch_config`
|
51
63
|
@base_config = @config if !@config.nil? && @mode == 'local'
|
@@ -88,8 +100,15 @@ module PmdTester
|
|
88
100
|
o.bool '-a', '--auto-gen-config',
|
89
101
|
'whether to generate configurations automatically based on branch differences,' \
|
90
102
|
'this option only works in online and local mode'
|
103
|
+
o.bool '--keep-reports',
|
104
|
+
'whether to keep old reports and skip running PMD again if possible'
|
91
105
|
o.bool '-d', '--debug',
|
92
106
|
'whether change log level to DEBUG to see more information'
|
107
|
+
o.bool '--error-recovery',
|
108
|
+
'enable error recovery mode when executing PMD. Might help to analyze errors.'
|
109
|
+
o.string '--baseline-download-url',
|
110
|
+
'download url prefix from where to download the baseline in online mode',
|
111
|
+
default: DEFAULT_BASELINE_URL_PREFIX
|
93
112
|
o.on '-v', '--version' do
|
94
113
|
puts VERSION
|
95
114
|
exit
|
@@ -8,80 +8,125 @@ module PmdTester
|
|
8
8
|
attr_reader :violations
|
9
9
|
attr_reader :errors
|
10
10
|
attr_reader :configerrors
|
11
|
+
attr_reader :infos_by_rules
|
12
|
+
|
11
13
|
def initialize(branch_name, working_dir, filter_set = nil)
|
12
|
-
@violations =
|
13
|
-
@errors =
|
14
|
-
@configerrors =
|
14
|
+
@violations = CollectionByFile.new
|
15
|
+
@errors = CollectionByFile.new
|
16
|
+
@configerrors = Hash.new { |hash, key| hash[key] = [] }
|
17
|
+
|
18
|
+
@infos_by_rules = {}
|
15
19
|
@current_violations = []
|
16
20
|
@current_violation = nil
|
17
21
|
@current_error = nil
|
18
22
|
@current_configerror = nil
|
19
|
-
@current_element = ''
|
20
|
-
@filename = ''
|
21
23
|
@filter_set = filter_set
|
22
24
|
@working_dir = working_dir
|
23
25
|
@branch_name = branch_name
|
26
|
+
|
27
|
+
@cur_text = String.new(capacity: 200)
|
24
28
|
end
|
25
29
|
|
26
30
|
def start_element(name, attrs = [])
|
27
31
|
attrs = attrs.to_h
|
28
|
-
@current_element = name
|
29
32
|
|
30
33
|
case name
|
31
34
|
when 'file'
|
32
|
-
|
33
|
-
@current_filename = remove_work_dir!(attrs['name'])
|
35
|
+
handle_start_file attrs
|
34
36
|
when 'violation'
|
35
|
-
|
37
|
+
handle_start_violation attrs
|
36
38
|
when 'error'
|
37
|
-
|
38
|
-
remove_work_dir!(attrs['msg'])
|
39
|
-
@current_error = PmdError.new(attrs, @branch_name)
|
39
|
+
handle_start_error attrs
|
40
40
|
when 'configerror'
|
41
|
-
|
41
|
+
handle_start_configerror attrs
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
|
-
def
|
46
|
-
|
45
|
+
def characters(string)
|
46
|
+
@cur_text << remove_work_dir!(string)
|
47
47
|
end
|
48
48
|
|
49
|
-
def
|
50
|
-
@
|
49
|
+
def cdata_block(string)
|
50
|
+
@cur_text << remove_work_dir!(string)
|
51
51
|
end
|
52
52
|
|
53
53
|
def end_element(name)
|
54
54
|
case name
|
55
55
|
when 'file'
|
56
|
-
@violations.
|
56
|
+
@violations.add_all(@current_filename, @current_violations)
|
57
57
|
@current_filename = nil
|
58
58
|
when 'violation'
|
59
|
-
@current_violation
|
60
|
-
|
59
|
+
if match_filter_set?(@current_violation)
|
60
|
+
@current_violation.message = finish_text!
|
61
|
+
@current_violations.push(@current_violation)
|
62
|
+
end
|
61
63
|
@current_violation = nil
|
62
64
|
when 'error'
|
63
|
-
@
|
65
|
+
@current_error.stack_trace = finish_text!
|
66
|
+
@errors.add_all(@current_filename, [@current_error])
|
64
67
|
@current_filename = nil
|
65
68
|
@current_error = nil
|
66
69
|
when 'configerror'
|
67
|
-
@configerrors.
|
70
|
+
@configerrors[@current_configerror.rulename].push(@current_configerror)
|
68
71
|
@current_configerror = nil
|
69
72
|
end
|
73
|
+
@cur_text.clear
|
70
74
|
end
|
71
75
|
|
72
|
-
|
73
|
-
|
74
|
-
|
76
|
+
private
|
77
|
+
|
78
|
+
# Modifies the string in place and returns it
|
79
|
+
# (this is what sub! does, except it returns nil if no replacement occurred)
|
80
|
+
def remove_work_dir!(str)
|
81
|
+
str.sub!(/#{@working_dir}/, '')
|
82
|
+
str
|
83
|
+
end
|
84
|
+
|
85
|
+
def finish_text!
|
86
|
+
res = @cur_text.strip!.dup.freeze
|
87
|
+
@cur_text.clear
|
88
|
+
res
|
75
89
|
end
|
76
90
|
|
77
91
|
def match_filter_set?(violation)
|
78
92
|
return true if @filter_set.nil?
|
79
93
|
|
80
|
-
|
81
|
-
|
82
|
-
|
94
|
+
ruleset_attr = violation.ruleset_name.delete(' ').downcase! << '.xml'
|
95
|
+
return true if @filter_set.include?(ruleset_attr)
|
96
|
+
|
97
|
+
rule_ref = "#{ruleset_attr}/#{violation.rule_name}"
|
98
|
+
|
99
|
+
@filter_set.include?(rule_ref)
|
100
|
+
end
|
101
|
+
|
102
|
+
def handle_start_file(attrs)
|
103
|
+
@current_filename = remove_work_dir!(attrs['name'])
|
104
|
+
@current_violations = []
|
105
|
+
end
|
106
|
+
|
107
|
+
def handle_start_violation(attrs)
|
108
|
+
@current_violation = PmdViolation.new(
|
109
|
+
branch: @branch_name,
|
110
|
+
fname: @current_filename,
|
111
|
+
info_url: attrs['externalInfoUrl'],
|
112
|
+
bline: attrs['beginline'].to_i,
|
113
|
+
rule_name: attrs['rule'],
|
114
|
+
ruleset_name: attrs['ruleset'].freeze
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
def handle_start_error(attrs)
|
119
|
+
@current_filename = remove_work_dir!(attrs['filename'])
|
120
|
+
|
121
|
+
@current_error = PmdError.new(
|
122
|
+
branch: @branch_name,
|
123
|
+
filename: @current_filename,
|
124
|
+
short_message: remove_work_dir!(attrs['msg'])
|
125
|
+
)
|
126
|
+
end
|
83
127
|
|
84
|
-
|
128
|
+
def handle_start_configerror(attrs)
|
129
|
+
@current_configerror = PmdConfigError.new(attrs, @branch_name)
|
85
130
|
end
|
86
131
|
end
|
87
132
|
end
|
@@ -11,9 +11,7 @@ module PmdTester
|
|
11
11
|
document = Nokogiri::XML(File.read(list_file))
|
12
12
|
|
13
13
|
errors = schema.validate(document)
|
14
|
-
unless errors.empty?
|
15
|
-
raise ProjectsParserException.new(errors), "Schema validate failed: In #{list_file}"
|
16
|
-
end
|
14
|
+
raise ProjectsParserException.new(errors), "Schema validate failed: In #{list_file}" unless errors.empty?
|
17
15
|
|
18
16
|
projects = []
|
19
17
|
document.xpath('//project').each do |project|
|
@@ -23,7 +21,7 @@ module PmdTester
|
|
23
21
|
end
|
24
22
|
|
25
23
|
def schema_file_path
|
26
|
-
ResourceLocator.locate('config/
|
24
|
+
ResourceLocator.locate('config/projectlist_1_1_0.xsd')
|
27
25
|
end
|
28
26
|
end
|
29
27
|
|
@@ -12,8 +12,9 @@ module PmdTester
|
|
12
12
|
attr_accessor :branch_name
|
13
13
|
# The branch's execution time on all standard projects
|
14
14
|
attr_accessor :execution_time
|
15
|
-
|
16
|
-
|
15
|
+
attr_accessor :jdk_version
|
16
|
+
attr_accessor :language
|
17
|
+
attr_accessor :pull_request
|
17
18
|
|
18
19
|
def self.branch_filename(branch_name)
|
19
20
|
branch_name&.tr('/', '_')
|
@@ -28,24 +29,29 @@ module PmdTester
|
|
28
29
|
@execution_time = 0
|
29
30
|
# the result of command 'java -version' is going to stderr
|
30
31
|
@jdk_version = Cmd.stderr_of('java -version')
|
31
|
-
@language = ENV['LANG']
|
32
|
+
@language = ENV['LANG'] # the locale
|
33
|
+
|
34
|
+
prnum = ENV[PR_NUM_ENV_VAR]
|
35
|
+
@pull_request = prnum == 'false' ? nil : prnum
|
32
36
|
end
|
33
37
|
|
34
|
-
def load
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
38
|
+
def self.load(branch_name, logger)
|
39
|
+
details = PmdBranchDetail.new(branch_name)
|
40
|
+
if File.exist?(details.path_to_save_file)
|
41
|
+
hash = JSON.parse(File.read(details.path_to_save_file))
|
42
|
+
details.branch_last_sha = hash['branch_last_sha']
|
43
|
+
details.branch_last_message = hash['branch_last_message']
|
44
|
+
details.branch_name = hash['branch_name']
|
45
|
+
details.execution_time = hash['execution_time']
|
46
|
+
details.jdk_version = hash['jdk_version']
|
47
|
+
details.language = hash['language']
|
48
|
+
details.pull_request = hash['pull_request']
|
43
49
|
else
|
44
|
-
|
45
|
-
|
46
|
-
logger
|
50
|
+
details.jdk_version = ''
|
51
|
+
details.language = ''
|
52
|
+
logger&.warn "#{details.path_to_save_file} doesn't exist!"
|
47
53
|
end
|
48
|
-
|
54
|
+
details
|
49
55
|
end
|
50
56
|
|
51
57
|
def save
|
@@ -54,14 +60,18 @@ module PmdTester
|
|
54
60
|
branch_name: @branch_name,
|
55
61
|
execution_time: @execution_time,
|
56
62
|
jdk_version: @jdk_version,
|
57
|
-
language: @language
|
58
|
-
|
63
|
+
language: @language,
|
64
|
+
pull_request: @pull_request }
|
65
|
+
|
66
|
+
FileUtils.mkdir_p(@base_branch_dir) unless File.directory?(@base_branch_dir)
|
67
|
+
|
68
|
+
file = File.new(path_to_save_file, 'w')
|
59
69
|
file.puts JSON.generate(hash)
|
60
70
|
file.close
|
61
71
|
self
|
62
72
|
end
|
63
73
|
|
64
|
-
def
|
74
|
+
def path_to_save_file
|
65
75
|
"#{@base_branch_dir}/branch_info.json"
|
66
76
|
end
|
67
77
|
|
@@ -1,28 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module PmdTester
|
4
|
-
# This class is used to store pmd config errors and its size.
|
5
|
-
class PmdConfigErrors
|
6
|
-
attr_reader :errors
|
7
|
-
attr_reader :size
|
8
|
-
|
9
|
-
def initialize
|
10
|
-
# key:rulename as String => value:PmdConfigError Array
|
11
|
-
@errors = {}
|
12
|
-
@size = 0
|
13
|
-
end
|
14
|
-
|
15
|
-
def add_error(error)
|
16
|
-
rulename = error.rulename
|
17
|
-
if @errors.key?(rulename)
|
18
|
-
@errors[rulename].push(error)
|
19
|
-
else
|
20
|
-
@errors.store(rulename, [error])
|
21
|
-
end
|
22
|
-
@size += 1
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
4
|
# This class represents a 'configerror' element of Pmd xml report
|
27
5
|
# and which Pmd branch the 'configerror' is from
|
28
6
|
class PmdConfigError
|
@@ -35,13 +13,13 @@ module PmdTester
|
|
35
13
|
# <xs:attribute name="msg" type="xs:string" use="required" />
|
36
14
|
# </xs:complexType>
|
37
15
|
attr_reader :attrs
|
38
|
-
attr_accessor :
|
16
|
+
attr_accessor :old_error
|
39
17
|
|
40
18
|
def initialize(attrs, branch)
|
41
19
|
@attrs = attrs
|
42
20
|
|
21
|
+
@changed = false
|
43
22
|
@branch = branch
|
44
|
-
@text = ''
|
45
23
|
end
|
46
24
|
|
47
25
|
def rulename
|
@@ -52,10 +30,31 @@ module PmdTester
|
|
52
30
|
@attrs['msg']
|
53
31
|
end
|
54
32
|
|
33
|
+
def sort_key
|
34
|
+
rulename
|
35
|
+
end
|
36
|
+
|
37
|
+
def changed?
|
38
|
+
@changed
|
39
|
+
end
|
40
|
+
|
55
41
|
def eql?(other)
|
56
42
|
rulename.eql?(other.rulename) && msg.eql?(other.msg)
|
57
43
|
end
|
58
44
|
|
45
|
+
def try_merge?(other)
|
46
|
+
if branch != BASE &&
|
47
|
+
branch != other.branch &&
|
48
|
+
rulename == other.rulename &&
|
49
|
+
!changed? # not already changed
|
50
|
+
@changed = true
|
51
|
+
@old_error = other
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
false
|
56
|
+
end
|
57
|
+
|
59
58
|
def hash
|
60
59
|
[rulename, msg].hash
|
61
60
|
end
|
data/lib/pmdtester/pmd_error.rb
CHANGED
@@ -1,30 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module PmdTester
|
4
|
-
# This class is used to store pmd errors and its size.
|
5
|
-
class PmdErrors
|
6
|
-
attr_reader :errors
|
7
|
-
attr_reader :errors_size
|
8
|
-
|
9
|
-
def initialize
|
10
|
-
# key:filename as String => value:PmdError Array
|
11
|
-
@errors = {}
|
12
|
-
@errors_size = 0
|
13
|
-
end
|
14
|
-
|
15
|
-
def add_error_by_filename(filename, error)
|
16
|
-
if @errors.key?(filename)
|
17
|
-
@errors[filename].push(error)
|
18
|
-
else
|
19
|
-
@errors.store(filename, [error])
|
20
|
-
end
|
21
|
-
@errors_size += 1
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
4
|
# This class represents a 'error' element of Pmd xml report
|
26
5
|
# and which Pmd branch the 'error' is from
|
27
6
|
class PmdError
|
7
|
+
include PmdTester
|
28
8
|
# The pmd branch type, 'base' or 'patch'
|
29
9
|
attr_reader :branch
|
30
10
|
|
@@ -37,31 +17,51 @@ module PmdTester
|
|
37
17
|
# </xs:extension>
|
38
18
|
# </xs:simpleContent>
|
39
19
|
# </xs:complexType>
|
40
|
-
|
41
|
-
attr_accessor :
|
42
|
-
|
43
|
-
def initialize(attrs, branch)
|
44
|
-
@attrs = attrs
|
20
|
+
attr_accessor :stack_trace
|
21
|
+
attr_accessor :old_error
|
22
|
+
attr_reader :filename, :short_message
|
45
23
|
|
24
|
+
def initialize(branch:, filename:, short_message:)
|
46
25
|
@branch = branch
|
47
|
-
@
|
26
|
+
@stack_trace = ''
|
27
|
+
@changed = false
|
28
|
+
@short_message = short_message
|
29
|
+
@filename = filename
|
48
30
|
end
|
49
31
|
|
50
|
-
def
|
51
|
-
|
32
|
+
def short_filename
|
33
|
+
filename.gsub(%r{([^/]*+/)+}, '')
|
52
34
|
end
|
53
35
|
|
54
|
-
def
|
55
|
-
@
|
36
|
+
def changed?
|
37
|
+
@changed
|
56
38
|
end
|
57
39
|
|
58
40
|
def eql?(other)
|
59
|
-
filename.eql?(other.filename) &&
|
60
|
-
|
41
|
+
filename.eql?(other.filename) &&
|
42
|
+
short_message.eql?(other.short_message) &&
|
43
|
+
stack_trace.eql?(other.stack_trace)
|
61
44
|
end
|
62
45
|
|
63
46
|
def hash
|
64
|
-
[filename,
|
47
|
+
[filename, stack_trace].hash
|
48
|
+
end
|
49
|
+
|
50
|
+
def sort_key
|
51
|
+
filename
|
52
|
+
end
|
53
|
+
|
54
|
+
def try_merge?(other)
|
55
|
+
if branch != BASE &&
|
56
|
+
branch != other.branch &&
|
57
|
+
filename == other.filename &&
|
58
|
+
!changed? # not already changed
|
59
|
+
@changed = true
|
60
|
+
@old_error = other
|
61
|
+
true
|
62
|
+
else
|
63
|
+
false
|
64
|
+
end
|
65
65
|
end
|
66
66
|
end
|
67
67
|
end
|