Welcome to mirror list, hosted at ThFree Co, Russian Federation.

experiments_test_coverage.rb « cop « rubocop - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
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