ndr_dev_support 2.1.2 → 3.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 +5 -5
- data/.rubocop.yml +23 -4
- data/README.md +24 -31
- data/code_safety.yml +99 -19
- data/lib/ndr_dev_support/daemon/ci_server.rb +94 -0
- data/lib/ndr_dev_support/daemon/stoppable.rb +111 -0
- data/lib/ndr_dev_support/integration_testing.rb +25 -3
- data/lib/ndr_dev_support/integration_testing/drivers/chrome.rb +9 -0
- data/lib/ndr_dev_support/integration_testing/drivers/chrome_headless.rb +17 -0
- data/lib/ndr_dev_support/integration_testing/drivers/firefox.rb +9 -0
- data/lib/ndr_dev_support/integration_testing/drivers/poltergeist.rb +15 -0
- data/lib/ndr_dev_support/integration_testing/drivers/switchable.rb +21 -0
- data/lib/ndr_dev_support/integration_testing/dsl.rb +62 -0
- data/lib/ndr_dev_support/rake_ci/brakeman_helper.rb +48 -0
- data/lib/ndr_dev_support/rake_ci/concerns/commit_metadata_persistable.rb +49 -0
- data/lib/ndr_dev_support/rake_ci/simple_cov_helper.rb +26 -0
- data/lib/ndr_dev_support/slack_message_publisher.rb +48 -0
- data/lib/ndr_dev_support/tasks.rb +12 -0
- data/lib/ndr_dev_support/version.rb +1 -1
- data/lib/tasks/audit_code.rake +94 -92
- data/lib/tasks/ci/brakeman.rake +54 -0
- data/lib/tasks/ci/bundle_audit.rake +45 -0
- data/lib/tasks/ci/bundle_install.rake +21 -0
- data/lib/tasks/ci/housekeep.rake +8 -0
- data/lib/tasks/ci/linguist.rake +42 -0
- data/lib/tasks/ci/notes.rake +30 -0
- data/lib/tasks/ci/prometheus.rake +50 -0
- data/lib/tasks/ci/rugged.rake +39 -0
- data/lib/tasks/ci/server.rake +12 -0
- data/lib/tasks/ci/simplecov.rake +37 -0
- data/lib/tasks/ci/slack.rake +30 -0
- data/lib/tasks/ci/stats.rake +24 -0
- data/ndr_dev_support.gemspec +15 -3
- metadata +164 -18
- data/lib/ndr_dev_support/integration_testing/capybara.rb +0 -3
- data/lib/ndr_dev_support/integration_testing/connection_sharing.rb +0 -47
- data/lib/ndr_dev_support/integration_testing/poltergeist.rb +0 -39
- data/lib/ndr_dev_support/integration_testing/screenshot.rb +0 -51
@@ -1,3 +1,25 @@
|
|
1
|
-
|
2
|
-
require '
|
3
|
-
|
1
|
+
# Set up basic capybara:
|
2
|
+
require 'capybara/rails'
|
3
|
+
ActionDispatch::IntegrationTest.include(Capybara::DSL)
|
4
|
+
|
5
|
+
# Set up basic screenshotting capability:
|
6
|
+
require 'capybara-screenshot'
|
7
|
+
if defined?(MiniTest)
|
8
|
+
require 'capybara-screenshot/minitest'
|
9
|
+
ActionDispatch::IntegrationTest.include(Capybara::Screenshot::MiniTestPlugin)
|
10
|
+
else
|
11
|
+
require 'capybara-screenshot/testunit'
|
12
|
+
end
|
13
|
+
|
14
|
+
# Include our custom DSL extensions, that also cover screenshotting:
|
15
|
+
require 'ndr_dev_support/integration_testing/dsl'
|
16
|
+
|
17
|
+
# These are all the drivers we have capybara / screenshot support for:
|
18
|
+
require 'ndr_dev_support/integration_testing/drivers/chrome'
|
19
|
+
require 'ndr_dev_support/integration_testing/drivers/chrome_headless'
|
20
|
+
require 'ndr_dev_support/integration_testing/drivers/firefox'
|
21
|
+
require 'ndr_dev_support/integration_testing/drivers/poltergeist'
|
22
|
+
require 'ndr_dev_support/integration_testing/drivers/switchable'
|
23
|
+
|
24
|
+
Capybara.default_driver = :switchable
|
25
|
+
Capybara.javascript_driver = :switchable
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'selenium-webdriver'
|
2
|
+
|
3
|
+
Capybara.register_driver(:chrome_headless) do |app|
|
4
|
+
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
|
5
|
+
'chromeOptions' => { args: %w[headless disable-gpu] }
|
6
|
+
)
|
7
|
+
|
8
|
+
Capybara::Selenium::Driver.new(
|
9
|
+
app,
|
10
|
+
browser: :chrome,
|
11
|
+
desired_capabilities: capabilities
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
Capybara::Screenshot.register_driver(:chrome_headless) do |driver, path|
|
16
|
+
driver.browser.save_screenshot(path)
|
17
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# The driver for PhantomJS is poltergeist:
|
2
|
+
require 'capybara/poltergeist'
|
3
|
+
|
4
|
+
Capybara.register_driver(:poltergeist) do |app|
|
5
|
+
options = { phantomjs_options: ['--proxy-type=none'], timeout: 60 }
|
6
|
+
|
7
|
+
options.merge!(debug: true, inspector: true) if ENV['DEBUG_PHANTOM_JS']
|
8
|
+
|
9
|
+
Capybara::Poltergeist::Driver.new(app, options)
|
10
|
+
end
|
11
|
+
|
12
|
+
Capybara::Screenshot.register_driver(:poltergeist) do |driver, path|
|
13
|
+
# Take full-height screenshots, rather than just capturing the viewport:
|
14
|
+
driver.render(path, full: true)
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# A meta-driver that allows the driver to be set using the `INTEGRATION_DRIVER`
|
2
|
+
# environment variable (e.g. for a CI matrix), assuming that driver has been pre-registered
|
3
|
+
# with Capybara.
|
4
|
+
|
5
|
+
# Although the aim is to move to Chrome headless, we keep poltergeist as the default
|
6
|
+
# driver for now. For motivation behind not changing immediately, see the "Differences
|
7
|
+
# between Poltergeist and Selenium" section of:
|
8
|
+
#
|
9
|
+
# https://about.gitlab.com/2017/12/19/moving-to-headless-chrome/
|
10
|
+
#
|
11
|
+
Capybara.register_driver(:switchable) do |app|
|
12
|
+
choice = ENV.fetch('INTEGRATION_DRIVER', 'poltergeist').to_sym
|
13
|
+
|
14
|
+
Capybara.drivers.fetch(choice).call(app)
|
15
|
+
end
|
16
|
+
|
17
|
+
Capybara::Screenshot.register_driver(:switchable) do |driver, path|
|
18
|
+
choice = ENV.fetch('INTEGRATION_DRIVER', 'poltergeist').to_sym
|
19
|
+
|
20
|
+
Capybara::Screenshot.registered_drivers.fetch(choice).call(driver, path)
|
21
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'capybara/rails'
|
2
|
+
|
3
|
+
module NdrDevSupport
|
4
|
+
module IntegrationTesting
|
5
|
+
# Additional integration testing DSL:
|
6
|
+
module DSL
|
7
|
+
# Instruct the headless browser to clear its session:
|
8
|
+
def clear_headless_session!
|
9
|
+
page.driver.reset!
|
10
|
+
end
|
11
|
+
|
12
|
+
# Get the headless browser to delete all of the cookies
|
13
|
+
# for the current page without resetting:
|
14
|
+
def delete_all_cookies!
|
15
|
+
page.driver.cookies.each_key do |name|
|
16
|
+
page.driver.remove_cookie(name)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Wrap up interacting with modals. The assumption is that the modal
|
21
|
+
# should be gone one the interaction is complete (as this is a good
|
22
|
+
# proxy for a triggered AJAX request to have completed, and therefore
|
23
|
+
# a signal for capybara to wait for); if this is not the case, pass
|
24
|
+
# `remain: true` to signal that the modal should remain active.
|
25
|
+
def within_modal(selector: '#modal', remain: false)
|
26
|
+
within(selector) { yield }
|
27
|
+
|
28
|
+
message = "modal was #{'not ' unless remain} expected to remain visible!"
|
29
|
+
assert(remain ? has_selector?(selector) : has_no_selector?(selector), message)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Adds variant of Capybara's #within_window method, that doesn't return
|
33
|
+
# to the preview window on an exception. This allows us to screenshot
|
34
|
+
# a popup automatically if a test errors/fails whilst it has focus.
|
35
|
+
module SessionExtensions
|
36
|
+
def within_screenshot_compatible_window(window_or_proc)
|
37
|
+
original = current_window
|
38
|
+
|
39
|
+
case window_or_proc
|
40
|
+
when Capybara::Window
|
41
|
+
switch_to_window(window_or_proc) unless original == window_or_proc
|
42
|
+
when Proc
|
43
|
+
switch_to_window { window_or_proc.call }
|
44
|
+
else
|
45
|
+
raise ArgumentError, 'Unsupported window type!'
|
46
|
+
end
|
47
|
+
|
48
|
+
scopes << nil
|
49
|
+
yield
|
50
|
+
@scopes.pop
|
51
|
+
switch_to_window(original)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
Capybara::Session.include(SessionExtensions)
|
56
|
+
|
57
|
+
delegate :within_screenshot_compatible_window, to: :page
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
ActionDispatch::IntegrationTest.include(NdrDevSupport::IntegrationTesting::DSL)
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module NdrDevSupport
|
2
|
+
module RakeCI
|
3
|
+
# Brakeman helper
|
4
|
+
class BrakemanHelper
|
5
|
+
require 'set'
|
6
|
+
require 'brakeman'
|
7
|
+
require_relative 'concerns/commit_metadata_persistable'
|
8
|
+
|
9
|
+
include CommitMetadataPersistable
|
10
|
+
|
11
|
+
attr_reader :new_fingerprints, :old_fingerprints
|
12
|
+
|
13
|
+
def run
|
14
|
+
@tracker = ::Brakeman.run(app_path: '.')
|
15
|
+
|
16
|
+
last_commit_fingerprints = load_last_commit_data
|
17
|
+
if last_commit_fingerprints
|
18
|
+
@new_fingerprints = current_fingerprints - last_commit_fingerprints
|
19
|
+
@old_fingerprints = last_commit_fingerprints - current_fingerprints
|
20
|
+
else
|
21
|
+
@new_fingerprints = @old_fingerprints = Set.new
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def warnings
|
26
|
+
@tracker.warnings
|
27
|
+
end
|
28
|
+
|
29
|
+
def warning_counts_by_confidence
|
30
|
+
return @warning_counts_by_confidence if @warning_counts_by_confidence
|
31
|
+
|
32
|
+
@warning_counts_by_confidence = {}
|
33
|
+
warnings.group_by(&:confidence).each do |confidence, grouped_warnings|
|
34
|
+
@warning_counts_by_confidence[confidence] = grouped_warnings.count
|
35
|
+
end
|
36
|
+
@warning_counts_by_confidence
|
37
|
+
end
|
38
|
+
|
39
|
+
def current_fingerprints
|
40
|
+
@current_fingerprints ||= warnings.map(&:fingerprint).to_set
|
41
|
+
end
|
42
|
+
|
43
|
+
def save_current_fingerprints
|
44
|
+
save_current_commit_data(current_fingerprints)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_support/inflector'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
# Provides methods relating to persisting commit metadata
|
6
|
+
module CommitMetadataPersistable
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
attr_accessor :commit
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def load_last_commit_data
|
16
|
+
hash = YAML.load_file(filename)
|
17
|
+
|
18
|
+
if commit.parents.map(&:oid).include?(hash[:commit])
|
19
|
+
# payload from parent commit
|
20
|
+
hash[:payload]
|
21
|
+
end
|
22
|
+
rescue Errno::ENOENT
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def load_current_commit_data
|
27
|
+
hash = YAML.load_file(filename)
|
28
|
+
|
29
|
+
if commit.oid == hash[:commit]
|
30
|
+
# payload from parent commit
|
31
|
+
hash[:payload]
|
32
|
+
end
|
33
|
+
rescue Errno::ENOENT
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def save_current_commit_data(data)
|
38
|
+
hash = { commit: commit.oid, payload: data }
|
39
|
+
File.write(filename, YAML.dump(hash))
|
40
|
+
end
|
41
|
+
|
42
|
+
def filename
|
43
|
+
"rake_ci.#{name}.yml"
|
44
|
+
end
|
45
|
+
|
46
|
+
def name
|
47
|
+
self.class.name.demodulize.underscore.sub(/_helper\z/, '')
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module NdrDevSupport
|
2
|
+
module RakeCI
|
3
|
+
# This helper persists the SimpleCov::Result
|
4
|
+
class SimpleCovHelper
|
5
|
+
require 'simplecov'
|
6
|
+
require_relative 'concerns/commit_metadata_persistable'
|
7
|
+
|
8
|
+
include CommitMetadataPersistable
|
9
|
+
|
10
|
+
def commit
|
11
|
+
return @commit if @commit
|
12
|
+
|
13
|
+
repo = Rugged::Repository.new('.')
|
14
|
+
@commit = repo.lookup(repo.head.target_id)
|
15
|
+
end
|
16
|
+
|
17
|
+
def load_current_result
|
18
|
+
load_current_commit_data
|
19
|
+
end
|
20
|
+
|
21
|
+
def save_current_result(result)
|
22
|
+
save_current_commit_data(result)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module NdrDevSupport
|
2
|
+
# This Class publishes messages to Slack
|
3
|
+
class SlackMessagePublisher
|
4
|
+
def initialize(url, default_options = {})
|
5
|
+
@url = url
|
6
|
+
@default_options = default_options
|
7
|
+
end
|
8
|
+
|
9
|
+
def post(options = {})
|
10
|
+
request = json_request
|
11
|
+
request.body = message(options)
|
12
|
+
|
13
|
+
use_ssl = request.uri.scheme == 'https'
|
14
|
+
http =
|
15
|
+
if proxy
|
16
|
+
proxy.start(request.uri.host, use_ssl: use_ssl)
|
17
|
+
else
|
18
|
+
Net::HTTP.start(request.uri.host, request.uri.port, use_ssl: use_ssl)
|
19
|
+
end
|
20
|
+
|
21
|
+
http.request(request)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def json_request
|
27
|
+
uri = URI.parse(@url)
|
28
|
+
request = Net::HTTP::Post.new(uri)
|
29
|
+
# request.basic_auth(*@auth.split(':')) if @auth
|
30
|
+
request['Content-Type'] = 'application/json'
|
31
|
+
request
|
32
|
+
end
|
33
|
+
|
34
|
+
def message(options)
|
35
|
+
@default_options.merge(options).to_json
|
36
|
+
end
|
37
|
+
|
38
|
+
def proxy
|
39
|
+
return @proxy if @proxy
|
40
|
+
|
41
|
+
return if ENV['https_proxy'].nil?
|
42
|
+
host_and_port = ENV['https_proxy'].match(%r{\A(?:https?://)?([^:]+):(\d+)})[1, 2]
|
43
|
+
|
44
|
+
return if host_and_port.nil?
|
45
|
+
@proxy = Net::HTTP.Proxy(*host_and_port)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -1,2 +1,14 @@
|
|
1
1
|
load 'tasks/audit_code.rake'
|
2
|
+
load 'tasks/ci/brakeman.rake'
|
3
|
+
load 'tasks/ci/bundle_audit.rake'
|
4
|
+
load 'tasks/ci/bundle_install.rake'
|
5
|
+
load 'tasks/ci/housekeep.rake'
|
6
|
+
load 'tasks/ci/linguist.rake'
|
7
|
+
load 'tasks/ci/notes.rake'
|
8
|
+
load 'tasks/ci/prometheus.rake'
|
9
|
+
load 'tasks/ci/rugged.rake'
|
10
|
+
load 'tasks/ci/server.rake'
|
11
|
+
load 'tasks/ci/simplecov.rake'
|
12
|
+
load 'tasks/ci/slack.rake'
|
13
|
+
load 'tasks/ci/stats.rake'
|
2
14
|
load 'tasks/rubocop.rake'
|
data/lib/tasks/audit_code.rake
CHANGED
@@ -18,20 +18,44 @@ def rake_cmd
|
|
18
18
|
ENV['BUNDLE_BIN_PATH'] ? 'bundle exec rake' : 'rake'
|
19
19
|
end
|
20
20
|
|
21
|
-
|
22
|
-
# (negative value => print all)
|
23
|
-
def audit_code_safety(max_print = 20, ignore_new = false, show_diffs = false, show_in_priority = false, user_name = 'usr')
|
24
|
-
puts 'Running source code safety audit script.'
|
25
|
-
puts
|
26
|
-
|
27
|
-
max_print = 1_000_000 if max_print < 0
|
21
|
+
def load_file_safety
|
28
22
|
safety_cfg = File.exist?(SAFETY_FILE) ? YAML.load_file(SAFETY_FILE) : {}
|
29
23
|
file_safety = safety_cfg['file safety']
|
30
24
|
if file_safety.nil?
|
31
25
|
puts "Creating new 'file safety' block in #{SAFETY_FILE}"
|
32
|
-
|
26
|
+
file_safety = {}
|
33
27
|
end
|
34
|
-
file_safety
|
28
|
+
file_safety
|
29
|
+
end
|
30
|
+
|
31
|
+
def update_safety_file(file_safety)
|
32
|
+
File.open(SAFETY_FILE, 'w') do |file|
|
33
|
+
# Consistent file diffs, as ruby preserves Hash insertion order since v1.9
|
34
|
+
list = {}
|
35
|
+
list['file safety'] = Hash[file_safety.sort]
|
36
|
+
YAML.dump(list, file) # Save changes before checking latest revisions
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_new_file_to_file_safety(file_safety, f)
|
41
|
+
return if file_safety.key?(f)
|
42
|
+
file_safety[f] = {
|
43
|
+
'comments' => nil,
|
44
|
+
'reviewed_by' => nil,
|
45
|
+
'safe_revision' => nil
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
# Parameter max_print is number of entries to print before truncating output
|
50
|
+
# (negative value => print all)
|
51
|
+
def audit_code_safety(max_print = 20, ignore_new = false, show_diffs = false, show_in_priority = false, usr = 'usr', interactive = false)
|
52
|
+
puts 'Running source code safety audit script.'
|
53
|
+
puts
|
54
|
+
|
55
|
+
max_print = 1_000_000 if max_print.negative?
|
56
|
+
show_diffs = true if interactive
|
57
|
+
file_safety = load_file_safety
|
58
|
+
file_safety.each_value do |v|
|
35
59
|
rev = v['safe_revision']
|
36
60
|
v['safe_revision'] = rev.to_s if rev.is_a?(Integer)
|
37
61
|
end
|
@@ -57,19 +81,8 @@ def audit_code_safety(max_print = 20, ignore_new = false, show_diffs = false, sh
|
|
57
81
|
new_files = get_new_files(safety_repo)
|
58
82
|
# Ignore subdirectories, and exclude code_safety.yml by default.
|
59
83
|
new_files.delete_if { |f| f =~ /[\/\\]$/ || Pathname.new(f).expand_path == SAFETY_FILE }
|
60
|
-
new_files.each
|
61
|
-
|
62
|
-
file_safety[f] = {
|
63
|
-
'comments' => nil,
|
64
|
-
'reviewed_by' => nil,
|
65
|
-
'safe_revision' => nil
|
66
|
-
}
|
67
|
-
end
|
68
|
-
File.open(SAFETY_FILE, 'w') do |file|
|
69
|
-
# Consistent file diffs, as ruby preserves Hash insertion order since v1.9
|
70
|
-
safety_cfg['file safety'] = Hash[file_safety.sort]
|
71
|
-
YAML.dump(safety_cfg, file) # Save changes before checking latest revisions
|
72
|
-
end
|
84
|
+
new_files.each { |f| add_new_file_to_file_safety(file_safety, f) }
|
85
|
+
update_safety_file(file_safety) # Save changes before checking latest revisions
|
73
86
|
end
|
74
87
|
puts "Updating latest revisions for #{file_safety.size} files"
|
75
88
|
set_last_changed_revision(trunk_repo, file_safety, file_safety.keys)
|
@@ -79,7 +92,7 @@ def audit_code_safety(max_print = 20, ignore_new = false, show_diffs = false, sh
|
|
79
92
|
|
80
93
|
missing_files = file_safety.keys.reject { |path| File.file?(path) }
|
81
94
|
|
82
|
-
|
95
|
+
unless missing_files.empty?
|
83
96
|
puts "Number of files no longer in repository but in code_safety.yml: #{missing_files.length}"
|
84
97
|
puts " Please run #{rake_cmd} audit:tidy_code_safety_file to remove redundant files"
|
85
98
|
missing_files.each { |path| puts ' ' + path }
|
@@ -106,15 +119,13 @@ def audit_code_safety(max_print = 20, ignore_new = false, show_diffs = false, sh
|
|
106
119
|
end
|
107
120
|
|
108
121
|
file_list.each do |f|
|
109
|
-
if print_file_safety(file_safety,
|
110
|
-
printed << f
|
111
|
-
end
|
122
|
+
printed << f if print_file_safety(file_safety, f, false, printed.size >= max_print)
|
112
123
|
end
|
113
124
|
puts "... and #{printed.size - max_print} others" if printed.size > max_print
|
114
125
|
if show_diffs
|
115
126
|
puts
|
116
127
|
printed.each do |f|
|
117
|
-
print_file_diffs(file_safety, trunk_repo, f,
|
128
|
+
print_file_diffs(file_safety, trunk_repo, f, usr, interactive)
|
118
129
|
end
|
119
130
|
end
|
120
131
|
|
@@ -126,7 +137,7 @@ end
|
|
126
137
|
# If not verbose, only prints details for unsafe files
|
127
138
|
# Returns true if anything printed (or would have been printed if silent),
|
128
139
|
# or false otherwise.
|
129
|
-
def print_file_safety(file_safety,
|
140
|
+
def print_file_safety(file_safety, fname, verbose = false, silent = false)
|
130
141
|
msg = "#{fname}\n "
|
131
142
|
entry = file_safety[fname]
|
132
143
|
msg += 'File not in audit list' if entry.nil?
|
@@ -154,21 +165,12 @@ def print_file_safety(file_safety, repo, fname, verbose = false, silent = false)
|
|
154
165
|
end
|
155
166
|
|
156
167
|
def flag_file_as_safe(release, reviewed_by, comments, f)
|
157
|
-
|
158
|
-
file_safety =
|
159
|
-
|
160
|
-
unless File.exist?(f)
|
161
|
-
abort("Error: Unable to flag non-existent file as safe: #{f}")
|
162
|
-
end
|
163
|
-
unless file_safety.key?(f)
|
164
|
-
file_safety[f] = {
|
165
|
-
'comments' => nil,
|
166
|
-
'reviewed_by' => :dummy, # dummy value, will be overwritten
|
167
|
-
'safe_revision' => nil }
|
168
|
-
end
|
168
|
+
abort("Error: Unable to flag non-existent file as safe: #{f}") unless File.exist?(f)
|
169
|
+
file_safety = load_file_safety
|
170
|
+
add_new_file_to_file_safety(file_safety, f)
|
169
171
|
entry = file_safety[f]
|
170
172
|
entry_orig = entry.dup
|
171
|
-
if comments.to_s.
|
173
|
+
if !comments.to_s.empty? && entry['comments'] != comments
|
172
174
|
entry['comments'] = if entry['comments'].to_s.empty?
|
173
175
|
comments
|
174
176
|
else
|
@@ -176,9 +178,7 @@ def flag_file_as_safe(release, reviewed_by, comments, f)
|
|
176
178
|
end
|
177
179
|
end
|
178
180
|
if entry['safe_revision']
|
179
|
-
unless release
|
180
|
-
abort("Error: File already has safe revision #{entry['safe_revision']}: #{f}")
|
181
|
-
end
|
181
|
+
abort("Error: File already has safe revision #{entry['safe_revision']}: #{f}") unless release
|
182
182
|
if release.is_a?(Integer) && release < entry['safe_revision']
|
183
183
|
puts("Warning: Rolling back safe revision from #{entry['safe_revision']} to #{release} for #{f}")
|
184
184
|
end
|
@@ -188,11 +188,7 @@ def flag_file_as_safe(release, reviewed_by, comments, f)
|
|
188
188
|
if entry == entry_orig
|
189
189
|
puts "No changes when updating safe_revision to #{release || '[none]'} for #{f}"
|
190
190
|
else
|
191
|
-
|
192
|
-
# Consistent file diffs, as ruby preserves Hash insertion order since v1.9
|
193
|
-
safety_cfg['file safety'] = Hash[file_safety.sort]
|
194
|
-
YAML.dump(safety_cfg, file) # Save changes before checking latest revisions
|
195
|
-
end
|
191
|
+
update_safety_file(file_safety)
|
196
192
|
puts "Updated safe_revision to #{release || '[none]'} for #{f}"
|
197
193
|
end
|
198
194
|
end
|
@@ -313,49 +309,38 @@ def get_last_changed_revision(repo, fname)
|
|
313
309
|
end
|
314
310
|
end
|
315
311
|
|
316
|
-
# Get mime type. Note that Git does not have this information
|
317
|
-
def get_mime_type(repo, fname)
|
318
|
-
case repository_type
|
319
|
-
when 'git'
|
320
|
-
'Git does not provide mime types'
|
321
|
-
when 'git-svn', 'svn'
|
322
|
-
%x[svn propget svn:mime-type "#{repo}/#{fname}"].chomp
|
323
|
-
end
|
324
|
-
end
|
325
|
-
|
326
312
|
# # Print file diffs, for code review
|
327
|
-
def print_file_diffs(file_safety, repo, fname,
|
313
|
+
def print_file_diffs(file_safety, repo, fname, usr, interactive)
|
328
314
|
entry = file_safety[fname]
|
329
315
|
repolatest = entry['last_changed_rev']
|
330
316
|
safe_revision = entry['safe_revision']
|
331
317
|
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
else
|
336
|
-
|
337
|
-
rev = get_last_changed_revision(repo, fname)
|
338
|
-
if rev
|
339
|
-
mime = get_mime_type(repo, fname)
|
340
|
-
end
|
341
|
-
|
342
|
-
print_repo_file_diffs(repolatest, repo, fname, user_name, safe_revision) if repolatest != safe_revision
|
343
|
-
end
|
318
|
+
return unless safe_revision.nil? || repolatest != safe_revision
|
319
|
+
safe_revision ||= root_revision
|
320
|
+
print_repo_file_diffs(repolatest, repo, fname, usr, safe_revision, interactive)
|
344
321
|
end
|
345
322
|
|
346
323
|
# Returns first commit for git and 0 for svn in order to be used to display
|
347
324
|
# new files. Called from print_file_diffs
|
348
|
-
def
|
325
|
+
def root_revision
|
349
326
|
case repository_type
|
350
327
|
when 'git'
|
351
|
-
|
328
|
+
`git rev-list --max-parents=0 HEAD`.chomp
|
352
329
|
when 'git-svn', 'svn'
|
353
330
|
0
|
354
331
|
end
|
355
332
|
end
|
356
333
|
|
357
|
-
def print_repo_file_diffs(repolatest, repo, fname,
|
334
|
+
def print_repo_file_diffs(repolatest, repo, fname, usr, safe_revision, interactive)
|
358
335
|
require 'open3'
|
336
|
+
require 'highline/import'
|
337
|
+
|
338
|
+
if interactive
|
339
|
+
ask("\n<%= color('Press Enter to continue ...', :yellow) %>")
|
340
|
+
system('clear')
|
341
|
+
system("printf '\033[3J'") # clear the scrollback
|
342
|
+
end
|
343
|
+
|
359
344
|
cmd = nil
|
360
345
|
case repository_type
|
361
346
|
when 'git'
|
@@ -366,15 +351,36 @@ def print_repo_file_diffs(repolatest, repo, fname, user_name, safe_revision)
|
|
366
351
|
if cmd
|
367
352
|
puts(cmd.join(' '))
|
368
353
|
stdout_and_err_str, _status = Open3.capture2e(*cmd)
|
369
|
-
puts 'Invalid commit ID in code_safety.yml ' + safe_revision
|
354
|
+
puts 'Invalid commit ID in code_safety.yml ' + safe_revision if stdout_and_err_str.start_with?('fatal: Invalid revision range ')
|
370
355
|
puts(stdout_and_err_str)
|
371
356
|
else
|
372
357
|
puts 'Unknown repo'
|
373
358
|
end
|
374
359
|
|
375
|
-
|
376
|
-
|
377
|
-
|
360
|
+
if interactive
|
361
|
+
response = ask("Flag #{fname} changes safe? [Yes|No|Abort]: ") { |q| q.case = :down }
|
362
|
+
if %w[yes y].include?(response)
|
363
|
+
puts 'Flagging as safe...'
|
364
|
+
release = get_release(repolatest)
|
365
|
+
if usr.to_s.strip.empty?
|
366
|
+
usr = ask('File reviewed by:') do |q|
|
367
|
+
q.whitespace = :strip_and_collapse
|
368
|
+
q.validate = /\A[\w \-]+\Z/
|
369
|
+
end
|
370
|
+
end
|
371
|
+
comment = ask('Please write your comments (optional):')
|
372
|
+
# use to_s to convert response from !ruby/string:HighLine::String to String
|
373
|
+
flag_file_as_safe(release, usr.to_s, comment.to_s, fname)
|
374
|
+
elsif %w[abort a].include?(response)
|
375
|
+
abort('Rake abort: user interrupt detected')
|
376
|
+
else
|
377
|
+
say("\n<%= color('Safey review for #{fname} skipped by user.', :magenta) %>")
|
378
|
+
end
|
379
|
+
else
|
380
|
+
puts 'To flag the changes to this file as safe, run:'
|
381
|
+
puts " #{rake_cmd} audit:safe release=#{repolatest} file=#{fname} reviewed_by=#{usr}" \
|
382
|
+
' comments=""'
|
383
|
+
end
|
378
384
|
puts
|
379
385
|
end
|
380
386
|
|
@@ -389,8 +395,8 @@ def release_valid?(release)
|
|
389
395
|
end
|
390
396
|
end
|
391
397
|
|
392
|
-
def get_release
|
393
|
-
release
|
398
|
+
def get_release(release = nil)
|
399
|
+
release ||= ENV['release']
|
394
400
|
release = nil if release == '0'
|
395
401
|
case repository_type
|
396
402
|
when 'svn', 'git-svn'
|
@@ -413,29 +419,24 @@ def clean_working_copy?
|
|
413
419
|
end
|
414
420
|
|
415
421
|
def remove_non_existent_files_from_code_safety
|
416
|
-
|
417
|
-
file_safety = safety_cfg['file safety']
|
422
|
+
file_safety = load_file_safety
|
418
423
|
files_no_longer_in_repo = file_safety.keys.reject { |ff| File.file?(ff) }
|
419
424
|
files_no_longer_in_repo.each do |f|
|
420
425
|
puts 'No longer in repository ' + f
|
421
426
|
file_safety.delete f
|
422
427
|
end
|
423
|
-
|
424
|
-
# Consistent file diffs, as ruby preserves Hash insertion order since v1.9
|
425
|
-
safety_cfg['file safety'] = Hash[file_safety.sort]
|
426
|
-
YAML.dump(safety_cfg, file) # Save changes before checking latest revisions
|
427
|
-
end
|
428
|
+
update_safety_file(file_safety)
|
428
429
|
end
|
429
430
|
|
430
431
|
namespace :audit do
|
431
432
|
desc "Audit safety of source code.
|
432
|
-
Usage: audit:code [max_print=n] [ignore_new=false|true] [show_diffs=false|true] [reviewed_by=usr]
|
433
|
+
Usage: audit:code [max_print=n] [ignore_new=false|true] [show_diffs=false|true] [reviewed_by=usr] [interactive=false|true]
|
433
434
|
|
434
435
|
File #{SAFETY_FILE} lists the safety and revision information
|
435
436
|
of the era source code. This task updates the list, and [TODO] warns about
|
436
437
|
files which have changed since they were last verified as safe."
|
437
438
|
task(:code) do
|
438
|
-
puts 'Usage: audit:code [max_print=n] [ignore_new=false|true] [show_diffs=false|true] [show_in_priority=false|true] [reviewed_by=usr]'
|
439
|
+
puts 'Usage: audit:code [max_print=n] [ignore_new=false|true] [show_diffs=false|true] [show_in_priority=false|true] [reviewed_by=usr] [interactive=false|true]'
|
439
440
|
puts "This is a #{repository_type} repository"
|
440
441
|
|
441
442
|
ignore_new = (ENV['ignore_new'].to_s =~ /\Atrue\Z/i)
|
@@ -443,10 +444,11 @@ files which have changed since they were last verified as safe."
|
|
443
444
|
show_in_priority = (ENV['show_in_priority'].to_s =~ /\Atrue\Z/i)
|
444
445
|
max_print = ENV['max_print'] =~ /\A-?[0-9][0-9]*\Z/ ? ENV['max_print'].to_i : 20
|
445
446
|
reviewer = ENV['reviewed_by']
|
447
|
+
interactive = (ENV['interactive'].to_s =~ /\Atrue\Z/i)
|
446
448
|
|
447
|
-
all_safe = audit_code_safety(max_print, ignore_new, show_diffs, show_in_priority, reviewer)
|
449
|
+
all_safe = audit_code_safety(max_print, ignore_new, show_diffs, show_in_priority, reviewer, interactive)
|
448
450
|
|
449
|
-
unless show_diffs
|
451
|
+
unless show_diffs || interactive
|
450
452
|
puts "To show file diffs, run: #{rake_cmd} audit:code max_print=-1 show_diffs=true"
|
451
453
|
end
|
452
454
|
|