Commit 12d18b33 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch '284596-introduce-static-analysis-check-for-feature-flag-usage-take-2' into 'master'

Introduce a new Gitlab/MarkUsedFeatureFlags cop (take 2)

See merge request gitlab-org/gitlab!63979
parents 085ff109 135ef018
......@@ -47,7 +47,6 @@
- rspec_profiling/
- tmp/capybara/
- tmp/memory_test/
- tmp/feature_flags/
- log/*.log
reports:
junit: junit_rspec.xml
......@@ -240,6 +239,11 @@ static-analysis:
script:
- run_timed_command "retry yarn install --frozen-lockfile"
- scripts/static-analysis
artifacts:
expire_in: 31d
when: always
paths:
- tmp/feature_flags/
static-analysis as-if-foss:
extends:
......@@ -490,23 +494,7 @@ rspec:feature-flags:
- .coverage-base
- .rails:rules:rspec-feature-flags
stage: post-test
# 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
needs: ["static-analysis"]
script:
- !reference [.minimal-bundle-install, script]
- if [ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]; then
......
......@@ -932,14 +932,6 @@
- <<: *if-merge-request-title-run-all-rspec
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:
rules:
- <<: *if-default-branch-schedule-nightly
......@@ -954,6 +946,12 @@
- <<: *if-merge-request
changes: [".gitlab/ci/rails.gitlab-ci.yml"]
.rails:rules:rspec-feature-flags:
rules:
- <<: *if-not-ee
when: never
- changes: *code-backstage-patterns
#########################
# Static analysis rules #
#########################
......
......@@ -50,10 +50,9 @@ class Projects::IssuesController < Projects::ApplicationController
end
before_action only: :show do
real_time_feature_flag = :real_time_issue_sidebar
real_time_enabled = Gitlab::ActionCable::Config.in_app? || Feature.enabled?(real_time_feature_flag, @project)
real_time_enabled = Gitlab::ActionCable::Config.in_app? || Feature.enabled?(:real_time_issue_sidebar, @project)
push_to_gon_attributes(:features, real_time_feature_flag, real_time_enabled)
push_to_gon_attributes(:features, :real_time_issue_sidebar, real_time_enabled)
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(:labels_widget, @project, default_enabled: :yaml)
......
......@@ -3,9 +3,8 @@
- return unless show_trial_status_widget?(root_group)
-# For the current Growth::Conversion experiment
- experiment_key = :show_trial_status_in_sidebar
- record_experiment_group(experiment_key, root_group)
- return unless experiment_enabled?(experiment_key, subject: root_group)
- record_experiment_group(:show_trial_status_in_sidebar, root_group)
- return unless experiment_enabled?(:show_trial_status_in_sidebar, subject: root_group)
= nav_link do
#js-trial-status-widget{ data: trial_status_widget_data_attrs(root_group) }
......
......@@ -32,59 +32,79 @@ module RuboCop
# Returns true if the given node resides in app/finders or ee/app/finders.
def in_finder?(node)
in_directory?(node, 'finders')
in_app_directory?(node, 'finders')
end
# Returns true if the given node resides in app/models or ee/app/models.
def in_model?(node)
in_directory?(node, 'models')
in_app_directory?(node, 'models')
end
# Returns true if the given node resides in app/services or ee/app/services.
def in_service_class?(node)
in_directory?(node, 'services')
in_app_directory?(node, 'services')
end
# Returns true if the given node resides in app/presenters or
# ee/app/presenters.
def in_presenter?(node)
in_directory?(node, 'presenters')
in_app_directory?(node, 'presenters')
end
# Returns true if the given node resides in app/serializers or
# ee/app/serializers.
def in_serializer?(node)
in_directory?(node, 'serializers')
in_app_directory?(node, 'serializers')
end
# Returns true if the given node resides in app/workers or ee/app/workers.
def in_worker?(node)
in_directory?(node, 'workers')
in_app_directory?(node, 'workers')
end
# Returns true if the given node resides in app/controllers or
# ee/app/controllers.
def in_controller?(node)
in_directory?(node, 'controllers')
in_app_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
# Returns true if the given node resides in lib/api or ee/lib/api.
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.join(ce_lib_directory, 'api'),
File.join(ee_lib_directory, 'api')
ce_spec_directory,
ee_spec_directory
)
end
# Returns `true` if the given AST node resides in the given directory,
# relative to app and/or ee/app.
def in_directory?(node, directory)
def in_app_directory?(node, directory)
file_path_for_node(node).start_with?(
File.join(ce_app_directory, directory),
File.join(ee_app_directory, directory)
)
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.
#
# For the AST node `(send (const nil? :Foo) ...)` this would return
......@@ -149,6 +169,14 @@ module RuboCop
File.join(rails_root, 'ee', 'lib')
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
File.expand_path('..', __dir__)
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
deduplicate
].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
......@@ -24,7 +24,10 @@ class StaticAnalysis
(Gitlab.ee? ? %w[bin/rake gettext:updated_check] : nil) => 410,
# Most of the time, RuboCop finishes in 30 seconds, but sometimes it can take around 1200 seconds so we set a
# duration of 300 to lower the likelihood that it will run in the same job as another long task...
%w[bundle exec rubocop --parallel] => 300,
%w[bundle exec rubocop --parallel --except Gitlab/MarkUsedFeatureFlags] => 300,
# We need to disable the cache for this cop since it creates files under tmp/feature_flags/*.used,
# the cache would prevent these files from being created.
%w[bundle exec rubocop --only Gitlab/MarkUsedFeatureFlags --cache false] => 600,
%w[yarn run lint:eslint:all] => 264,
%w[yarn run lint:prettier] => 134,
%w[bin/rake gettext:lint] => 81,
......
......@@ -28,6 +28,16 @@ flags_paths = [
# For EE additionally process `ee/` feature flags
if File.exist?('ee/app/models/license.rb') && !%w[true 1].include?(ENV['FOSS_ONLY'].to_s)
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
all_flags = {}
......@@ -41,7 +51,17 @@ flags_paths.each do |flags_path|
feature_flag_name = File.basename(path, '.yml')
# TODO: we need a better way of tracking use of Gitaly FF across Gitaly and GitLab
next if feature_flag_name.start_with?('gitaly_')
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'))
end
......
......@@ -19,12 +19,6 @@ RSpec.describe ApplicationExperiment, :experiment do
allow(subject).to receive(:enabled?).and_return(true)
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
# because we have a default behavior defined
......
......@@ -22,3 +22,8 @@ ActiveSupport::Dependencies.autoload_paths << 'lib'
ActiveSupport::Dependencies.autoload_paths << 'ee/lib'
ActiveSupport::XmlMini.backend = 'Nokogiri'
RSpec.configure do |config|
config.filter_run focus: true
config.run_all_when_everything_filtered = true
end
......@@ -150,6 +150,31 @@ RSpec.describe RuboCop::CodeReuseHelpers do
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
it 'returns true for a node in the API directory' do
node = build_and_parse_source('10', rails_root_join('lib', 'api', 'foo.rb'))
......@@ -164,25 +189,67 @@ RSpec.describe RuboCop::CodeReuseHelpers do
end
end
describe '#in_directory?' do
describe '#in_spec?' 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
node = build_and_parse_source('10', rails_root_join('app', 'models', 'foo.rb'))
expect(cop.in_directory?(node, 'models')).to eq(true)
expect(cop.in_app_directory?(node, 'models')).to eq(true)
end
it 'returns true for a directory in the EE app/ directory' do
node =
build_and_parse_source('10', rails_root_join('ee', 'app', 'models', 'foo.rb'))
expect(cop.in_directory?(node, 'models')).to eq(true)
expect(cop.in_app_directory?(node, 'models')).to eq(true)
end
it 'returns false for a directory in the lib/ directory' do
node =
build_and_parse_source('10', rails_root_join('lib', 'models', 'foo.rb'))
expect(cop.in_directory?(node, 'models')).to eq(false)
expect(cop.in_app_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
......
# 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 'Worker `deduplicate` method' do
include_examples 'sets flag as used', 'deduplicate :delayed, feature_flag: :foo', 'foo'
include_examples 'does not set any flags as used', 'deduplicate :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,16 +4,6 @@
require 'gitlab/experiment/rspec'
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|
config.include StubSnowplow, :experiment
......
......@@ -11,7 +11,6 @@ module StubExperiments
allow(Gitlab::Experimentation).to receive(:active?).and_call_original
experiments.each do |experiment_key, enabled|
Feature.persist_used!("#{experiment_key}#{feature_flag_suffix}")
allow(Gitlab::Experimentation).to receive(:active?).with(experiment_key) { enabled }
end
end
......@@ -26,7 +25,6 @@ module StubExperiments
allow(Gitlab::Experimentation).to receive(:in_experiment_group?).and_call_original
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 }
end
end
......
......@@ -4,14 +4,6 @@
module StubbedFeature
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
# Turn stubbed feature flags on or off.
def stub=(stub)
......@@ -41,8 +33,6 @@ module StubbedFeature
feature_flag = super
return feature_flag unless stub?
persist_used!(args.first)
# 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
# and a feature flag state if it is not persisted
......@@ -52,17 +42,5 @@ module StubbedFeature
feature_flag
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
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