pmdtester 1.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.ci/build.sh +99 -0
  3. data/.ci/inc/fetch_ci_scripts.bash +19 -0
  4. data/.ci/manual-integration-tests.sh +37 -0
  5. data/.github/workflows/build.yml +55 -0
  6. data/.github/workflows/manual-integration-tests.yml +43 -0
  7. data/.gitignore +9 -0
  8. data/.hoerc +1 -1
  9. data/.rubocop.yml +9 -2
  10. data/.ruby-version +1 -0
  11. data/History.md +79 -0
  12. data/Manifest.txt +28 -9
  13. data/README.rdoc +59 -33
  14. data/Rakefile +7 -5
  15. data/config/all-java.xml +1 -1
  16. data/config/design.xml +1 -1
  17. data/config/project-list.xml +8 -7
  18. data/config/projectlist_1_0_0.xsd +2 -1
  19. data/config/projectlist_1_1_0.xsd +31 -0
  20. data/config/projectlist_1_2_0.xsd +39 -0
  21. data/lib/pmdtester.rb +8 -7
  22. data/lib/pmdtester/builders/liquid_renderer.rb +130 -0
  23. data/lib/pmdtester/builders/pmd_report_builder.rb +107 -79
  24. data/lib/pmdtester/builders/project_builder.rb +105 -0
  25. data/lib/pmdtester/builders/project_hasher.rb +128 -0
  26. data/lib/pmdtester/builders/rule_set_builder.rb +96 -47
  27. data/lib/pmdtester/builders/simple_progress_logger.rb +4 -4
  28. data/lib/pmdtester/builders/summary_report_builder.rb +63 -131
  29. data/lib/pmdtester/collection_by_file.rb +55 -0
  30. data/lib/pmdtester/parsers/options.rb +24 -0
  31. data/lib/pmdtester/parsers/pmd_report_document.rb +72 -28
  32. data/lib/pmdtester/parsers/projects_parser.rb +2 -4
  33. data/lib/pmdtester/pmd_branch_detail.rb +35 -19
  34. data/lib/pmdtester/pmd_configerror.rb +23 -24
  35. data/lib/pmdtester/pmd_error.rb +34 -34
  36. data/lib/pmdtester/pmd_report_detail.rb +10 -13
  37. data/lib/pmdtester/pmd_tester_utils.rb +58 -0
  38. data/lib/pmdtester/pmd_violation.rb +66 -28
  39. data/lib/pmdtester/project.rb +42 -56
  40. data/lib/pmdtester/report_diff.rb +203 -109
  41. data/lib/pmdtester/resource_locator.rb +4 -0
  42. data/lib/pmdtester/runner.rb +67 -64
  43. data/pmdtester.gemspec +28 -37
  44. data/resources/_includes/diff_pill_row.html +6 -0
  45. data/resources/css/bootstrap.min.css +7 -0
  46. data/resources/css/datatables.min.css +36 -0
  47. data/resources/css/pmd-tester.css +132 -0
  48. data/resources/js/bootstrap.min.js +7 -0
  49. data/resources/js/code-snippets.js +73 -0
  50. data/resources/js/datatables.min.js +726 -0
  51. data/resources/js/jquery-3.2.1.slim.min.js +4 -0
  52. data/resources/js/jquery.min.js +2 -0
  53. data/resources/js/popper.min.js +5 -0
  54. data/resources/js/project-report.js +137 -0
  55. data/resources/project_diff_report.html +214 -0
  56. data/resources/project_index.html +113 -0
  57. data/resources/project_pmd_report.html +186 -0
  58. metadata +73 -25
  59. data/.travis.yml +0 -40
  60. data/lib/pmdtester/builders/diff_builder.rb +0 -31
  61. data/lib/pmdtester/builders/diff_report/configerrors.rb +0 -50
  62. data/lib/pmdtester/builders/diff_report/errors.rb +0 -71
  63. data/lib/pmdtester/builders/diff_report/violations.rb +0 -77
  64. data/lib/pmdtester/builders/diff_report_builder.rb +0 -99
  65. data/lib/pmdtester/builders/html_report_builder.rb +0 -56
  66. data/resources/css/maven-base.css +0 -155
  67. data/resources/css/maven-theme.css +0 -171
