rspec-core 3.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/.document +5 -0
- data/.yardopts +8 -0
- data/Changelog.md +2243 -0
- data/LICENSE.md +26 -0
- data/README.md +384 -0
- data/exe/rspec +4 -0
- data/lib/rspec/autorun.rb +3 -0
- data/lib/rspec/core.rb +185 -0
- data/lib/rspec/core/backtrace_formatter.rb +65 -0
- data/lib/rspec/core/bisect/coordinator.rb +62 -0
- data/lib/rspec/core/bisect/example_minimizer.rb +173 -0
- data/lib/rspec/core/bisect/fork_runner.rb +134 -0
- data/lib/rspec/core/bisect/server.rb +61 -0
- data/lib/rspec/core/bisect/shell_command.rb +126 -0
- data/lib/rspec/core/bisect/shell_runner.rb +73 -0
- data/lib/rspec/core/bisect/utilities.rb +58 -0
- data/lib/rspec/core/configuration.rb +2308 -0
- data/lib/rspec/core/configuration_options.rb +233 -0
- data/lib/rspec/core/drb.rb +113 -0
- data/lib/rspec/core/dsl.rb +98 -0
- data/lib/rspec/core/example.rb +656 -0
- data/lib/rspec/core/example_group.rb +889 -0
- data/lib/rspec/core/example_status_persister.rb +235 -0
- data/lib/rspec/core/filter_manager.rb +231 -0
- data/lib/rspec/core/flat_map.rb +20 -0
- data/lib/rspec/core/formatters.rb +269 -0
- data/lib/rspec/core/formatters/base_bisect_formatter.rb +45 -0
- data/lib/rspec/core/formatters/base_formatter.rb +70 -0
- data/lib/rspec/core/formatters/base_text_formatter.rb +75 -0
- data/lib/rspec/core/formatters/bisect_drb_formatter.rb +29 -0
- data/lib/rspec/core/formatters/bisect_progress_formatter.rb +157 -0
- data/lib/rspec/core/formatters/console_codes.rb +68 -0
- data/lib/rspec/core/formatters/deprecation_formatter.rb +223 -0
- data/lib/rspec/core/formatters/documentation_formatter.rb +70 -0
- data/lib/rspec/core/formatters/exception_presenter.rb +508 -0
- data/lib/rspec/core/formatters/fallback_message_formatter.rb +28 -0
- data/lib/rspec/core/formatters/helpers.rb +110 -0
- data/lib/rspec/core/formatters/html_formatter.rb +153 -0
- data/lib/rspec/core/formatters/html_printer.rb +414 -0
- data/lib/rspec/core/formatters/html_snippet_extractor.rb +120 -0
- data/lib/rspec/core/formatters/json_formatter.rb +102 -0
- data/lib/rspec/core/formatters/profile_formatter.rb +68 -0
- data/lib/rspec/core/formatters/progress_formatter.rb +29 -0
- data/lib/rspec/core/formatters/protocol.rb +182 -0
- data/lib/rspec/core/formatters/snippet_extractor.rb +134 -0
- data/lib/rspec/core/formatters/syntax_highlighter.rb +91 -0
- data/lib/rspec/core/hooks.rb +624 -0
- data/lib/rspec/core/invocations.rb +87 -0
- data/lib/rspec/core/memoized_helpers.rb +554 -0
- data/lib/rspec/core/metadata.rb +498 -0
- data/lib/rspec/core/metadata_filter.rb +255 -0
- data/lib/rspec/core/minitest_assertions_adapter.rb +31 -0
- data/lib/rspec/core/mocking_adapters/flexmock.rb +31 -0
- data/lib/rspec/core/mocking_adapters/mocha.rb +57 -0
- data/lib/rspec/core/mocking_adapters/null.rb +14 -0
- data/lib/rspec/core/mocking_adapters/rr.rb +31 -0
- data/lib/rspec/core/mocking_adapters/rspec.rb +32 -0
- data/lib/rspec/core/notifications.rb +521 -0
- data/lib/rspec/core/option_parser.rb +309 -0
- data/lib/rspec/core/ordering.rb +158 -0
- data/lib/rspec/core/output_wrapper.rb +29 -0
- data/lib/rspec/core/pending.rb +165 -0
- data/lib/rspec/core/profiler.rb +34 -0
- data/lib/rspec/core/project_initializer.rb +48 -0
- data/lib/rspec/core/project_initializer/.rspec +1 -0
- data/lib/rspec/core/project_initializer/spec/spec_helper.rb +100 -0
- data/lib/rspec/core/rake_task.rb +168 -0
- data/lib/rspec/core/reporter.rb +257 -0
- data/lib/rspec/core/ruby_project.rb +53 -0
- data/lib/rspec/core/runner.rb +199 -0
- data/lib/rspec/core/sandbox.rb +37 -0
- data/lib/rspec/core/set.rb +54 -0
- data/lib/rspec/core/shared_context.rb +55 -0
- data/lib/rspec/core/shared_example_group.rb +269 -0
- data/lib/rspec/core/shell_escape.rb +49 -0
- data/lib/rspec/core/test_unit_assertions_adapter.rb +30 -0
- data/lib/rspec/core/version.rb +9 -0
- data/lib/rspec/core/warnings.rb +40 -0
- data/lib/rspec/core/world.rb +275 -0
- metadata +292 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,235 @@
|
|
1
|
+
RSpec::Support.require_rspec_support "directory_maker"
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module Core
|
5
|
+
# Persists example ids and their statuses so that we can filter
|
6
|
+
# to just the ones that failed the last time they ran.
|
7
|
+
# @private
|
8
|
+
class ExampleStatusPersister
|
9
|
+
def self.load_from(file_name)
|
10
|
+
return [] unless File.exist?(file_name)
|
11
|
+
ExampleStatusParser.parse(File.read(file_name))
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.persist(examples, file_name)
|
15
|
+
new(examples, file_name).persist
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(examples, file_name)
|
19
|
+
@examples = examples
|
20
|
+
@file_name = file_name
|
21
|
+
end
|
22
|
+
|
23
|
+
def persist
|
24
|
+
RSpec::Support::DirectoryMaker.mkdir_p(File.dirname(@file_name))
|
25
|
+
File.open(@file_name, File::RDWR | File::CREAT) do |f|
|
26
|
+
# lock the file while reading / persisting to avoid a race
|
27
|
+
# condition where parallel or unrelated spec runs race to
|
28
|
+
# update the same file
|
29
|
+
f.flock(File::LOCK_EX)
|
30
|
+
unparsed_previous_runs = f.read
|
31
|
+
f.rewind
|
32
|
+
f.write(dump_statuses(unparsed_previous_runs))
|
33
|
+
f.flush
|
34
|
+
f.truncate(f.pos)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def dump_statuses(unparsed_previous_runs)
|
41
|
+
statuses_from_previous_runs = ExampleStatusParser.parse(unparsed_previous_runs)
|
42
|
+
merged_statuses = ExampleStatusMerger.merge(statuses_from_this_run, statuses_from_previous_runs)
|
43
|
+
ExampleStatusDumper.dump(merged_statuses)
|
44
|
+
end
|
45
|
+
|
46
|
+
def statuses_from_this_run
|
47
|
+
@examples.map do |ex|
|
48
|
+
result = ex.execution_result
|
49
|
+
|
50
|
+
{
|
51
|
+
:example_id => ex.id,
|
52
|
+
:status => result.status ? result.status.to_s : Configuration::UNKNOWN_STATUS,
|
53
|
+
:run_time => result.run_time ? Formatters::Helpers.format_duration(result.run_time) : ""
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Merges together a list of example statuses from this run
|
60
|
+
# and a list from previous runs (presumably loaded from disk).
|
61
|
+
# Each example status object is expected to be a hash with
|
62
|
+
# at least an `:example_id` and a `:status` key. Examples that
|
63
|
+
# were loaded but not executed (due to filtering, `--fail-fast`
|
64
|
+
# or whatever) should have a `:status` of `UNKNOWN_STATUS`.
|
65
|
+
#
|
66
|
+
# This willl produce a new list that:
|
67
|
+
# - Will be missing examples from previous runs that we know for sure
|
68
|
+
# no longer exist.
|
69
|
+
# - Will have the latest known status for any examples that either
|
70
|
+
# definitively do exist or may still exist.
|
71
|
+
# - Is sorted by file name and example definition order, so that
|
72
|
+
# the saved file is easily scannable if users want to inspect it.
|
73
|
+
# @private
|
74
|
+
class ExampleStatusMerger
|
75
|
+
def self.merge(this_run, from_previous_runs)
|
76
|
+
new(this_run, from_previous_runs).merge
|
77
|
+
end
|
78
|
+
|
79
|
+
def initialize(this_run, from_previous_runs)
|
80
|
+
@this_run = hash_from(this_run)
|
81
|
+
@from_previous_runs = hash_from(from_previous_runs)
|
82
|
+
@file_exists_cache = Hash.new { |hash, file| hash[file] = File.exist?(file) }
|
83
|
+
end
|
84
|
+
|
85
|
+
def merge
|
86
|
+
delete_previous_examples_that_no_longer_exist
|
87
|
+
|
88
|
+
@this_run.merge(@from_previous_runs) do |_ex_id, new, old|
|
89
|
+
new.fetch(:status) == Configuration::UNKNOWN_STATUS ? old : new
|
90
|
+
end.values.sort_by(&method(:sort_value_from))
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def hash_from(example_list)
|
96
|
+
example_list.inject({}) do |hash, example|
|
97
|
+
hash[example.fetch(:example_id)] = example
|
98
|
+
hash
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def delete_previous_examples_that_no_longer_exist
|
103
|
+
@from_previous_runs.delete_if do |ex_id, _|
|
104
|
+
example_must_no_longer_exist?(ex_id)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def example_must_no_longer_exist?(ex_id)
|
109
|
+
# Obviously, it exists if it was loaded for this spec run...
|
110
|
+
return false if @this_run.key?(ex_id)
|
111
|
+
|
112
|
+
spec_file = spec_file_from(ex_id)
|
113
|
+
|
114
|
+
# `this_run` includes examples that were loaded but not executed.
|
115
|
+
# Given that, if the spec file for this example was loaded,
|
116
|
+
# but the id does not still exist, it's safe to assume that
|
117
|
+
# the example must no longer exist.
|
118
|
+
return true if loaded_spec_files.include?(spec_file)
|
119
|
+
|
120
|
+
# The example may still exist as long as the file exists...
|
121
|
+
!@file_exists_cache[spec_file]
|
122
|
+
end
|
123
|
+
|
124
|
+
def loaded_spec_files
|
125
|
+
@loaded_spec_files ||= Set.new(@this_run.keys.map(&method(:spec_file_from)))
|
126
|
+
end
|
127
|
+
|
128
|
+
def spec_file_from(ex_id)
|
129
|
+
ex_id.split("[").first
|
130
|
+
end
|
131
|
+
|
132
|
+
def sort_value_from(example)
|
133
|
+
file, scoped_id = Example.parse_id(example.fetch(:example_id))
|
134
|
+
[file, *scoped_id.split(":").map(&method(:Integer))]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Dumps a list of hashes in a pretty, human readable format
|
139
|
+
# for later parsing. The hashes are expected to have symbol
|
140
|
+
# keys and string values, and each hash should have the same
|
141
|
+
# set of keys.
|
142
|
+
# @private
|
143
|
+
class ExampleStatusDumper
|
144
|
+
def self.dump(examples)
|
145
|
+
new(examples).dump
|
146
|
+
end
|
147
|
+
|
148
|
+
def initialize(examples)
|
149
|
+
@examples = examples
|
150
|
+
end
|
151
|
+
|
152
|
+
def dump
|
153
|
+
return nil if @examples.empty?
|
154
|
+
(formatted_header_rows + formatted_value_rows).join("\n") << "\n"
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def formatted_header_rows
|
160
|
+
@formatted_header_rows ||= begin
|
161
|
+
dividers = column_widths.map { |w| "-" * w }
|
162
|
+
[formatted_row_from(headers.map(&:to_s)), formatted_row_from(dividers)]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def formatted_value_rows
|
167
|
+
@foramtted_value_rows ||= rows.map do |row|
|
168
|
+
formatted_row_from(row)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def rows
|
173
|
+
@rows ||= @examples.map { |ex| ex.values_at(*headers) }
|
174
|
+
end
|
175
|
+
|
176
|
+
def formatted_row_from(row_values)
|
177
|
+
padded_values = row_values.each_with_index.map do |value, index|
|
178
|
+
value.ljust(column_widths[index])
|
179
|
+
end
|
180
|
+
|
181
|
+
padded_values.join(" | ") << " |"
|
182
|
+
end
|
183
|
+
|
184
|
+
def headers
|
185
|
+
@headers ||= @examples.first.keys
|
186
|
+
end
|
187
|
+
|
188
|
+
def column_widths
|
189
|
+
@column_widths ||= begin
|
190
|
+
value_sets = rows.transpose
|
191
|
+
|
192
|
+
headers.each_with_index.map do |header, index|
|
193
|
+
values = value_sets[index] << header.to_s
|
194
|
+
values.map(&:length).max
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Parses a string that has been previously dumped by ExampleStatusDumper.
|
201
|
+
# Note that this parser is a bit naive in that it does a simple split on
|
202
|
+
# "\n" and " | ", with no concern for handling escaping. For now, that's
|
203
|
+
# OK because the values we plan to persist (example id, status, and perhaps
|
204
|
+
# example duration) are highly unlikely to contain "\n" or " | " -- after
|
205
|
+
# all, who puts those in file names?
|
206
|
+
# @private
|
207
|
+
class ExampleStatusParser
|
208
|
+
def self.parse(string)
|
209
|
+
new(string).parse
|
210
|
+
end
|
211
|
+
|
212
|
+
def initialize(string)
|
213
|
+
@header_line, _, *@row_lines = string.lines.to_a
|
214
|
+
end
|
215
|
+
|
216
|
+
def parse
|
217
|
+
@row_lines.map { |line| parse_row(line) }
|
218
|
+
end
|
219
|
+
|
220
|
+
private
|
221
|
+
|
222
|
+
def parse_row(line)
|
223
|
+
Hash[headers.zip(split_line(line))]
|
224
|
+
end
|
225
|
+
|
226
|
+
def headers
|
227
|
+
@headers ||= split_line(@header_line).grep(/\S/).map(&:to_sym)
|
228
|
+
end
|
229
|
+
|
230
|
+
def split_line(line)
|
231
|
+
line.split(/\s+\|\s+?/, -1)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
module RSpec
|
2
|
+
module Core
|
3
|
+
# @private
|
4
|
+
class FilterManager
|
5
|
+
attr_reader :exclusions, :inclusions
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@exclusions, @inclusions = FilterRules.build
|
9
|
+
end
|
10
|
+
|
11
|
+
# @api private
|
12
|
+
#
|
13
|
+
# @param file_path [String]
|
14
|
+
# @param line_numbers [Array]
|
15
|
+
def add_location(file_path, line_numbers)
|
16
|
+
# locations is a hash of expanded paths to arrays of line
|
17
|
+
# numbers to match against. e.g.
|
18
|
+
# { "path/to/file.rb" => [37, 42] }
|
19
|
+
add_path_to_arrays_filter(:locations, File.expand_path(file_path), line_numbers)
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_ids(rerun_path, scoped_ids)
|
23
|
+
# ids is a hash of relative paths to arrays of ids
|
24
|
+
# to match against. e.g.
|
25
|
+
# { "./path/to/file.rb" => ["1:1", "2:4"] }
|
26
|
+
rerun_path = Metadata.relative_path(File.expand_path rerun_path)
|
27
|
+
add_path_to_arrays_filter(:ids, rerun_path, scoped_ids)
|
28
|
+
end
|
29
|
+
|
30
|
+
def empty?
|
31
|
+
inclusions.empty? && exclusions.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
def prune(examples)
|
35
|
+
# Semantically, this is unnecessary (the filtering below will return the empty
|
36
|
+
# array unmodified), but for perf reasons it's worth exiting early here. Users
|
37
|
+
# commonly have top-level examples groups that do not have any direct examples
|
38
|
+
# and instead have nested groups with examples. In that kind of situation,
|
39
|
+
# `examples` will be empty.
|
40
|
+
return examples if examples.empty?
|
41
|
+
|
42
|
+
examples = prune_conditionally_filtered_examples(examples)
|
43
|
+
|
44
|
+
if inclusions.standalone?
|
45
|
+
examples.select { |e| inclusions.include_example?(e) }
|
46
|
+
else
|
47
|
+
locations, ids, non_scoped_inclusions = inclusions.split_file_scoped_rules
|
48
|
+
|
49
|
+
examples.select do |ex|
|
50
|
+
file_scoped_include?(ex.metadata, ids, locations) do
|
51
|
+
!exclusions.include_example?(ex) && non_scoped_inclusions.include_example?(ex)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def exclude(*args)
|
58
|
+
exclusions.add(args.last)
|
59
|
+
end
|
60
|
+
|
61
|
+
def exclude_only(*args)
|
62
|
+
exclusions.use_only(args.last)
|
63
|
+
end
|
64
|
+
|
65
|
+
def exclude_with_low_priority(*args)
|
66
|
+
exclusions.add_with_low_priority(args.last)
|
67
|
+
end
|
68
|
+
|
69
|
+
def include(*args)
|
70
|
+
inclusions.add(args.last)
|
71
|
+
end
|
72
|
+
|
73
|
+
def include_only(*args)
|
74
|
+
inclusions.use_only(args.last)
|
75
|
+
end
|
76
|
+
|
77
|
+
def include_with_low_priority(*args)
|
78
|
+
inclusions.add_with_low_priority(args.last)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def add_path_to_arrays_filter(filter_key, path, values)
|
84
|
+
filter = inclusions.delete(filter_key) || Hash.new { |h, k| h[k] = [] }
|
85
|
+
filter[path].concat(values)
|
86
|
+
inclusions.add(filter_key => filter)
|
87
|
+
end
|
88
|
+
|
89
|
+
def prune_conditionally_filtered_examples(examples)
|
90
|
+
examples.reject do |ex|
|
91
|
+
meta = ex.metadata
|
92
|
+
!meta.fetch(:if, true) || meta[:unless]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# When a user specifies a particular spec location, that takes priority
|
97
|
+
# over any exclusion filters (such as if the spec is tagged with `:slow`
|
98
|
+
# and there is a `:slow => true` exclusion filter), but only for specs
|
99
|
+
# defined in the same file as the location filters. Excluded specs in
|
100
|
+
# other files should still be excluded.
|
101
|
+
def file_scoped_include?(ex_metadata, ids, locations)
|
102
|
+
no_id_filters = ids[ex_metadata[:rerun_file_path]].empty?
|
103
|
+
no_location_filters = locations[
|
104
|
+
File.expand_path(ex_metadata[:rerun_file_path])
|
105
|
+
].empty?
|
106
|
+
|
107
|
+
return yield if no_location_filters && no_id_filters
|
108
|
+
|
109
|
+
MetadataFilter.filter_applies?(:ids, ids, ex_metadata) ||
|
110
|
+
MetadataFilter.filter_applies?(:locations, locations, ex_metadata)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# @private
|
115
|
+
class FilterRules
|
116
|
+
PROC_HEX_NUMBER = /0x[0-9a-f]+@/
|
117
|
+
PROJECT_DIR = File.expand_path('.')
|
118
|
+
|
119
|
+
attr_accessor :opposite
|
120
|
+
attr_reader :rules
|
121
|
+
|
122
|
+
def self.build
|
123
|
+
exclusions = ExclusionRules.new
|
124
|
+
inclusions = InclusionRules.new
|
125
|
+
exclusions.opposite = inclusions
|
126
|
+
inclusions.opposite = exclusions
|
127
|
+
[exclusions, inclusions]
|
128
|
+
end
|
129
|
+
|
130
|
+
def initialize(rules={})
|
131
|
+
@rules = rules
|
132
|
+
end
|
133
|
+
|
134
|
+
def add(updated)
|
135
|
+
@rules.merge!(updated).each_key { |k| opposite.delete(k) }
|
136
|
+
end
|
137
|
+
|
138
|
+
def add_with_low_priority(updated)
|
139
|
+
updated = updated.merge(@rules)
|
140
|
+
opposite.each_pair { |k, v| updated.delete(k) if updated[k] == v }
|
141
|
+
@rules.replace(updated)
|
142
|
+
end
|
143
|
+
|
144
|
+
def use_only(updated)
|
145
|
+
updated.each_key { |k| opposite.delete(k) }
|
146
|
+
@rules.replace(updated)
|
147
|
+
end
|
148
|
+
|
149
|
+
def clear
|
150
|
+
@rules.clear
|
151
|
+
end
|
152
|
+
|
153
|
+
def delete(key)
|
154
|
+
@rules.delete(key)
|
155
|
+
end
|
156
|
+
|
157
|
+
def fetch(*args, &block)
|
158
|
+
@rules.fetch(*args, &block)
|
159
|
+
end
|
160
|
+
|
161
|
+
def [](key)
|
162
|
+
@rules[key]
|
163
|
+
end
|
164
|
+
|
165
|
+
def empty?
|
166
|
+
rules.empty?
|
167
|
+
end
|
168
|
+
|
169
|
+
def each_pair(&block)
|
170
|
+
@rules.each_pair(&block)
|
171
|
+
end
|
172
|
+
|
173
|
+
def description
|
174
|
+
rules.inspect.gsub(PROC_HEX_NUMBER, '').gsub(PROJECT_DIR, '.').gsub(' (lambda)', '')
|
175
|
+
end
|
176
|
+
|
177
|
+
def include_example?(example)
|
178
|
+
MetadataFilter.apply?(:any?, @rules, example.metadata)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# @private
|
183
|
+
ExclusionRules = FilterRules
|
184
|
+
|
185
|
+
# @private
|
186
|
+
class InclusionRules < FilterRules
|
187
|
+
def add(*args)
|
188
|
+
apply_standalone_filter(*args) || super
|
189
|
+
end
|
190
|
+
|
191
|
+
def add_with_low_priority(*args)
|
192
|
+
apply_standalone_filter(*args) || super
|
193
|
+
end
|
194
|
+
|
195
|
+
def include_example?(example)
|
196
|
+
@rules.empty? || super
|
197
|
+
end
|
198
|
+
|
199
|
+
def standalone?
|
200
|
+
is_standalone_filter?(@rules)
|
201
|
+
end
|
202
|
+
|
203
|
+
def split_file_scoped_rules
|
204
|
+
rules_dup = @rules.dup
|
205
|
+
locations = rules_dup.delete(:locations) { Hash.new([]) }
|
206
|
+
ids = rules_dup.delete(:ids) { Hash.new([]) }
|
207
|
+
|
208
|
+
return locations, ids, self.class.new(rules_dup)
|
209
|
+
end
|
210
|
+
|
211
|
+
private
|
212
|
+
|
213
|
+
def apply_standalone_filter(updated)
|
214
|
+
return true if standalone?
|
215
|
+
return nil unless is_standalone_filter?(updated)
|
216
|
+
|
217
|
+
replace_filters(updated)
|
218
|
+
true
|
219
|
+
end
|
220
|
+
|
221
|
+
def replace_filters(new_rules)
|
222
|
+
@rules.replace(new_rules)
|
223
|
+
opposite.clear
|
224
|
+
end
|
225
|
+
|
226
|
+
def is_standalone_filter?(rules)
|
227
|
+
rules.key?(:full_description)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|