locatine 0.02878 → 0.03050

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 457cb5d21eae999673da17f8c5c200181755f846946e81c81d9a23bbafa85528
4
- data.tar.gz: f3ae98ffc268889f4781126b71106ffc99c29c73f9cd4e8321743388fb56bf70
3
+ metadata.gz: 6ec7ffd6c0a45544ae5cbd20581f72190bbfa3ee5dd2c5e444740c1d03d7ba04
4
+ data.tar.gz: 7552e77cfc41106c4679a99554980f67ad4decc354348ee0d01479c9799687fa
5
5
  SHA512:
6
- metadata.gz: ed6f3ad8a5e29ea3b3e672b5a5cbd006571fa9536821dbc0b1467b005cf0998efc19425ba17f166a608cf84b5a884908c1d718275dbbebd0a56372a85f10d988
7
- data.tar.gz: a7e5b86fe05b8710fd5bbd675733937dab017f8c27d8e27e4dbd5865da9313211775877c931a56c36414e0f1a3d94d3c66365a511f543daad4022fb00be91b14
6
+ metadata.gz: 6d42ad78b43ff2335357546b50daa98734e1d06527e26fc6608540135ffb73d4f1aebcf9b5e3fb44c74b3125a31045b3206a8ddabdc0c08211f28f71390fdc9f
7
+ data.tar.gz: 2b752ff4e3a7b60ae8eb12f585eef9df6445f3c659169e7470bcfbc18f848938762815a8a977ce4825be250f527a425b0c2ff1b96d52240db4223e9d1d8a201e
data/README.md CHANGED
@@ -14,7 +14,7 @@ That's it.
14
14
 
15
15
  ## Stage of development:
16
16
 
17
- Version of Locatine is **0.02878**. The 4th version since rewriting. 5-15 next versions is dedicated to bug fixing, tweaking.
17
+ Version of Locatine is **0.03050**. The 4th version since rewriting. 5-15 next versions is dedicated to bug fixing, tweaking.
18
18
 
19
19
  ## Attention
20
20
 
@@ -6,6 +6,8 @@ require 'locatine/results_helpers/find_by_magic'
6
6
  require 'locatine/results_helpers/guess'
7
7
  require 'locatine/results_helpers/info_generator'
8
8
  require 'locatine/results_helpers/xpath_generator'
9
+ require 'locatine/results_helpers/comparing'
10
+ require 'locatine/results_helpers/config'
9
11
 
10
12
  require 'locatine/logger'
11
13
  require 'locatine/version'
@@ -4,7 +4,7 @@ require 'sinatra/base'
4
4
  require 'json'
5
5
 
6
6
  module Locatine
7
- #
7
+ ##
8
8
  # Locatine daemon based on sinatra
9
9
  #
10
10
  # run Locatine::Daemon.run!
@@ -34,12 +34,6 @@ module Locatine
34
34
  { result: result }.to_json
35
35
  end
36
36
 
37
- def send_error(error)
38
- status error.status
39
-
40
- error.answer
41
- end
42
-
43
37
  # selenium calls
44
38
  post '/wd/hub/session/*/element' do
45
39
  content_type settings.headers['Content-Type']
@@ -7,6 +7,8 @@ module Locatine
7
7
  #
8
8
  # Methods that are used by daemon
9
9
  module Methods
10
+ private
11
+
10
12
  def api_request(type, path, query_string, body, new_headers)
11
13
  uri = make_uri(path, query_string)