@@ -8,92 +8,74 @@ module PmdTester
8
8
  # projects and branch of pmd source code
9
9
  class PmdReportBuilder
10
10
  include PmdTester
11
- def initialize(branch_config, projects, local_git_repo, pmd_branch_name, threads = 1)
12
- @branch_config = branch_config
11
+
12
+ def initialize(projects, options, branch_config, branch_name)
13
13
  @projects = projects
14
- @local_git_repo = local_git_repo
15
- @pmd_branch_name = pmd_branch_name
16
- @threads = threads
14
+ @local_git_repo = options.local_git_repo
15
+ @threads = options.threads
16
+ @error_recovery = options.error_recovery
17
+ @branch_config = branch_config
18
+ @pmd_branch_name = branch_name
17
19
  @pwd = Dir.getwd
18
20
 
19
- @pmd_branch_details = PmdBranchDetail.new(pmd_branch_name)
20
- end
21
-
22
- def execute_reset_cmd(type, tag)
23
- case type
24
- when 'git'
25
- reset_cmd = "git reset --hard #{tag}"
26
- when 'hg'
27
- reset_cmd = "hg up #{tag}"
28
- end
29
-
30
- Cmd.execute(reset_cmd)
31
- end
32
-
33
- def clone_projects
34
- logger.info 'Cloning projects started'
35
-
36
- @projects.each do |project|
37
- logger.info "Start cloning #{project.name} repository"
38
- path = project.local_source_path
39
- clone_cmd = "#{project.type} clone #{project.connection} #{path}"
40
- if File.exist?(path)
41
- logger.warn "Skipping clone, project path #{path} already exists"
42
- else
43
- Cmd.execute(clone_cmd)
44
- end
45
-
46
- Dir.chdir(path) do
47
- execute_reset_cmd(project.type, project.tag)
48
- end
49
- logger.info "Cloning #{project.name} completed"
50
- end
51
-
52
- logger.info 'Cloning projects completed'
21
+ @pmd_branch_details = PmdBranchDetail.new(@pmd_branch_name)
22
+ @project_builder = ProjectBuilder.new(@projects)
53
23
  end
54
24
 
55
25
  def get_pmd_binary_file
56
26
  logger.info "#{@pmd_branch_name}: Start packaging PMD"
57
27
  Dir.chdir(@local_git_repo) do
58
- current_head_sha = Cmd.execute('git rev-parse HEAD')
59
- current_branch_sha = Cmd.execute("git rev-parse #{@pmd_branch_name}")
28
+ build_branch_sha = Cmd.execute("git rev-parse #{@pmd_branch_name}^{commit}")
29
+
30
+ checkout_build_branch # needs a clean working tree, otherwise fails
60
31
 
61
- @pmd_version = determine_pmd_version
32
+ raise "Wrong branch #{get_last_commit_sha}" unless build_branch_sha == get_last_commit_sha
62
33
 
63
- # in case we are already on the correct branch
64
- # and a binary zip already exists...
65
- if current_head_sha == current_branch_sha &&
66
- File.exist?("pmd-dist/target/pmd-bin-#{@pmd_version}.zip")
67
- logger.warn "#{@pmd_branch_name}: Skipping packaging - zip for " \
68
- "#{@pmd_version} already exists"
34
+ distro_path = saved_distro_path(build_branch_sha)
35
+ logger.debug "#{@pmd_branch_name}: PMD Version is #{@pmd_version} " \
36
+ "(sha=#{build_branch_sha})"
37
+ logger.debug "#{@pmd_branch_name}: distro_path=#{distro_path}"
38
+ if File.directory?(distro_path)
39
+ logger.info "#{@pmd_branch_name}: Skipping packaging - saved version exists " \
40
+ " in #{distro_path}"
69
41
  else
70
- build_pmd
42
+ build_pmd(into_dir: distro_path)
71
43
  end
72
44
 
73
- @pmd_branch_details.branch_last_sha = get_last_commit_sha
45
+ # we're still on the build branch
46
+ @pmd_branch_details.branch_last_sha = build_branch_sha
74
47
  @pmd_branch_details.branch_last_message = get_last_commit_message
