locatine 0.02878 → 0.03050

Sign up to get free protection for your applications and to get access to all the features.
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