12
14
  req = Net::HTTP.const_get(type).new(uri,
@@ -80,6 +82,12 @@ module Locatine
80
82
  path_array = request.path_info.split('/')
81
83
  path_array.size >= 7 ? path_array[6] : nil
82
84
  end
85
+
86
+ def send_error(error)
87
+ status error.status
88
+
89
+ error.answer
90
+ end
83
91
  end
84
92
  end
85
93
  end
@@ -1,11 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Locatine
4
- #
4
+ ##
5
5
  # Locatine single element
6
+ #
7
+ # It is used to store element info and to return parent.
8
+ # I am thinking about moving staleness check here.
6
9
  class Element
7
10
  attr_accessor :answer
8
11
 
12
+ ##
13
+ # Init method
14
+ #
15
+ # +session+ is a Locatine::Session instance
16
+ # +element_code+ is an element hash returned by selenium it is shaped like:
17
+ # {"element-6066-11e4-a52e-4f735466cecf"=>"c95a0580-4ac7-4c6d-..."}
9
18
  def initialize(session, element_code)
10
19
  unless element_code
11
20
  raise ArgumentError, 'Cannot init element with no element data'
@@ -15,12 +24,17 @@ module Locatine
15
24
  @answer = element_code
16
25
  end
17
26
 
27
+ ##
28
+ # Returning a parent element
18
29
  def parent
19
30
  parent = File.read("#{HOME}/scripts/parent.js")
20
31
  new_answer = @session.execute_script(parent, self)
21
32
  new_answer.nil? ? nil : Locatine::Element.new(@session, new_answer)
22
33
  end
23
34
 
35
+ ##
36
+ # Method to get the info about particular element or return it if it was
37
+ # gathered before
24
38
  def info
25
39
  return @info if @info
26
40
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Locatine
4
- #
4
+ ##
5
5
  # Simple error
6
6
  class Error
7
7
  attr_accessor :answer, :status
@@ -3,9 +3,13 @@
3
3
  require 'colorize'
4
4
 
5
5
  module Locatine
6
- #
6
+ ##
7
7
  # Methods for sending lines to STDOUT
8
+ #
9
+ # Some of methods are specific and should be run in a correct class only!
8
10
  module Logger
11
+ private
12
+
9
13
  def warn(text)
10
14
  puts "WARNING: #{Time.now}: ".yellow + text
11
15
  end
@@ -20,7 +24,7 @@ module Locatine
20
24
 
21
25
  def warn_locator
22
26
  warn("Locator is broken. For #{name} by"\
23
- " #{@locator['using']}>>>'#{locator['value']}'")
27
+ " #{@locator['using']}>>>'#{@locator['value']}'")
24
28
  end
25
29
 
26
30
  def warn_guess
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Locatine
4
+ ##
5
+ # Locatine results container.
4
6
  #
5
- # Locatine results container
7
+ # Results is a pretty strange concept. It is an array of returned elements
8
+ # which is extended by methods of finding and gathering elements.
6
9
  class Results < Array
7
10
  include Locatine::ResultsHelpers::InfoGenerator
8
11
  include Locatine::ResultsHelpers::XpathGenerator
@@ -10,16 +13,49 @@ module Locatine
10
13
  include Locatine::ResultsHelpers::Common
11
14
  include Locatine::Logger
12
15
  include Locatine::ResultsHelpers::Guess
16
+ include Locatine::ResultsHelpers::Comparing
17
+ include Locatine::ResultsHelpers::Config
18
+
19
+ attr_accessor :name
20
+
21
+ ##
22
+ # Method to find elements
23
+ #
24
+ # +session+ instance of Locatine::Session
25
+ # +locator+ can be a classic locator shaped for webdriver protocol like:
26
+ # {'using => 'xpath', 'value' => '//div'} or
27
+ # {'using' => 'css selector', 'value' => 'div'}
28
+ # It also can be a locator with magic comment like
29
+ # {'using' => 'css selector', 'value' => 'div/*magic comment*/'}
30
+ # It also can be a locator with incapsulated json
31
+ # {'using' => 'css selector', 'value' => 'div/*{"name": "magic comment"}*/'}
32
+ # It can be a locatine locator
33
+ # {'using' => 'locatine', 'value' => '{"name": "magic comment"}'} or
34
+ # {'using' => 'locatine', 'value' => 'magic comment'
35
+ # +parent+ is the parent element to look for the nested ones.
36
+ def find(session, locator, parent)
37
+ configure(session, locator, parent)
38
+ timer
39
+ classic_find
40
+ guess if name_only?
41
+ return self unless empty?
13
42
 