75
-
76
- logger.info "#{@pmd_branch_name}: Extracting the zip"
77
- unzip_cmd = "unzip -qo pmd-dist/target/pmd-bin-#{@pmd_version}.zip -d #{@pwd}/target"
78
- Cmd.execute(unzip_cmd)
79
48
  end
80
49
  logger.info "#{@pmd_branch_name}: Packaging PMD completed"
81
50
  end
82
51
 
83
- def build_pmd
84
- logger.info "#{@pmd_branch_name}: Checking out the branch"
85
- checkout_cmd = "git checkout #{@pmd_branch_name}"
86
- Cmd.execute(checkout_cmd)
87
-
88
- # determine the version again - it might be different in the other branch
89
- @pmd_version = determine_pmd_version
52
+ # builds pmd on currently checked out branch
53
+ def build_pmd(into_dir:)
54
+ # in CI there might have been a build performed already. In that case
55
+ # we reuse the build result, otherwise we build PMD freshly
56
+ pmd_dist_target = "pmd-dist/target/pmd-bin-#{@pmd_version}.zip"
57
+ binary_exists = File.exist?(pmd_dist_target)
58
+ logger.debug "#{@pmd_branch_name}: Does the file #{pmd_dist_target} exist? #{binary_exists} (cwd: #{Dir.getwd})"
59
+ if binary_exists
60
+ # that's a warning, because we don't know, whether this build really
61
+ # belongs to the current branch or whether it's from a previous branch.
62
+ # In CI, that's not a problem, because the workspace is always fresh.
63
+ logger.warn "#{@pmd_branch_name}: Reusing already existing #{pmd_dist_target}"
64
+ else
65
+ logger.info "#{@pmd_branch_name}: Building PMD #{@pmd_version}..."
66
+ package_cmd = './mvnw clean package' \
67
+ ' -Dmaven.test.skip=true' \
68
+ ' -Dmaven.javadoc.skip=true' \
69
+ ' -Dmaven.source.skip=true' \
70
+ ' -Dcheckstyle.skip=true' \
71
+ ' -Dpmd.skip=true' \
72
+ ' -T1C -B'
73
+ Cmd.execute(package_cmd)
74
+ end
90
75
 
91
- logger.info "#{@pmd_branch_name}: Building PMD #{@pmd_version}..."
92
- package_cmd = './mvnw clean package' \
93
- ' -Dmaven.test.skip=true' \
94
- ' -Dmaven.javadoc.skip=true' \
95
- ' -Dmaven.source.skip=true'
96
- Cmd.execute(package_cmd)
76
+ logger.info "#{@pmd_branch_name}: Extracting the zip"
77
+ Cmd.execute("unzip -qo #{pmd_dist_target} -d pmd-dist/target/exploded")
78
+ Cmd.execute("mv pmd-dist/target/exploded/pmd-bin-#{@pmd_version} #{into_dir}")
97
79
  end
98
80
 
99
81
  def determine_pmd_version
@@ -103,7 +85,7 @@ module PmdTester
103
85
  end
104
86
 
105
87
  def get_last_commit_sha
106
- get_last_commit_sha_cmd = 'git rev-parse HEAD'
88
+ get_last_commit_sha_cmd = 'git rev-parse HEAD^{commit}'
107
89
  Cmd.execute(get_last_commit_sha_cmd)
108
90
  end
109
91
 
@@ -113,27 +95,39 @@ module PmdTester
113
95
  end
114
96
 
115
97
  def generate_pmd_report(project)
116
- run_path = "target/pmd-bin-#{@pmd_version}/bin/run.sh"
117
- pmd_cmd = "#{run_path} pmd -d #{project.local_source_path} -f xml " \
98
+ error_recovery_options = @error_recovery ? 'PMD_JAVA_OPTS="-Dpmd.error_recovery -ea" ' : ''
99
+ run_path = "#{saved_distro_path(@pmd_branch_details.branch_last_sha)}/bin/run.sh"
100
+ pmd_cmd = "#{error_recovery_options}" \
101
+ "#{run_path} pmd -d #{project.local_source_path} -f xml " \
118
102
  "-R #{project.get_config_path(@pmd_branch_name)} " \
119
103
  "-r #{project.get_pmd_report_path(@pmd_branch_name)} " \
