blob: 4bb2832030cae0d8ee8d85b760b983cc9c94135a (
plain)
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
111
112
113
114
|
# 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)
# haml-lint add .rb extension to *.haml files
# https://gitlab.com/gitlab-org/gitlab/-/issues/415330#caveats
test_file_path = filepath(node).gsub('app/', 'spec/').gsub('.rb', '_spec.rb')
"#{read_file(test_file_path)}\n#{additional_tests_code(test_file_path)}"
end
def additional_tests_code(test_file_path)
# rubocop:disable Gitlab/NoCodeCoverageComment
# :nocov: File paths stubed in tests
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
# :nocov:
# rubocop:enable Gitlab/NoCodeCoverageComment
end
def read_file(file_path)
File.exist?(file_path) ? File.new(file_path).read : ''
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
|