14
- attr_accessor :name, :config, :locator
43
+ find_by_magic if known && tolerance.positive?
44
+ similar? ? found : not_found
45
+ end
15
46
 
16
- def configure(session, locator, parent)
17
- @session = session
18
- @locator = locator.clone
19
- read_locator
20
- @parent = parent
47
+ ##
48
+ # Method to return information about elements found
49
+ #
50
+ # Information is returned combined with the previously known data and can
51
+ # be stored and used as is. It means that its returning not the data about
52
+ # one particular search. But the combined data of all previous searches
53
+ def info
54
+ stability_bump(raw_info)
21
55
  end
22
56
 
57
+ private
58
+
23
59
  def simple_find
24
60
  path = @parent ? "/element/#{@parent}/elements" : '/elements'
25
61
  response = @session.api_request(path, 'Post', @locator.to_json)
@@ -75,16 +111,6 @@ module Locatine
75
111
  find_by_data(base) if empty? && !trusted.empty? && !base['0'].empty?
76
112
  end
77
113
 
78
- def find
79
- timer
80
- classic_find
81
- guess if name_only?
82
- return self unless empty?
83
-
84
- find_by_magic if known && tolerance.positive?
85
- similar? ? found : not_found
86
- end
87
-
88
114
  def found
89
115
  log_found
90
116
  uniq
@@ -94,35 +120,5 @@ module Locatine
94
120
  warn_lost
95
121
  []
96
122
  end