120
- "-failOnViolation false -t #{@threads}"
104
+ "-failOnViolation false -t #{@threads} " \
105
+ "#{project.auxclasspath}"
121
106
  start_time = Time.now
122
- Cmd.execute(pmd_cmd)
107
+ if File.exist?(project.get_pmd_report_path(@pmd_branch_name))
108
+ logger.warn "#{@pmd_branch_name}: Skipping PMD run - report " \
109
+ "#{project.get_pmd_report_path(@pmd_branch_name)} already exists"
110
+ else
111
+ Cmd.execute(pmd_cmd)
112
+ end
123
113
  end_time = Time.now
124
114
  [end_time - start_time, end_time]
125
115
  end
126
116
 
127
117
  def generate_config_for(project)
118
+ logger.debug "Generating ruleset with excludes from #{@branch_config}"
128
119
  doc = Nokogiri::XML(File.read(@branch_config))
129
120
  ruleset = doc.at_css('ruleset')
130
- project.exclude_pattern.each do |exclude_pattern|
131
- ruleset.add_child("<exclude-pattern>#{exclude_pattern}</exclude-pattern>")
121
+ ruleset.add_child("\n")
122
+ project.exclude_patterns.each do |exclude_pattern|
123
+ ruleset.add_child(" <exclude-pattern>#{exclude_pattern}</exclude-pattern>\n")
132
124
  end
133
125
 
134
126
  File.open(project.get_config_path(@pmd_branch_name), 'w') do |x|
135
127
  x << doc.to_s
136
128
  end
129
+
130
+ logger.debug "Created file #{project.get_config_path(@pmd_branch_name)}"
137
131
  end
138
132
 
139
133
  def generate_pmd_reports
@@ -141,16 +135,14 @@ module PmdTester
141
135
 
142
136
  sum_time = 0
143
137
  @projects.each do |project|
144
- progress_logger = SimpleProgressLogger.new(project.name)
138
+ progress_logger = SimpleProgressLogger.new("generating #{project.name}'s PMD report")
145
139
  progress_logger.start
146
140
  generate_config_for(project)
147
141
  execution_time, end_time = generate_pmd_report(project)
148
142
  progress_logger.stop
149
143
  sum_time += execution_time
150
144
 
151
- report_details = PmdReportDetail.new
152
- report_details.execution_time = execution_time
153
- report_details.timestamp = end_time
145
+ report_details = PmdReportDetail.new(execution_time: execution_time, timestamp: end_time)
154
146
  report_details.save(project.get_report_info_path(@pmd_branch_name))
155
147
  logger.info "#{project.name}'s PMD report was generated successfully"
156
148
  end
@@ -161,10 +153,46 @@ module PmdTester
161
153
  @pmd_branch_details
162
154
  end
163
155
 
156
+ # returns the branch details
164
157
  def build
165
- clone_projects
158
+ @project_builder.clone_projects
159
+ @project_builder.build_projects
166
160
  get_pmd_binary_file
167
161
  generate_pmd_reports
168
162
  end
163
+
164
+ private
165
+
166
+ def checkout_build_branch
167
+ logger.info "#{@pmd_branch_name}: Checking out the branch"
168
+ # note that this would fail if the tree is dirty
169
+ Cmd.execute("git checkout #{@pmd_branch_name}")
170
+
171
+ # determine the version
172
+ @pmd_version = determine_pmd_version
173
+
174
+ return unless wd_has_dirty_git_changes
175
+
176
+ # working dir is dirty....
177
+ # we don't allow this because we need the SHA to address the zip file
178
+ logger.error "#{@pmd_branch_name}: Won\'t build without a clean working tree, " \
179
+ 'commit your changes'
180
+ end
181
+
182
+ def work_dir
183
+ "#{@pwd}/target"
184
+ end
185
+
186
+ # path to the unzipped distribution
187
+ # e.g. <cwd>/pmd-bin-<version>-<branch>-<sha>
188
+ def saved_distro_path(build_sha)
189
+ "#{work_dir}/pmd-bin-#{@pmd_version}" \
190
+ "-#{PmdBranchDetail.branch_filename(@pmd_branch_name)}" \
191
+ "-#{build_sha}"
192
+ end
193
+
194
+ def wd_has_dirty_git_changes
195
+ !Cmd.execute('git status --porcelain').empty?
196
+ end
169
197
  end
