Commit b3a74cc0 authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch '333443-revert-static-check' into 'master'

Revert rspec:feature-flag-changes [RUN ALL RSPEC]

See merge request gitlab-org/gitlab!63916
parents fec70b63 216fa729
...@@ -47,6 +47,7 @@ ...@@ -47,6 +47,7 @@
- rspec_profiling/ - rspec_profiling/
- tmp/capybara/ - tmp/capybara/
- tmp/memory_test/ - tmp/memory_test/
- tmp/feature_flags/
- log/*.log - log/*.log
reports: reports:
junit: junit_rspec.xml junit: junit_rspec.xml
...@@ -221,11 +222,6 @@ static-analysis: ...@@ -221,11 +222,6 @@ static-analysis:
script: script:
- run_timed_command "retry yarn install --frozen-lockfile" - run_timed_command "retry yarn install --frozen-lockfile"
- scripts/static-analysis - scripts/static-analysis
artifacts:
expire_in: 31d
when: always
paths:
- tmp/feature_flags/
static-analysis as-if-foss: static-analysis as-if-foss:
extends: extends:
...@@ -464,9 +460,25 @@ rspec:coverage: ...@@ -464,9 +460,25 @@ rspec:coverage:
rspec:feature-flags: rspec:feature-flags:
extends: extends:
- .coverage-base - .coverage-base
- .static-analysis:rules:ee-and-foss - .rails:rules:rspec-feature-flags
stage: post-test stage: post-test
needs: ["static-analysis"] # We cannot use needs since it would mean needing 84 jobs (since most are parallelized)
# so we use `dependencies` here.
dependencies:
- setup-test-env
- rspec migration pg12
- rspec unit pg12
- rspec integration pg12
- rspec system pg12
- rspec-ee migration pg12
- rspec-ee unit pg12
- rspec-ee integration pg12
- rspec-ee system pg12
- rspec-ee unit pg12 geo
- rspec-ee integration pg12 geo
- rspec-ee system pg12 geo
- memory-static
- memory-on-boot
script: script:
- !reference [.minimal-bundle-install, script] - !reference [.minimal-bundle-install, script]
- if [ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]; then - if [ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]; then
......
...@@ -932,6 +932,14 @@ ...@@ -932,6 +932,14 @@
- <<: *if-merge-request-title-run-all-rspec - <<: *if-merge-request-title-run-all-rspec
when: always when: always
.rails:rules:rspec-feature-flags:
rules:
- <<: *if-not-ee
when: never
- <<: *if-default-branch-schedule-2-hourly
allow_failure: true
- <<: *if-merge-request-title-run-all-rspec
.rails:rules:default-branch-schedule-nightly--code-backstage: .rails:rules:default-branch-schedule-nightly--code-backstage:
rules: rules:
- <<: *if-default-branch-schedule-nightly - <<: *if-default-branch-schedule-nightly
......
...@@ -49,9 +49,10 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -49,9 +49,10 @@ class Projects::IssuesController < Projects::ApplicationController
end end
before_action only: :show do before_action only: :show do
real_time_enabled = Gitlab::ActionCable::Config.in_app? || Feature.enabled?(:real_time_issue_sidebar, @project) real_time_feature_flag = :real_time_issue_sidebar
real_time_enabled = Gitlab::ActionCable::Config.in_app? || Feature.enabled?(real_time_feature_flag, @project)
push_to_gon_attributes(:features, :real_time_issue_sidebar, real_time_enabled) push_to_gon_attributes(:features, real_time_feature_flag, real_time_enabled)
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml) push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml) push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, @project, default_enabled: :yaml) push_frontend_feature_flag(:labels_widget, @project, default_enabled: :yaml)
......
...@@ -68,3 +68,27 @@ end ...@@ -68,3 +68,27 @@ end
if helper.security_mr? && feature_flag_file_added? if helper.security_mr? && feature_flag_file_added?
fail "Feature flags are discouraged from security merge requests. Read the [security documentation](https://gitlab.com/gitlab-org/release/docs/-/blob/master/general/security/utilities/feature_flags.md) for details." fail "Feature flags are discouraged from security merge requests. Read the [security documentation](https://gitlab.com/gitlab-org/release/docs/-/blob/master/general/security/utilities/feature_flags.md) for details."
end end
if feature_flag_file_added_or_removed?
new_mr_title = helper.mr_title.dup
new_mr_title << ' [RUN ALL RSPEC]' unless helper.run_all_rspec_mr?
new_mr_title << ' [RUN AS-IF-FOSS]' unless helper.run_as_if_foss_mr?
changes = {}
changes[:add_labels] = FEATURE_FLAG_LABEL unless helper.mr_has_labels?(FEATURE_FLAG_LABEL)
if new_mr_title != helper.mr_title
changes[:title] = new_mr_title
else
message "You're adding or removing a feature flag, your MR title needs to include `[RUN ALL RSPEC] [RUN AS-IF-FOSS]` (we may have updated it automatically for you and started a new MR pipeline) to ensure everything is covered."
end
if changes.any?
gitlab.api.update_merge_request(
gitlab.mr_json['project_id'],
gitlab.mr_json['iid'],
**changes
)
gitlab.api.post("/projects/#{gitlab.mr_json['project_id']}/merge_requests/#{gitlab.mr_json['iid']}/pipelines")
end
end
...@@ -9,8 +9,7 @@ SPECIALIZATIONS = { ...@@ -9,8 +9,7 @@ SPECIALIZATIONS = {
docs: 'documentation', docs: 'documentation',
qa: 'QA', qa: 'QA',
engineering_productivity: 'Engineering Productivity', engineering_productivity: 'Engineering Productivity',
ci_template: 'ci::templates', ci_template: 'ci::templates'
feature_flag: 'feature flag'
}.freeze }.freeze
labels_to_add = project_helper.changes_by_category.each_with_object([]) do |(category, _changes), memo| labels_to_add = project_helper.changes_by_category.each_with_object([]) do |(category, _changes), memo|
......
...@@ -3,8 +3,9 @@ ...@@ -3,8 +3,9 @@
- return unless show_trial_status_widget?(root_group) - return unless show_trial_status_widget?(root_group)
-# For the current Growth::Conversion experiment -# For the current Growth::Conversion experiment
- record_experiment_group(:show_trial_status_in_sidebar, root_group) - experiment_key = :show_trial_status_in_sidebar
- return unless experiment_enabled?(:show_trial_status_in_sidebar, subject: root_group) - record_experiment_group(experiment_key, root_group)
- return unless experiment_enabled?(experiment_key, subject: root_group)
= nav_link do = nav_link do
#js-trial-status-widget{ data: trial_status_widget_data_attrs(root_group) } #js-trial-status-widget{ data: trial_status_widget_data_attrs(root_group) }
......
...@@ -32,79 +32,59 @@ module RuboCop ...@@ -32,79 +32,59 @@ module RuboCop
# Returns true if the given node resides in app/finders or ee/app/finders. # Returns true if the given node resides in app/finders or ee/app/finders.
def in_finder?(node) def in_finder?(node)
in_app_directory?(node, 'finders') in_directory?(node, 'finders')
end end
# Returns true if the given node resides in app/models or ee/app/models. # Returns true if the given node resides in app/models or ee/app/models.
def in_model?(node) def in_model?(node)
in_app_directory?(node, 'models') in_directory?(node, 'models')
end end
# Returns true if the given node resides in app/services or ee/app/services. # Returns true if the given node resides in app/services or ee/app/services.
def in_service_class?(node) def in_service_class?(node)
in_app_directory?(node, 'services') in_directory?(node, 'services')
end end
# Returns true if the given node resides in app/presenters or # Returns true if the given node resides in app/presenters or
# ee/app/presenters. # ee/app/presenters.
def in_presenter?(node) def in_presenter?(node)
in_app_directory?(node, 'presenters') in_directory?(node, 'presenters')
end end
# Returns true if the given node resides in app/serializers or # Returns true if the given node resides in app/serializers or
# ee/app/serializers. # ee/app/serializers.
def in_serializer?(node) def in_serializer?(node)
in_app_directory?(node, 'serializers') in_directory?(node, 'serializers')
end end
# Returns true if the given node resides in app/workers or ee/app/workers. # Returns true if the given node resides in app/workers or ee/app/workers.
def in_worker?(node) def in_worker?(node)
in_app_directory?(node, 'workers') in_directory?(node, 'workers')
end end
# Returns true if the given node resides in app/controllers or # Returns true if the given node resides in app/controllers or
# ee/app/controllers. # ee/app/controllers.
def in_controller?(node) def in_controller?(node)
in_app_directory?(node, 'controllers') in_directory?(node, 'controllers')
end
# Returns true if the given node resides in app/graphql/types,
# ee/app/graphql/types, or ee/app/graphql/ee/types.
def in_graphql_types?(node)
in_app_directory?(node, 'graphql/types') || in_app_directory?(node, 'graphql/ee/types')
end end
# Returns true if the given node resides in lib/api or ee/lib/api. # Returns true if the given node resides in lib/api or ee/lib/api.
def in_api?(node) def in_api?(node)
in_lib_directory?(node, 'api')
end
# Returns true if the given node resides in spec or ee/spec.
def in_spec?(node)
file_path_for_node(node).start_with?( file_path_for_node(node).start_with?(
ce_spec_directory, File.join(ce_lib_directory, 'api'),
ee_spec_directory File.join(ee_lib_directory, 'api')
) )
end end
# Returns `true` if the given AST node resides in the given directory, # Returns `true` if the given AST node resides in the given directory,
# relative to app and/or ee/app. # relative to app and/or ee/app.
def in_app_directory?(node, directory) def in_directory?(node, directory)
file_path_for_node(node).start_with?( file_path_for_node(node).start_with?(
File.join(ce_app_directory, directory), File.join(ce_app_directory, directory),
File.join(ee_app_directory, directory) File.join(ee_app_directory, directory)
) )
end end
# Returns `true` if the given AST node resides in the given directory,
# relative to lib and/or ee/lib.
def in_lib_directory?(node, directory)
file_path_for_node(node).start_with?(
File.join(ce_lib_directory, directory),
File.join(ee_lib_directory, directory)
)
end
# Returns the receiver name of a send node. # Returns the receiver name of a send node.
# #
# For the AST node `(send (const nil? :Foo) ...)` this would return # For the AST node `(send (const nil? :Foo) ...)` this would return
...@@ -169,14 +149,6 @@ module RuboCop ...@@ -169,14 +149,6 @@ module RuboCop
File.join(rails_root, 'ee', 'lib') File.join(rails_root, 'ee', 'lib')
end end
def ce_spec_directory
File.join(rails_root, 'spec')
end
def ee_spec_directory
File.join(rails_root, 'ee', 'spec')
end
def rails_root def rails_root
File.expand_path('..', __dir__) File.expand_path('..', __dir__)
end end
......
# frozen_string_literal: true
require_relative '../../code_reuse_helpers'
module RuboCop
module Cop
module Gitlab
# This cop tracks the usage of feature flags among the codebase.
#
# The files set in `tmp/feature_flags/*.used` can then be used for verification purpose.
#
class MarkUsedFeatureFlags < RuboCop::Cop::Cop
include RuboCop::CodeReuseHelpers
FEATURE_METHODS = %i[enabled? disabled?].freeze
EXPERIMENTATION_METHODS = %i[active?].freeze
EXPERIMENT_METHODS = %i[
experiment
experiment_enabled?
push_frontend_experiment
].freeze
RUGGED_METHODS = %i[
use_rugged?
].freeze
WORKER_METHODS = %i[
data_consistency
].freeze
GRAPHQL_METHODS = %i[
field
].freeze
SELF_METHODS = %i[
push_frontend_feature_flag
limit_feature_flag=
].freeze + EXPERIMENT_METHODS + RUGGED_METHODS + WORKER_METHODS
RESTRICT_ON_SEND = FEATURE_METHODS + EXPERIMENTATION_METHODS + GRAPHQL_METHODS + SELF_METHODS
USAGE_DATA_COUNTERS_EVENTS_YAML_GLOBS = [
File.expand_path("../../../config/metrics/aggregates/*.yml", __dir__),
File.expand_path("../../../lib/gitlab/usage_data_counters/known_events/*.yml", __dir__)
].freeze
DYNAMIC_FEATURE_FLAGS = [
:usage_data_static_site_editor_commits, # https://gitlab.com/gitlab-org/gitlab/-/issues/284082
:usage_data_static_site_editor_merge_requests # https://gitlab.com/gitlab-org/gitlab/-/issues/284083
].freeze
# Called before all on_... have been called
# When refining this method, always call `super`
def on_new_investigation
super
track_dynamic_feature_flags!
track_usage_data_counters_known_events!
end
def on_casgn(node)
_, lhs_name, rhs = *node
save_used_feature_flag(rhs.value) if lhs_name == :FEATURE_FLAG
end
def on_send(node)
return if in_spec?(node)
return unless trackable_flag?(node)
flag_arg = flag_arg(node)
flag_value = flag_value(node)
return unless flag_value
if flag_arg_is_str_or_sym?(node)
if caller_is_feature_gitaly?(node)
save_used_feature_flag("gitaly_#{flag_value}")
else
save_used_feature_flag(flag_value)
end
if experiment_method?(node) || experimentation_method?(node)
# Additionally, mark experiment-related feature flag as used as well
matching_feature_flags = defined_feature_flags.select { |flag| flag == "#{flag_value}_experiment_percentage" }
matching_feature_flags.each do |matching_feature_flag|
puts_if_ci(node, "The '#{matching_feature_flag}' feature flag tracks the #{flag_value} experiment, which is still in use, so we'll mark it as used.")
save_used_feature_flag(matching_feature_flag)
end
end
elsif flag_arg_is_send_type?(node)
puts_if_ci(node, "Feature flag is dynamic: '#{flag_value}.")
elsif flag_arg_is_dstr_or_dsym?(node)
str_prefix = flag_arg.children[0]
rest_children = flag_arg.children[1..]
if rest_children.none? { |child| child.str_type? }
matching_feature_flags = defined_feature_flags.select { |flag| flag.start_with?(str_prefix.value) }
matching_feature_flags.each do |matching_feature_flag|
puts_if_ci(node, "The '#{matching_feature_flag}' feature flag starts with '#{str_prefix.value}', so we'll optimistically mark it as used.")
save_used_feature_flag(matching_feature_flag)
end
else
puts_if_ci(node, "Interpolated feature flag name has multiple static string parts, we won't track it.")
end
else
puts_if_ci(node, "Feature flag has an unknown type: #{flag_arg.type}.")
end
end
private
def puts_if_ci(node, text)
puts "#{text} (call: `#{node.source}`, source: #{node.location.expression.source_buffer.name})" if ENV['CI']
end
def save_used_feature_flag(feature_flag_name)
used_feature_flag_file = File.expand_path("../../../tmp/feature_flags/#{feature_flag_name}.used", __dir__)
return if File.exist?(used_feature_flag_file)
FileUtils.touch(used_feature_flag_file)
end
def class_caller(node)
node.children[0]&.const_name.to_s
end
def method_name(node)
node.children[1]
end
def flag_arg(node)
if worker_method?(node)
return unless node.children.size > 3
node.children[3].each_pair.find do |pair|
pair.key.value == :feature_flag
end&.value
elsif graphql_method?(node)
return unless node.children.size > 3
opts_index = node.children[3].hash_type? ? 3 : 4
return unless node.children[opts_index]
node.children[opts_index].each_pair.find do |pair|
pair.key.value == :feature_flag
end&.value
else
arg_index = rugged_method?(node) ? 3 : 2
node.children[arg_index]
end
end
def flag_value(node)
flag_arg = flag_arg(node)
return unless flag_arg
if flag_arg.respond_to?(:value)
flag_arg.value
else
flag_arg
end.to_s.tr("\n/", ' _')
end
def flag_arg_is_str_or_sym?(node)
flag_arg = flag_arg(node)
flag_arg.str_type? || flag_arg.sym_type?
end
def flag_arg_is_send_type?(node)
flag_arg(node).send_type?
end
def flag_arg_is_dstr_or_dsym?(node)
flag = flag_arg(node)
(flag.dstr_type? || flag.dsym_type?) && flag.children[0].str_type?
end
def caller_is_feature?(node)
class_caller(node) == "Feature"
end
def caller_is_feature_gitaly?(node)
class_caller(node) == "Feature::Gitaly"
end
def caller_is_experimentation?(node)
class_caller(node) == "Gitlab::Experimentation"
end
def experiment_method?(node)
EXPERIMENT_METHODS.include?(method_name(node))
end
def rugged_method?(node)
RUGGED_METHODS.include?(method_name(node))
end
def feature_method?(node)
FEATURE_METHODS.include?(method_name(node)) && (caller_is_feature?(node) || caller_is_feature_gitaly?(node))
end
def experimentation_method?(node)
EXPERIMENTATION_METHODS.include?(method_name(node)) && caller_is_experimentation?(node)
end
def worker_method?(node)
WORKER_METHODS.include?(method_name(node))
end
def graphql_method?(node)
GRAPHQL_METHODS.include?(method_name(node)) && in_graphql_types?(node)
end
def self_method?(node)
SELF_METHODS.include?(method_name(node)) && class_caller(node).empty?
end
def trackable_flag?(node)
feature_method?(node) || experimentation_method?(node) || graphql_method?(node) || self_method?(node)
end
# Marking all event's feature flags as used as Gitlab::UsageDataCounters::HLLRedisCounter.track_event{,context}
# is mostly used with dynamic event name.
def track_dynamic_feature_flags!
DYNAMIC_FEATURE_FLAGS.each(&method(:save_used_feature_flag))
end
# Marking all event's feature flags as used as Gitlab::UsageDataCounters::HLLRedisCounter.track_event{,context}
# is mostly used with dynamic event name.
def track_usage_data_counters_known_events!
usage_data_counters_known_event_feature_flags.each(&method(:save_used_feature_flag))
end
def usage_data_counters_known_event_feature_flags
USAGE_DATA_COUNTERS_EVENTS_YAML_GLOBS.each_with_object(Set.new) do |glob, memo|
Dir.glob(glob).each do |path|
YAML.safe_load(File.read(path))&.each do |hash|
memo << hash['feature_flag'] if hash['feature_flag']
end
end
end
end
def defined_feature_flags
@defined_feature_flags ||= begin
flags_paths = [
'config/feature_flags/**/*.yml'
]
# For EE additionally process `ee/` feature flags
if File.exist?(File.expand_path('../../../ee/app/models/license.rb', __dir__)) && !%w[true 1].include?(ENV['FOSS_ONLY'].to_s)
flags_paths << 'ee/config/feature_flags/**/*.yml'
end
flags_paths.each_with_object([]) do |flags_path, memo|
flags_path = File.expand_path("../../../#{flags_path}", __dir__)
Dir.glob(flags_path).each do |path|
feature_flag_name = File.basename(path, '.yml')
memo << feature_flag_name
end
end
end
end
end
end
end
end
...@@ -28,16 +28,6 @@ flags_paths = [ ...@@ -28,16 +28,6 @@ flags_paths = [
# For EE additionally process `ee/` feature flags # For EE additionally process `ee/` feature flags
if File.exist?('ee/app/models/license.rb') && !%w[true 1].include?(ENV['FOSS_ONLY'].to_s) if File.exist?('ee/app/models/license.rb') && !%w[true 1].include?(ENV['FOSS_ONLY'].to_s)
flags_paths << 'ee/config/feature_flags/**/*.yml' flags_paths << 'ee/config/feature_flags/**/*.yml'
# Geo feature flags are constructed dynamically and there's no explicit checks in the codebase so we mark all
# the replicators' derived feature flags as used.
# See https://gitlab.com/gitlab-org/gitlab/-/blob/54e802e8fe76b6f93656d75ef9b566bf57b60f41/ee/lib/gitlab/geo/replicator.rb#L183-185
Dir.glob('ee/app/replicators/geo/*_replicator.rb').each_with_object(Set.new) do |path, memo|
replicator_name = File.basename(path, '.rb')
feature_flag_name = "geo_#{replicator_name.delete_suffix('_replicator')}_replication"
FileUtils.touch(File.join('tmp', 'feature_flags', "#{feature_flag_name}.used"))
end
end end
all_flags = {} all_flags = {}
...@@ -51,17 +41,7 @@ flags_paths.each do |flags_path| ...@@ -51,17 +41,7 @@ flags_paths.each do |flags_path|
feature_flag_name = File.basename(path, '.yml') feature_flag_name = File.basename(path, '.yml')
# TODO: we need a better way of tracking use of Gitaly FF across Gitaly and GitLab # TODO: we need a better way of tracking use of Gitaly FF across Gitaly and GitLab
if feature_flag_name.start_with?('gitaly_') next if feature_flag_name.start_with?('gitaly_')
puts "Skipping the #{feature_flag_name} feature flag since it starts with 'gitaly_'."
next
end
# Dynamic feature flag names for redirect to latest CI templates
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144/diffs#fa2193ace3f6a02f7ef9995ef9bc519eca92c4ee_57_84
if feature_flag_name.start_with?('redirect_to_latest_template_')
puts "Skipping the #{feature_flag_name} feature flag since it starts with 'redirect_to_latest_template_'."
next
end
all_flags[feature_flag_name] = File.exist?(File.join('tmp', 'feature_flags', feature_flag_name + '.used')) all_flags[feature_flag_name] = File.exist?(File.join('tmp', 'feature_flags', feature_flag_name + '.used'))
end end
......
...@@ -19,6 +19,12 @@ RSpec.describe ApplicationExperiment, :experiment do ...@@ -19,6 +19,12 @@ RSpec.describe ApplicationExperiment, :experiment do
allow(subject).to receive(:enabled?).and_return(true) allow(subject).to receive(:enabled?).and_return(true)
end end
it "naively assumes a 1x1 relationship to feature flags for tests" do
expect(Feature).to receive(:persist_used!).with('namespaced_stub')
described_class.new('namespaced/stub')
end
it "doesn't raise an exception without a defined control" do it "doesn't raise an exception without a defined control" do
# because we have a default behavior defined # because we have a default behavior defined
......
...@@ -22,8 +22,3 @@ ActiveSupport::Dependencies.autoload_paths << 'lib' ...@@ -22,8 +22,3 @@ ActiveSupport::Dependencies.autoload_paths << 'lib'
ActiveSupport::Dependencies.autoload_paths << 'ee/lib' ActiveSupport::Dependencies.autoload_paths << 'ee/lib'
ActiveSupport::XmlMini.backend = 'Nokogiri' ActiveSupport::XmlMini.backend = 'Nokogiri'
RSpec.configure do |config|
config.filter_run focus: true
config.run_all_when_everything_filtered = true
end
...@@ -150,31 +150,6 @@ RSpec.describe RuboCop::CodeReuseHelpers do ...@@ -150,31 +150,6 @@ RSpec.describe RuboCop::CodeReuseHelpers do
end end
end end
describe '#in_graphql_types?' do
%w[
app/graphql/types
ee/app/graphql/ee/types
ee/app/graphql/types
].each do |path|
it "returns true for a node in #{path}" do
node = build_and_parse_source('10', rails_root_join(path, 'foo.rb'))
expect(cop.in_graphql_types?(node)).to eq(true)
end
end
%w[
app/graphql/resolvers
app/foo
].each do |path|
it "returns true for a node in #{path}" do
node = build_and_parse_source('10', rails_root_join(path, 'foo.rb'))
expect(cop.in_graphql_types?(node)).to eq(false)
end
end
end
describe '#in_api?' do describe '#in_api?' do
it 'returns true for a node in the API directory' do it 'returns true for a node in the API directory' do
node = build_and_parse_source('10', rails_root_join('lib', 'api', 'foo.rb')) node = build_and_parse_source('10', rails_root_join('lib', 'api', 'foo.rb'))
...@@ -189,67 +164,25 @@ RSpec.describe RuboCop::CodeReuseHelpers do ...@@ -189,67 +164,25 @@ RSpec.describe RuboCop::CodeReuseHelpers do
end end
end end
describe '#in_spec?' do describe '#in_directory?' do
it 'returns true for a node in the spec directory' do
node = build_and_parse_source('10', rails_root_join('spec', 'foo.rb'))
expect(cop.in_spec?(node)).to eq(true)
end
it 'returns true for a node in the ee/spec directory' do
node = build_and_parse_source('10', rails_root_join('ee', 'spec', 'foo.rb'))
expect(cop.in_spec?(node)).to eq(true)
end
it 'returns false for a node outside the spec directory' do
node = build_and_parse_source('10', rails_root_join('lib', 'foo.rb'))
expect(cop.in_spec?(node)).to eq(false)
end
end
describe '#in_app_directory?' do
it 'returns true for a directory in the CE app/ directory' do it 'returns true for a directory in the CE app/ directory' do
node = build_and_parse_source('10', rails_root_join('app', 'models', 'foo.rb')) node = build_and_parse_source('10', rails_root_join('app', 'models', 'foo.rb'))
expect(cop.in_app_directory?(node, 'models')).to eq(true) expect(cop.in_directory?(node, 'models')).to eq(true)
end end
it 'returns true for a directory in the EE app/ directory' do it 'returns true for a directory in the EE app/ directory' do
node = node =
build_and_parse_source('10', rails_root_join('ee', 'app', 'models', 'foo.rb')) build_and_parse_source('10', rails_root_join('ee', 'app', 'models', 'foo.rb'))
expect(cop.in_app_directory?(node, 'models')).to eq(true) expect(cop.in_directory?(node, 'models')).to eq(true)
end end
it 'returns false for a directory in the lib/ directory' do it 'returns false for a directory in the lib/ directory' do
node = node =
build_and_parse_source('10', rails_root_join('lib', 'models', 'foo.rb')) build_and_parse_source('10', rails_root_join('lib', 'models', 'foo.rb'))
expect(cop.in_app_directory?(node, 'models')).to eq(false) expect(cop.in_directory?(node, 'models')).to eq(false)
end
end
describe '#in_lib_directory?' do
it 'returns true for a directory in the CE lib/ directory' do
node = build_and_parse_source('10', rails_root_join('lib', 'models', 'foo.rb'))
expect(cop.in_lib_directory?(node, 'models')).to eq(true)
end
it 'returns true for a directory in the EE lib/ directory' do
node =
build_and_parse_source('10', rails_root_join('ee', 'lib', 'models', 'foo.rb'))
expect(cop.in_lib_directory?(node, 'models')).to eq(true)
end
it 'returns false for a directory in the app/ directory' do
node =
build_and_parse_source('10', rails_root_join('app', 'models', 'foo.rb'))
expect(cop.in_lib_directory?(node, 'models')).to eq(false)
end end
end end
......
# frozen_string_literal: true
require 'fast_spec_helper'
require 'rubocop'
require 'rubocop/rspec/support'
require_relative '../../../../rubocop/cop/gitlab/mark_used_feature_flags'
RSpec.describe RuboCop::Cop::Gitlab::MarkUsedFeatureFlags do
let(:defined_feature_flags) do
%w[a_feature_flag foo_hello foo_world baz_experiment_percentage bar_baz]
end
subject(:cop) { described_class.new }
before do
stub_const("#{described_class}::DYNAMIC_FEATURE_FLAGS", [])
allow(cop).to receive(:defined_feature_flags).and_return(defined_feature_flags)
allow(cop).to receive(:usage_data_counters_known_event_feature_flags).and_return([])
end
def feature_flag_path(feature_flag_name)
File.expand_path("../../../../tmp/feature_flags/#{feature_flag_name}.used", __dir__)
end
shared_examples 'sets flag as used' do |method_call, flags_to_be_set|
it 'sets the flag as used' do
Array(flags_to_be_set).each do |flag_to_be_set|
expect(FileUtils).to receive(:touch).with(feature_flag_path(flag_to_be_set))
end
expect_no_offenses(<<~RUBY)
class Foo < ApplicationRecord
#{method_call}
end
RUBY
end
end
shared_examples 'does not set any flags as used' do |method_call|
it 'sets the flag as used' do
expect(FileUtils).not_to receive(:touch)
expect_no_offenses(method_call)
end
end
%w[
Feature.enabled?
Feature.disabled?
push_frontend_feature_flag
].each do |feature_flag_method|
context "#{feature_flag_method} method" do
context 'a string feature flag' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}("foo")|, 'foo'
end
context 'a symbol feature flag' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(:foo)|, 'foo'
end
context 'an interpolated string feature flag with a string prefix' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}("foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated symbol feature flag with a string prefix' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(:"foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'a string with a "/" in it' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}("bar/baz")|, 'bar_baz'
end
context 'an interpolated string feature flag with a string prefix and suffix' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(:"foo_\#{bar}_baz")|
end
context 'a dynamic string feature flag as a variable' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(a_variable, an_arg)|
end
context 'an integer feature flag' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(123)|
end
end
end
%w[
Feature::Gitaly.enabled?
Feature::Gitaly.disabled?
].each do |feature_flag_method|
context "#{feature_flag_method} method" do
context 'a string feature flag' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}("foo")|, 'gitaly_foo'
end
context 'a symbol feature flag' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(:foo)|, 'gitaly_foo'
end
context 'an interpolated string feature flag with a string prefix' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}("foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated symbol feature flag with a string prefix' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(:"foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated string feature flag with a string prefix and suffix' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(:"foo_\#{bar}_baz")|
end
context 'a dynamic string feature flag as a variable' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(a_variable, an_arg)|
end
context 'an integer feature flag' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(123)|
end
end
end
%w[
experiment
experiment_enabled?
push_frontend_experiment
Gitlab::Experimentation.active?
].each do |feature_flag_method|
context "#{feature_flag_method} method" do
context 'a string feature flag' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}("baz")|, %w[baz baz_experiment_percentage]
end
context 'a symbol feature flag' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(:baz)|, %w[baz baz_experiment_percentage]
end
context 'an interpolated string feature flag with a string prefix' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}("foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated symbol feature flag with a string prefix' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(:"foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated string feature flag with a string prefix and suffix' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(:"foo_\#{bar}_baz")|
end
context 'a dynamic string feature flag as a variable' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(a_variable, an_arg)|
end
context 'an integer feature flag' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(123)|
end
end
end
%w[
use_rugged?
].each do |feature_flag_method|
context "#{feature_flag_method} method" do
context 'a string feature flag' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(arg, "baz")|, 'baz'
end
context 'a symbol feature flag' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(arg, :baz)|, 'baz'
end
context 'an interpolated string feature flag with a string prefix' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(arg, "foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated symbol feature flag with a string prefix' do
include_examples 'sets flag as used', %Q|#{feature_flag_method}(arg, :"foo_\#{bar}")|, %w[foo_hello foo_world]
end
context 'an interpolated string feature flag with a string prefix and suffix' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(arg, :"foo_\#{bar}_baz")|
end
context 'a dynamic string feature flag as a variable' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(a_variable, an_arg)|
end
context 'an integer feature flag' do
include_examples 'does not set any flags as used', %Q|#{feature_flag_method}(arg, 123)|
end
end
end
describe 'self.limit_feature_flag = :foo' do
include_examples 'sets flag as used', 'self.limit_feature_flag = :foo', 'foo'
end
describe 'FEATURE_FLAG = :foo' do
include_examples 'sets flag as used', 'FEATURE_FLAG = :foo', 'foo'
end
describe 'Worker `data_consistency` method' do
include_examples 'sets flag as used', 'data_consistency :delayed, feature_flag: :foo', 'foo'
include_examples 'does not set any flags as used', 'data_consistency :delayed'
end
describe 'GraphQL `field` method' do
before do
allow(cop).to receive(:in_graphql_types?).and_return(true)
end
include_examples 'sets flag as used', 'field :runners, Types::Ci::RunnerType.connection_type, null: true, feature_flag: :foo', 'foo'
include_examples 'sets flag as used', 'field :runners, null: true, feature_flag: :foo', 'foo'
include_examples 'does not set any flags as used', 'field :solution'
include_examples 'does not set any flags as used', 'field :runners, Types::Ci::RunnerType.connection_type'
include_examples 'does not set any flags as used', 'field :runners, Types::Ci::RunnerType.connection_type, null: true, description: "hello world"'
include_examples 'does not set any flags as used', 'field :solution, type: GraphQL::STRING_TYPE, null: true, description: "URL to the vulnerabilitys details page."'
end
describe "tracking of usage data metrics known events happens at the beginning of inspection" do
let(:usage_data_counters_known_event_feature_flags) { ['an_event_feature_flag'] }
before do
allow(cop).to receive(:usage_data_counters_known_event_feature_flags).and_return(usage_data_counters_known_event_feature_flags)
end
include_examples 'sets flag as used', "FEATURE_FLAG = :foo", %w[foo an_event_feature_flag]
end
end
...@@ -4,6 +4,16 @@ ...@@ -4,6 +4,16 @@
require 'gitlab/experiment/rspec' require 'gitlab/experiment/rspec'
require_relative 'stub_snowplow' require_relative 'stub_snowplow'
# This is a temporary fix until we have a larger discussion around the
# challenges raised in https://gitlab.com/gitlab-org/gitlab/-/issues/300104
require Rails.root.join('app', 'experiments', 'application_experiment')
class ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
def initialize(...)
super(...)
Feature.persist_used!(feature_flag_name)
end
end
RSpec.configure do |config| RSpec.configure do |config|
config.include StubSnowplow, :experiment config.include StubSnowplow, :experiment
......
...@@ -11,6 +11,7 @@ module StubExperiments ...@@ -11,6 +11,7 @@ module StubExperiments
allow(Gitlab::Experimentation).to receive(:active?).and_call_original allow(Gitlab::Experimentation).to receive(:active?).and_call_original
experiments.each do |experiment_key, enabled| experiments.each do |experiment_key, enabled|
Feature.persist_used!("#{experiment_key}#{feature_flag_suffix}")
allow(Gitlab::Experimentation).to receive(:active?).with(experiment_key) { enabled } allow(Gitlab::Experimentation).to receive(:active?).with(experiment_key) { enabled }
end end
end end
...@@ -25,6 +26,7 @@ module StubExperiments ...@@ -25,6 +26,7 @@ module StubExperiments
allow(Gitlab::Experimentation).to receive(:in_experiment_group?).and_call_original allow(Gitlab::Experimentation).to receive(:in_experiment_group?).and_call_original
experiments.each do |experiment_key, enabled| experiments.each do |experiment_key, enabled|
Feature.persist_used!("#{experiment_key}#{feature_flag_suffix}")
allow(Gitlab::Experimentation).to receive(:in_experiment_group?).with(experiment_key, anything) { enabled } allow(Gitlab::Experimentation).to receive(:in_experiment_group?).with(experiment_key, anything) { enabled }
end end
end end
......
...@@ -4,6 +4,14 @@ ...@@ -4,6 +4,14 @@
module StubbedFeature module StubbedFeature
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do
cattr_reader(:persist_used) do
# persist feature flags in CI
# nil: indicates that we do not want to persist used feature flags
Gitlab::Utils.to_boolean(ENV['CI']) ? {} : nil
end
end
class_methods do class_methods do
# Turn stubbed feature flags on or off. # Turn stubbed feature flags on or off.
def stub=(stub) def stub=(stub)
...@@ -33,6 +41,8 @@ module StubbedFeature ...@@ -33,6 +41,8 @@ module StubbedFeature
feature_flag = super feature_flag = super
return feature_flag unless stub? return feature_flag unless stub?
persist_used!(args.first)
# If feature flag is not persisted we mark the feature flag as enabled # If feature flag is not persisted we mark the feature flag as enabled
# We do `m.call` as we want to validate the execution of method arguments # We do `m.call` as we want to validate the execution of method arguments
# and a feature flag state if it is not persisted # and a feature flag state if it is not persisted
...@@ -42,5 +52,17 @@ module StubbedFeature ...@@ -42,5 +52,17 @@ module StubbedFeature
feature_flag feature_flag
end end
# This method creates a temporary file in `tmp/feature_flags`
# if feature flag was touched during execution
def persist_used!(name)
return unless persist_used
return if persist_used[name]
persist_used[name] = true
FileUtils.touch(
Rails.root.join('tmp', 'feature_flags', name.to_s + ".used")
)
end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment