1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
|
# frozen_string_literal: true
module RuboCop
module Cop
# Check for test coverage for GitLab experiments.
class ExperimentsTestCoverage < RuboCop::Cop::Base
CLASS_OFFENSE = 'Make sure experiment class has test coverage for all the variants.'
BLOCK_OFFENSE = 'Make sure experiment block has test coverage for all the variants.'
# Validates classes inherited from ApplicationExperiment
# These classes are located under app/experiments or ee/app/experiments
def on_class(node)
return if node.parent_class&.const_name != 'ApplicationExperiment'
return if covered_with_tests?(node)
add_offense(node, message: CLASS_OFFENSE)
end
# Validates experiments block in *.rb and *.haml files:
# experiment(:experiment_name) do |e|
# e.candidate { 'candidate' }
# e.run
# end
def on_block(node)
return if node.method_name != :experiment
return if covered_with_tests?(node)
add_offense(node, message: BLOCK_OFFENSE)
end
private
def covered_with_tests?(node)
tests_code = test_files_code(node)
return false if tests_code.blank?
return false unless tests_code.match?(stub_experiments_matcher)
return false unless tests_code.include?(experiment_name(node))
experiment_variants(node).map { |variant| tests_code.include?(variant) }.all?(&:present?)
end
def test_files_code(node)
test_file_path = filepath(node).gsub('app/', 'spec/').gsub('.rb', '_spec.rb')
test_file_path << '_spec.rb' if test_file_path.end_with?('.haml')
"#{read_file(test_file_path)}\n#{additional_tests_code(test_file_path)}"
end
def additional_tests_code(test_file_path)
if test_file_path.include?('/controllers/')
read_file(test_file_path.gsub('/controllers/', '/requests/'))
elsif test_file_path.include?('/lib/api/')
read_file(test_file_path.gsub('/lib/', '/spec/requests/'))
end
end
def read_file(file_path)
File.exist?(file_path) ? File.read(file_path) : ''
end
def experiment_name(node)
if node.is_a?(RuboCop::AST::ClassNode)
File.basename(filepath(node), '_experiment.rb')
else
block_node_value(node)
end
end
def experiment_variants(node)
node.body.children.filter_map do |child|
next unless child.is_a?(RuboCop::AST::SendNode) || child.is_a?(RuboCop::AST::BlockNode)
extract_variant(child)
end
end
def extract_variant(node)
# control enabled by default for tests
case node.method_name
when :candidate then 'candidate'
when :variant then variant_name(node)
end
end
def variant_name(node)
return send_node_value(node) if node.is_a?(RuboCop::AST::SendNode)
block_node_value(node)
end
def block_node_value(node)
send_node_value(node.children[0])
end
def send_node_value(node)
node.children[2].value.to_s
end
def filepath(node)
node.location.expression.source_buffer.name
end
def stub_experiments_matcher
# validates test files contains uncommented stub_experiments(...
/^([^#]|\s*|\w*)stub_experiments\(/
end
end
end
end
|