170
198
  end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tempfile'
5
+
6
+ module PmdTester
7
+ # Clones and builds the projects, that are configured in the project-list.xml
8
+ class ProjectBuilder
9
+ include PmdTester
10
+
11
+ def initialize(projects)
12
+ @projects = projects
13
+ end
14
+
15
+ def clone_projects
16
+ logger.info 'Cloning projects started'
17
+
18
+ @projects.each do |project|
19
+ logger.info "Start cloning #{project.name} repository"
20
+ path = project.clone_root_path
21
+
22
+ if File.exist?(path)
23
+ logger.warn "Skipping clone, project path #{path} already exists"
24
+ else
25
+ raise "Unsupported project type '#{project.type}' - only git is supported" unless project.type == 'git'
26
+
27
+ # git:
28
+ # Don't download whole history
29
+ # Note we don't use --single-branch, because the repo is downloaded
30
+ # once but may be used with several tags.
31
+ clone_cmd = "git clone --no-single-branch --depth 1 #{project.connection} #{path}"
32
+
33
+ Cmd.execute(clone_cmd)
34
+ end
35
+
36
+ Dir.chdir(path) do
37
+ execute_reset_cmd(project.type, project.tag)
38
+ end
39
+ logger.info "Cloning #{project.name} completed"
40
+ end
41
+
42
+ logger.info 'Cloning projects completed'
43
+ end
44
+
45
+ def build_projects
46
+ logger.info 'Building projects started'
47
+
48
+ @projects.each do |project|
49
+ path = project.clone_root_path
50
+ Dir.chdir(path) do
51
+ progress_logger = SimpleProgressLogger.new("building #{project.name} in #{path}")
52
+ progress_logger.start
53
+ prepare_project(project)
54
+ progress_logger.stop
55
+ end
56
+ logger.info "Building #{project.name} completed"
57
+ end
58
+
59
+ logger.info 'Building projects completed'
60
+ end
61
+
62
+ private
63
+
64
+ def prepare_project(project)
65
+ # Note: current working directory is the project directory,
66
+ # where the source code has been cloned to
67
+ if project.build_command
68
+ logger.debug "Executing build-command: #{project.build_command}"
69
+ run_as_script(Dir.getwd, project.build_command)
70
+ end
71
+ if project.auxclasspath_command
72
+ logger.debug "Executing auxclasspath-command: #{project.auxclasspath_command}"
73
+ auxclasspath = run_as_script(Dir.getwd, project.auxclasspath_command)
74
+ project.auxclasspath = "-auxclasspath #{auxclasspath}"
75
+ else
76
+ project.auxclasspath = ''
77
+ end
78
+ end
79
+
80
+ def run_as_script(path, command)
81
+ script = Tempfile.new(['pmd-regression-', '.sh'], path)
82
+ logger.debug "Creating script #{script.path}"
83
+ begin
84
+ script.write(command)
85
+ script.close
86
+ shell = 'sh -xe'
87
+ if command.start_with?('#!')
88
+ shell = command.lines[0].chomp[2..] # remove leading "#!"
89
+ end
90
+ stdout = Cmd.execute("#{shell} #{script.path}")
91
+ ensure
92
+ script.unlink
93
+ end
94
+ stdout
95
+ end
96
+
97
+ def execute_reset_cmd(type, tag)
98
+ raise "Unsupported project type '#{type}' - only git is supported" unless type == 'git'
99
+
100
+ reset_cmd = "git checkout #{tag}; git reset --hard #{tag}"
101
+
102
+ Cmd.execute(reset_cmd)
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'differ'
4
+
5
+ module PmdTester
6
+ # Turn a project report into a hash that can be rendered somewhere else
7
+ module ProjectHasher
8
+ include PmdTester
9
+
10
+ def report_diff_to_h(rdiff)
11
+ {
12
+ 'violation_counts' => rdiff.violation_counts.to_h.transform_keys(&:to_s),
13
+ 'error_counts' => rdiff.error_counts.to_h.transform_keys(&:to_s),
14
+ 'configerror_counts' => rdiff.configerror_counts.to_h.transform_keys(&:to_s),
15
+
16
+ 'base_execution_time' => PmdReportDetail.convert_seconds(rdiff.base_report.exec_time),
17
+ 'patch_execution_time' => PmdReportDetail.convert_seconds(rdiff.patch_report.exec_time),
18
+ 'diff_execution_time' => PmdReportDetail.convert_seconds(rdiff.patch_report.exec_time -
19
+ rdiff.base_report.exec_time),
20
+
21
+ 'base_timestamp' => rdiff.base_report.timestamp,
22
+ 'patch_timestamp' => rdiff.patch_report.timestamp,
23
+
24
+ 'rule_diffs' => rdiff.rule_summaries
25
+ }
26
+ end
27
+
28
+ def violations_to_hash(project, violations_by_file, is_diff)
29
+ filename_index = []
30
+ all_vs = []
31
+ violations_by_file.each do |file, vs|
32
+ file_ref = filename_index.size
33
+ filename_index.push(project.get_local_path(file))
34
+ vs.each do |v|
35
+ all_vs.push(make_violation_hash(file_ref, v, is_diff))
36
+ end
37
+ end
38
+
39
+ {
40
+ 'file_index' => filename_index,
41
+ 'violations' => all_vs
42
+ }
43
+ end
44
+
45
+ def errors_to_h(project)
46
+ errors = project.report_diff.error_diffs_by_file.values.flatten
47
+ errors.map { |e| error_to_hash(e, project) }
48
+ end
49
+
50
+ def configerrors_to_h(project)
51
+ configerrors = project.report_diff.configerror_diffs_by_rule.values.flatten
52
+ configerrors.map { |e| configerror_to_hash(e) }
53
+ end
54
+
55
+ def link_template(project)
56
+ l_str = project.type == 'git' ? 'L' : 'l'
57
+ "#{project.webview_url}/{file}##{l_str}{line}"
58
+ end
59
+
60
+ def error_to_hash(error, project)
61
+ escaped_stacktrace = sanitize_stacktrace(error)
62
+ old_stacktrace = error.old_error.nil? ? nil : sanitize_stacktrace(error.old_error)
63
+
64
+ {
65
+ 'file_url' => project.get_webview_url(error.filename),
66
+ 'stack_trace_html' => escaped_stacktrace,
67
+ 'old_stack_trace_html' => old_stacktrace,
68
+ 'short_message' => error.short_message,
69
+ 'short_filename' => error.short_filename,
70
+ 'filename' => error.filename,
71
+ 'change_type' => change_type(error)
72
+ }
73
+ end
74
+
75
+ def sanitize_stacktrace(error)
76
+ CGI.escapeHTML(error.stack_trace)
77
+ .gsub(error.filename, '<span class="meta-var">$FILE</span>')
78
+ .gsub(/\w++(?=\(\w++\.java:\d++\))/, '<span class="stack-trace-method">\\0</span>')
79
+ end
80
+
81
+ def configerror_to_hash(configerror)
82
+ {
83
+ 'rule' => configerror.rulename,
84
+ 'message' => configerror.msg,
85
+ 'change_type' => change_type(configerror)
86
+ }
87
+ end
88
+
89
+ def change_type(item)
90
+ if item.branch == BASE
91
+ 'removed'
92
+ elsif item.changed?
93
+ 'changed'
94
+ else
95
+ 'added'
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def violation_type(violation)
102
+ if violation.changed?
103
+ '~'
104
+ elsif violation.branch == PATCH
105
+ '+'
106
+ else
107
+ '-'
108
+ end
109
+ end
110
+
111
+ def make_violation_hash(file_ref, violation, is_diff = TRUE)
112
+ h = {
113
+ 't' => is_diff ? violation_type(violation) : '+',
114
+ 'l' => violation.line,
115
+ 'f' => file_ref,
116
+ 'r' => violation.rule_name,
117
+ 'm' => is_diff && violation.changed? ? diff_fragments(violation) : violation.message
118
+ }
119
+ h['ol'] = violation.old_line if is_diff && violation.changed? && violation.line != violation.old_line
120
+ h
121
+ end
122
+
123
+ def diff_fragments(violation)
124
+ diff = Differ.diff_by_word(violation.message, violation.old_message)
125
+ diff.format_as(:html)
126
+ end
127
+ end
128
+ end