File: factory_default.rb

package info (click to toggle)
ruby-test-prof 1.5.2%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 660 kB
  • sloc: ruby: 6,064; makefile: 4
file content (356 lines) | stat: -rw-r--r-- 8,687 bytes parent folder | download
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# frozen_string_literal: true

require "test_prof/core"

require "test_prof/factory_default/factory_bot_patch"
require "test_prof/factory_default/fabrication_patch"

require "test_prof/ext/float_duration"
require "test_prof/ext/active_record_refind" if defined?(::ActiveRecord::Base)

module TestProf
  # FactoryDefault allows use to re-use associated objects
  # in factories implicilty
  module FactoryDefault
    using FloatDuration
    using Ext::ActiveRecordRefind if defined?(::ActiveRecord::Base)

    using(Module.new do
      refine Object do
        def to_override_key
          "<#{self.class.name}::$id$#{object_id}$di$>"
        end

        def refind
          self
        end
      end

      if defined?(::ActiveRecord::Base)
        refine ::ActiveRecord::Base do
          def to_override_key
            "<#{self.class.name}\#$id$#{public_send(self.class.primary_key)}$di$>"
          end
        end
      end

      [
        String,
        Integer,
        Float,
        FalseClass,
        TrueClass,
        NilClass,
        Regexp
      ].each do |mod|
        refine(mod) do
          def to_override_key
            inspect
          end
        end
      end
    end)

    class Profiler
      include Logging

      attr_reader :data

      def initialize
        @data = Hash.new { |h, k| h[k] = {count: 0, time: 0.0} }
      end

      def instrument(name, traits, overrides)
        start = TestProf.now
        yield.tap do
          time = TestProf.now - start
          key = build_association_name(name, traits, overrides)
          data[key][:count] += 1
          data[key][:time] += time
        end
      end

      def print_report
        if data.empty?
          log :info, "FactoryDefault profiler collected no data"
          return
        end

        # Merge object overrides into one stats record
        data = self.data.each_with_object({}) do |(name, stats), acc|
          name = name.gsub(/\$id\$.+\$di\$/, "<id>")
          if acc.key?(name)
            acc[name][:count] += stats[:count]
            acc[name][:time] += stats[:time]
          else
            acc[name] = stats
          end
        end

        msgs = []

        msgs <<
          <<~MSG
            Factory associations usage:
          MSG

        first_column = data.keys.map(&:size).max + 2

        msgs << format(
          "%#{first_column}s  %9s  %12s",
          "factory", "count", "total time"
        )

        msgs << ""

        total_count = 0
        total_time = 0.0

        data.to_a.sort_by { |(_, v)| -v[:time] }.each do |(key, factory_stats)|
          total_count += factory_stats[:count]
          total_time += factory_stats[:time]

          msgs << format(
            "%#{first_column}s  %9d  %12s",
            key, factory_stats[:count], factory_stats[:time].duration
          )
        end

        msgs <<
          <<~MSG

            Total associations created: #{total_count}
            Total uniq associations created: #{data.size}
            Total time spent: #{total_time.duration}

          MSG

        log :info, msgs.join("\n")
      end

      private

      def build_association_name(name, traits, overrides)
        traits_str = "[#{traits.join(",")}]" if traits&.any?
        overrides_str = "{#{overrides.map { |k, v| "#{k}:#{v.to_override_key}" }.join(",")}}" if overrides&.any?
        "#{name}#{traits_str}#{overrides_str}"
      end
    end

    class NoopProfiler
      def instrument(*)
        yield
      end

      def print_report
      end
    end

    class Configuration
      attr_accessor :preserve_traits, :preserve_attributes,
        :report_summary, :report_stats,
        :profiling_enabled

      alias_method :profiling_enabled?, :profiling_enabled

      def initialize
        # TODO(v2): Switch to true
        @preserve_traits = false
        @preserve_attributes = false
        @profiling_enabled = ENV["FACTORY_DEFAULT_PROF"] == "1"
        @report_summary = ENV["FACTORY_DEFAULT_SUMMARY"] == "1"
        @report_stats = ENV["FACTORY_DEFAULT_STATS"] == "1"
      end
    end

    class << self
      include Logging

      attr_accessor :current_context
      attr_reader :stats, :profiler

      def init
        FactoryBotPatch.patch
        FabricationPatch.patch

        @profiler = config.profiling_enabled? ? Profiler.new : NoopProfiler.new
        @enabled = ENV["FACTORY_DEFAULT_DISABLED"] != "1"
        @stats = {}
      end

      def config
        @config ||= Configuration.new
      end

      def configure
        yield config
      end

      # TODO(v2): drop
      def preserve_traits=(val)
        config.preserve_traits = val
      end

      def preserve_attributes=(val)
        config.preserve_attributes = val
      end

      def register(name, obj, **options)
        # Name with traits
        if name.is_a?(Array)
          register_traited_record(*name, obj, **options)
        else
          register_default_record(name, obj, **options)
        end

        obj
      end

      def get(name, traits = nil, overrides = nil, skip_stats: false)
        return unless enabled?

        record = store[name]
        return unless record

        if traits && (trait_key = record[:traits][traits])
          name = trait_key
          record = store[name]
          traits = nil
        end

        stats[name][:miss] += 1 unless skip_stats

        if traits && !traits.empty? && record[:preserve_traits]
          return
        end

        object = record[:object]

        if overrides && !overrides.empty? && record[:preserve_attributes]
          overrides.each do |name, value|
            return unless object.respond_to?(name) # rubocop:disable Lint/NonLocalExitFromIterator
            return if object.public_send(name) != value # rubocop:disable Lint/NonLocalExitFromIterator
          end
        end

        unless skip_stats
          stats[name][:miss] -= 1
          stats[name][:hit] += 1
        end

        if record[:context] && (record[:context] != :example)
          object.refind
        else
          object
        end
      end

      def remove(name)
        store.delete(name)
      end

      def reset(context: nil)
        return store.clear unless context

        store.delete_if do |_name, metadata|
          metadata[:context] == context
        end
      end

      def enabled?
        @enabled
      end

      def enable!
        was_enabled = @enabled
        @enabled = true
        return unless block_given?
        yield
      ensure
        @enabled = was_enabled
      end

      def disable!
        was_enabled = @enabled
        @enabled = false
        return unless block_given?
        yield
      ensure
        @enabled = was_enabled
      end

      def print_report
        profiler.print_report
        return unless config.report_stats || config.report_summary

        if stats.empty?
          log :info, "FactoryDefault has not been used"
          return
        end

        msgs = []

        if config.report_stats
          msgs <<
            <<~MSG
              FactoryDefault usage stats:
            MSG

          first_column = stats.keys.map(&:size).max + 2

          msgs << format(
            "%#{first_column}s  %9s  %9s",
            "factory", "hit", "miss"
          )

          msgs << ""
        end

        total_hit = 0
        total_miss = 0

        stats.to_a.sort_by { |(_, v)| -v[:hit] }.each do |(key, record_stats)|
          total_hit += record_stats[:hit]
          total_miss += record_stats[:miss]

          if config.report_stats
            msgs << format(
              "%#{first_column}s  %9d  %9d",
              key, record_stats[:hit], record_stats[:miss]
            )
          end
        end

        msgs << "" if config.report_stats

        msgs <<
          <<~MSG
            FactoryDefault summary: hit=#{total_hit} miss=#{total_miss}
          MSG

        log :info, msgs.join("\n")
      end

      private

      def register_default_record(name, obj, **options)
        store[name] = {object: obj, traits: {}, context: current_context, **options}
        stats[name] ||= {hit: 0, miss: 0}
      end

      def register_traited_record(name, *traits, obj, **options)
        name_with_traits = "#{name}[#{traits.join(",")}]"

        register_default_record(name_with_traits, obj, **options)
        register_default_record(name, obj, **options) unless store[name]

        # Add reference to the traited default to the original default record
        store[name][:traits][traits] = name_with_traits
      end

      def store
        Thread.current[:testprof_factory_default_store] ||= {}
      end
    end
  end
end