97
-
98
- def read_locator
99
- case @locator['using']
100
- when 'css selector'
101
- # "button/*{json}*/"
102
- read_locator_routine(%r{/\*(.*)\*/$})
103
- when 'xpath'
104
- # "//button['{json}']"
105
- read_locator_routine(/\[\'(.*)\'\]$/)
106
- when 'locatine'
107
- read_locator_routine(/(.*)/)
108
- end
109
- end
110
-
111
- def read_locator_routine(regexp)
112
- matched = @locator['value'].match(regexp)
113
- @config = matched ? config_provided(matched[1]) : {}
114
- @locator['value'] = @locator['value'].gsub(matched[0], '') if matched
115
- @locator = @config['locator'] if @config['locator']
116
- @name = @config['name'] || @locator['value']
117
- end
118
-
119
- def config_provided(config)
120
- JSON.parse(config)
121
- rescue StandardError
122
- result = {}
123
- result['tolerance'] = 0 if config.start_with?('exactly')
124
- result['name'] = config.gsub('exactly ', '')
125
- result
126
- end
127
123
  end
128
124
  end
@@ -2,9 +2,11 @@
2
2
 
3
3
  module Locatine
4
4
  module ResultsHelpers
5
- #
5
+ ##
6
6
  # Some common methods without much logic
7
7
  module Common
8
+ private
9
+
8
10
  def timer
9
11
  @time ||= Time.now
10
12
  timeout > Time.now - @time
@@ -27,15 +29,6 @@ module Locatine
27
29
  (item['name'] == hash['name']) && (item['type'] == hash['type'])
28
30
  end
29
31
 
30
- def info_hash_eq(item, hash)
31
- # Return true
32
- # If type is unknown (but not a text)
33
- # Or when type and name are similar
34
- (unknown_no_text(item, hash) || same_name_type(item, hash)) &&
35
- # And at the same time values are (almost) the same
36
- (item['value'].downcase == hash['value'].downcase)
37
- end
38
-
39
32
  def stability
40
33
  @config['stability'] || @session.stability
41
34
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Locatine
4
+ module ResultsHelpers
5
+ #
6
+ # Trying to guess element here
7
+ module Comparing
8
+ private
9
+
10
+ def count_similarity
11
+ all = 0
12
+ same = 0
13
+ # Next is necessary for unknown reason (smthing thread related)
14
+ raw = raw_info['0']
15
+ get_trusted(known['0']).each do |hash|
16
+ caught = (raw.select { |item| info_hash_eq(item, hash) }).first
17
+ all += 1
18
+ same += 1 if caught
19
+ end
20
+ similar_enough(same, all)
21
+ end
22
+
23
+ def similar?
24
+ return false if empty?
25
+
26
+ return true if tolerance == 100
27
+
28
+ count_similarity
29
+ rescue RuntimeError => e
30
+ raise e.message unless e.message == 'stale element reference'
31
+
32
+ warn_unstable_page
33
+ false
34
+ end
35
+
36
+ def similar_enough(same, all)
37
+ sameness = (same * 100) / all
38
+ sameness >= 100 - tolerance
39
+ end
40
+
41
+ def info_hash_eq(item, hash)
42
+ # Return true
43
+ # If type is unknown (but not a text)
44
+ # Or when type and name are similar
45
+ (unknown_no_text(item, hash) || same_name_type(item, hash)) &&
46
+ # And at the same time values are (almost) the same
47
+ (item['value'].downcase == hash['value'].downcase)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Locatine
4
+ module ResultsHelpers
5
+ #
6
+ # Trying to guess element here
7
+ module Config
8
+ private
9
+
10
+ def configure(session, locator, parent)
11
+ @session = session
12
+ @locator = locator.clone
13
+ read_locator
14
+ @parent = parent
15
+ end
16
+
17
+ def read_locator
18
+ case @locator['using']
19
+ when 'css selector'
20
+ # "button/*{json}*/"
21
+ read_locator_routine(%r{/\*(.*)\*/$})
22
+ when 'xpath'
23
+ # "//button['{json}']"
24
+ read_locator_routine(/\[\'(.*)\'\]$/)
25
+ when 'locatine'
26
+ read_locator_routine(/(.*)/)
27
+ end
28
+ end
29
+
30
+ def read_locator_routine(regexp)
31
+ matched = @locator['value'].match(regexp)
32
+ @config = matched ? config_provided(matched[1]) : {}
33
+ @locator['value'] = @locator['value'].gsub(matched[0], '') if matched
34
+ @locator = @config['locator'] if @config['locator']
35
+ @name = @config['name'] || @locator['value']
36
+ end
37
+
38
+ def config_provided(config)
39
+ JSON.parse(config)
40
+ rescue StandardError
41
+ result = {}
42
+ result['tolerance'] = 0 if config.start_with?('exactly')
43
+ result['name'] = config.gsub('exactly ', '')
44
+ result
45
+ end
46
+ end
47
+ end
48
+ end
@@ -2,9 +2,11 @@
2
2
 
3
3
  module Locatine
4
4
  module ResultsHelpers
5
- #
5
+ ##
6
6
  # Methods for active looking for element
7
7
  module FindByMagic
8
+ private
9
+
8
10
  def magic_thread
9
11
  Thread.new do
10
12
  while empty?
@@ -2,9 +2,11 @@
2
2
 
3
3
  module Locatine
4
4
  module ResultsHelpers
5
- #
5
+ ##
6
6
  # Trying to guess element here
7
7
  module Guess
8
+ private
9
+
8
10
  def guess
9
11
  warn_guess
10
12
  @config['tolerance'] = 100
@@ -40,37 +42,6 @@ module Locatine
40
42
  end
41
43
  answer
42
44
  end
43
-
44
- def count_similarity
45
- all = 0
46
- same = 0
47
- # Next is necessary for unknown reason (smthing thread related)
48
- raw = raw_info['0']
49
- get_trusted(known['0']).each do |hash|
50
- caught = (raw.select { |item| info_hash_eq(item, hash) }).first
51
- all += 1
52
- same += 1 if caught
53
- end
54
- similar_enough(same, all)
55
- end
56
-
57
- def similar?
58
- return false if empty?
59
-
60
- return true if tolerance == 100
61
-
62
- count_similarity
63
- rescue RuntimeError => e
64
- raise e.message unless e.message == 'stale element reference'
65
-
66
- warn_unstable_page
67
- false
68
- end
69
-
70
- def similar_enough(same, all)
71
- sameness = (same * 100) / all
72
- sameness >= 100 - tolerance
73
- end
74
45
  end
75
46
  end
76
47
  end
@@ -2,12 +2,10 @@
2
2
 
3
3
  module Locatine
4
4
  module ResultsHelpers
5
- #
5
+ ##
6
6
  # Methods for gathering element info
7
7
  module InfoGenerator
8
- def info
9
- stability_bump(raw_info)
10
- end
8
+ private
11
9
 
12
10
  def raw_info
13
11
  result = {}
@@ -5,6 +5,8 @@ module Locatine
5
5
  ##
6
6
  # Methods for generation xpath from stored data
7
7
  module XpathGenerator
8
+ private
9
+
8
10
  def find_by_data(base = known)
9
11
  xpath = generate_xpath(base)
10
12
  @locator = { 'using' => 'xpath', 'value' => xpath }
@@ -1,18 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Locatine
4
+ ##
5
+ # Locatine Session class.
4
6
  #
5
- # Locatine session operator finds and returns
7
+ # Each selenium session gets a locatine session. Locatine session knows
8
+ # selenium address and session_id so it is able to communicate with selenium.
9
+ # It has no complex logic, but acts like a bridge between daemon and selenium
10
+ # while element is searched
6
11
  class Session
7
12
  include Locatine::Logger
8
13
  attr_accessor :json, :depth, :trusted, :untrusted, :tolerance, :stability,
9
14
  :elements, :timeout
10
15
 
11
- def defaults
12
- { json: "#{Dir.pwd}/locatine_files/default.json", depth: 3, trusted: [],
13
- untrusted: [], tolerance: 50, stability: 10, timeout: 25 }
14
- end
15
-
16
+ ##
17
+ # Init of the new session instance
18
+ #
19
+ # +selenium+ is a selenium address like "https://host:port"
20
+ # +session+ is a session id provided by selenium
16
21
  def initialize(selenium, session)
17
22
  @selenium = selenium
18
23
  @parsed_selenium = URI.parse @selenium
@@ -23,6 +28,10 @@ module Locatine
23
28
  configure defaults
24
29
  end
25
30
 
31
+ ##
32
+ # This method is to set settings
33
+ #
34
+ # +params+ is a hash of settings like {json: "some", depth: 0...}
26
35
  def configure(params)
27
36
  params.to_h.each_pair do |var, value|
28
37
  instance_variable_set("@#{var}", value)
@@ -31,23 +40,14 @@ module Locatine
31
40
  params
32
41
  end
33
42
 
34
- def read
35
- dir = File.dirname(@json)
36
- FileUtils.mkdir_p(dir) unless File.directory?(dir)
37
- unless File.exist?(@json)
38
- File.open(@json, 'w') do |f|
39
- f.write('{"elements" : {}}')
40
- end
41
- end
42
- @elements = JSON.parse(File.read(@json))['elements']
43
- end
44
-
45
- def write
46
- File.open(@json, 'w') do |f|
47
- f.write(JSON.pretty_generate('elements' => @elements))
48
- end
49
- end
50
-
43
+ ##
44
+ # Find method is for finding elements.
45
+ #
46
+ # That is the part that is replacing simple finding by selenium
47
+ # +params+ is a hash of settings like {json: "some", depth: 0...}
48
+ # +parent+ is an element code of the element to look under. It is counted
49
+ # only for the most simple search. If element is lost parent will be
50
+ # ignored
51
51
  def find(params, parent = nil)
52
52
  find_routine(params, parent)
53
53
  rescue RuntimeError => e
@@ -57,17 +57,13 @@ module Locatine
57
57
  find(params, parent)
58
58
  end
59
59
 
60
- def find_routine(params, parent)
61
- results = Results.new
62
- results.configure(self, params, parent)
63
- answer = results.find
64
- if !answer.empty? && answer.first.class != Locatine::Error
65
- @elements[results.name] = results.info
66
- write
67
- end
68
- answer
69
- end
70
-
60
+ ##
61
+ # Session can execute js scripts on a page
62
+ #
63
+ # Note this method will be not called when you are asking selenoum via
64
+ # locatine to execute a script. This class is for internal use.
65
+ # +script+ some valid js code
66
+ # +*args+ arguments to be passed to script.
71
67
  def execute_script(script, *args)
72
68
  args.map! { |item| item.class == Locatine::Element ? item.answer : item }
73
69
  response = api_request('/execute/sync', 'Post',
@@ -79,6 +75,8 @@ module Locatine
79
75
  value
80
76
  end
81
77
 
78
+ ##
79
+ # Returning information about the current page
82
80
  def page
83
81
  # We need duplicated JSON parse since standart
84
82
  # chromedriver giving an error here if the page is too large
@@ -86,14 +84,14 @@ module Locatine
86
84
  JSON.parse(page, max_nesting: false)['result']
87
85
  end
88
86
 
89
- def call_uri(path)
90
- URI::HTTP.build(
91
- host: @parsed_selenium.host,
92
- port: @parsed_selenium.port,
93
- path: "/wd/hub/session/#{@session}#{path}"
94
- )
95
- end
96
-
87
+ ##
88
+ # This method is used to ask selenium about something.
89
+ #
90
+ # We are using it to ask for elements found by selenium or
91
+ # for script execution
92
+ # +path+ is a relative path to call on selenium like '/elements'
93
+ # +method+ is an http method to perform ('Get', 'Post', etc.)
94
+ # +body+ is for request data. json here (selenium wants it) or nil
97
95
  def api_request(path, method, body)
98
96
  uri = call_uri(path)
99
97
  req = Net::HTTP.const_get(method)
@@ -103,5 +101,47 @@ module Locatine
103
101
  req.body = body
104
102
  Net::HTTP.new(uri.hostname, uri.port).start { |http| http.request(req) }
105
103
  end
104
+
105
+ private
106
+
107
+ def defaults
108
+ { json: "#{Dir.pwd}/locatine_files/default.json", depth: 3, trusted: [],
109
+ untrusted: [], tolerance: 50, stability: 10, timeout: 25 }
110
+ end
111
+
112
+ def read
113
+ dir = File.dirname(@json)
114
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
115
+ unless File.exist?(@json)
116
+ File.open(@json, 'w') do |f|
117
+ f.write('{"elements" : {}}')
118
+ end
119
+ end
120
+ @elements = JSON.parse(File.read(@json))['elements']
121
+ end
122
+
123
+ def write
124
+ File.open(@json, 'w') do |f|
125
+ f.write(JSON.pretty_generate('elements' => @elements))
126
+ end
127
+ end
128
+
129
+ def find_routine(params, parent)
130
+ results = Results.new
131
+ answer = results.find(self, params, parent)
132
+ if !answer.empty? && answer.first.class != Locatine::Error
133
+ @elements[results.name] = results.info
134
+ write
135
+ end
136
+ answer
137
+ end
138
+
139
+ def call_uri(path)
140
+ URI::HTTP.build(
141
+ host: @parsed_selenium.host,
142
+ port: @parsed_selenium.port,
143
+ path: "/wd/hub/session/#{@session}#{path}"
144
+ )
145
+ end
106
146
  end
107
147
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Locatine
4
4
  # constants here...
5
- VERSION = '0.02878'
5
+ VERSION = '0.03050'
6
6
  NAME = 'locatine'
7
7
  HOME = if File.readable?("#{Dir.pwd}/lib/#{Locatine::NAME}")
8
8
  "#{Dir.pwd}/lib/#{Locatine::NAME}"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: locatine
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.02878'
4
+ version: '0.03050'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergei Seleznev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-08 00:00:00.000000000 Z
11
+ date: 2020-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -145,6 +145,8 @@ files:
145
145
  - lib/locatine/logger.rb
146
146
  - lib/locatine/results.rb
147
147
  - lib/locatine/results_helpers/common.rb
148
+ - lib/locatine/results_helpers/comparing.rb
149
+ - lib/locatine/results_helpers/config.rb
148
150
  - lib/locatine/results_helpers/find_by_magic.rb
149
151
  - lib/locatine/results_helpers/guess.rb
150
152
  - lib/locatine/results_helpers/info_generator.rb