geolocal 0.5 → 0.6.1
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 +4 -4
- data/README.md +10 -8
- data/geolocal.gemspec +0 -3
- data/lib/geolocal/configuration.rb +2 -2
- data/lib/geolocal/provider/base.rb +62 -22
- data/lib/geolocal/provider/db_ip.rb +12 -4
- data/lib/geolocal/version.rb +1 -1
- data/spec/configuration_spec.rb +2 -2
- data/spec/provider/base_spec.rb +86 -0
- data/spec/provider/db_ip_spec.rb +33 -12
- metadata +4 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 94b1ea23b169d938aedf7afa726b35f8c59025e7
|
4
|
+
data.tar.gz: 7bde790f3742741c3ea0f94f0640927f93c02b6c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5c3267bab152d3583da3aeedb7d2ba1467c9d3762061392d972db95974a5297a56f03056d96e7274c2897bd0f98dc7174710a7e67570c90eddfd26ef38131569
|
7
|
+
data.tar.gz: 9427d86aa844125c4259c356801a3ad10716651b0b825306af08e3c3eb289d59fd98bc29e530526b72a0302278aa0a425d2e0b37284762fbfba5ff3183ee0974
|
data/README.md
CHANGED
@@ -36,18 +36,21 @@ Geolocal.in_us?(request.remote_ip)
|
|
36
36
|
Geolocal.in_central_america?(IPAddr.new('200.16.66.0'))
|
37
37
|
```
|
38
38
|
|
39
|
+
You can pass:
|
40
|
+
* a string: `Geolocal.in_us?("10.1.2.3")`
|
41
|
+
* an [IPAddr](http://www.ruby-doc.org/stdlib-2.2.0/libdoc/ipaddr/rdoc/IPAddr.html) object:
|
42
|
+
`Geolocal.in_eu?(IPAddr.new('2.16.54.0'))`
|
43
|
+
* an integer/family combo: `Geolocal.in_us?(167838211, Socket::AF_INET)`
|
39
44
|
|
40
45
|
## Config
|
41
46
|
|
42
|
-
Here are the
|
43
|
-
you're using for more.
|
47
|
+
Here are the supported configuration options:
|
44
48
|
|
45
49
|
* **provider**: Where to download the geocoding data. Default: DB_IP.
|
46
50
|
* **module**: The name of the module to receive the `in_*` methods. Default: 'Geolocal'.
|
47
51
|
* **file**: Path to the file to contain the generated code. Default: `lib/#{module}.rb`.
|
48
52
|
* **tmpdir**: the directory to contain intermediate files. They will require tens of megabytes
|
49
53
|
for countries, hundreds for cities). Default: `./tmp/geolocal`
|
50
|
-
* **expires**: the amount of time to consider downloaded data valid. Default: 1.month
|
51
54
|
* **countries**: the ISO-codes of the countries to include in the lookup.
|
52
55
|
* **ipv6**: whether the ranges should support ipv6 addresses.
|
53
56
|
|
@@ -67,8 +70,8 @@ Geolocal.configure do |config|
|
|
67
70
|
end
|
68
71
|
```
|
69
72
|
|
70
|
-
Now you can call `Geolocal.in_eu?(ip)`. If the European Union ever changes,
|
71
|
-
run `bundle update countries` and then `rake geolocal
|
73
|
+
Now you can call `Geolocal.in_eu?(ip)`. If the European Union membership ever changes,
|
74
|
+
run `bundle update countries` and then `rake geolocal` to bring your app up to date.
|
72
75
|
|
73
76
|
## Providers
|
74
77
|
|
@@ -88,12 +91,11 @@ environments like Heroku.
|
|
88
91
|
|
89
92
|
## TODO
|
90
93
|
|
91
|
-
- [ ] performance information? benchmarks. space saving by going ipv4-only?
|
92
94
|
- [ ] include a Rails generator for the config file?
|
93
|
-
- [ ]
|
95
|
+
- [ ] performance information? benchmarks. space saving by going ipv4-only?
|
94
96
|
- [ ] Add support for cities
|
95
|
-
- [ ] replace Nokogiri dependency with a single regex? Shame to force that dependency on all clients.
|
96
97
|
- [ ] other sources for this data? [MainFacts](http://mainfacts.com/ip-address-space-addresses), [NirSoft](http://www.nirsoft.net/countryip/)
|
98
|
+
Also maybe allow providers to accept their own options?
|
97
99
|
- [ ] Add support for for-pay features like lat/lon and timezones?
|
98
100
|
|
99
101
|
|
data/geolocal.gemspec
CHANGED
@@ -17,9 +17,6 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
18
|
spec.require_paths = ["lib"]
|
19
19
|
|
20
|
-
# hm... this is only required when running 'rake download'.
|
21
|
-
spec.add_runtime_dependency "nokogiri"
|
22
|
-
|
23
20
|
spec.add_development_dependency "bundler"
|
24
21
|
spec.add_development_dependency "rake"
|
25
22
|
spec.add_development_dependency "rspec"
|
@@ -1,5 +1,5 @@
|
|
1
1
|
class Configuration
|
2
|
-
OPTIONS = [ :provider, :module, :file, :tmpdir, :
|
2
|
+
OPTIONS = [ :provider, :module, :file, :tmpdir, :ipv4, :ipv6, :quiet, :countries ]
|
3
3
|
attr_accessor(*OPTIONS)
|
4
4
|
|
5
5
|
def initialize
|
@@ -8,7 +8,7 @@ class Configuration
|
|
8
8
|
@module = 'Geolocal'
|
9
9
|
@file = nil # default is computed
|
10
10
|
@tmpdir = 'tmp/geolocal'
|
11
|
-
@
|
11
|
+
@ipv4 = true
|
12
12
|
@ipv6 = true
|
13
13
|
@quiet = false
|
14
14
|
@countries = {}
|
@@ -13,20 +13,39 @@ module Geolocal
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def download
|
16
|
-
# TODO: skip download if local files are new enough
|
17
|
-
# TODO: provide a FORCE argument to force download anyway
|
18
16
|
download_files
|
19
17
|
end
|
20
18
|
|
21
19
|
def update
|
22
|
-
countries = config[:countries].
|
23
|
-
Array(
|
20
|
+
countries = config[:countries].reduce({}) { |a, (k, v)|
|
21
|
+
a.merge! k.to_s.upcase => Array(v).map(&:upcase).to_set
|
24
22
|
}
|
25
23
|
|
26
|
-
|
24
|
+
ipv4 = Socket::AF_INET if config[:ipv4]
|
25
|
+
ipv6 = Socket::AF_INET6 if config[:ipv6]
|
27
26
|
|
28
|
-
|
29
|
-
|
27
|
+
results = countries.keys.reduce({}) { |a, k|
|
28
|
+
a.merge! k.upcase+'v4' => '' if ipv4
|
29
|
+
a.merge! k.upcase+'v6' => '' if ipv6
|
30
|
+
a
|
31
|
+
}
|
32
|
+
|
33
|
+
read_ranges(countries) do |name,lostr,histr|
|
34
|
+
loaddr = IPAddr.new(lostr)
|
35
|
+
hiaddr = IPAddr.new(histr)
|
36
|
+
lofam = loaddr.family
|
37
|
+
hifam = hiaddr.family
|
38
|
+
raise "#{lostr} is family #{lofam} but #{histr} is #{hifam}" if lofam != hifam
|
39
|
+
|
40
|
+
if lofam == ipv4
|
41
|
+
namefam = name+'v4'
|
42
|
+
elsif lofam == ipv6
|
43
|
+
namefam = name+'v6'
|
44
|
+
else
|
45
|
+
raise "unknown family #{lofam} for #{lostr}"
|
46
|
+
end
|
47
|
+
|
48
|
+
results[namefam] << "#{loaddr.to_i}..#{hiaddr.to_i},\n"
|
30
49
|
end
|
31
50
|
|
32
51
|
File.open(config[:file], 'w') do |file|
|
@@ -36,15 +55,24 @@ module Geolocal
|
|
36
55
|
status "done, result in #{config[:file]}\n"
|
37
56
|
end
|
38
57
|
|
58
|
+
def up_to_date?(file, expiry)
|
59
|
+
return false unless File.exist?(file)
|
60
|
+
diff = Time.now - File.mtime(file)
|
61
|
+
if diff < expiry
|
62
|
+
status "using #{file} since it's #{diff.round} seconds old\n"
|
63
|
+
return true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
39
67
|
def output file, results
|
40
|
-
names = results.keys
|
41
68
|
modname = config[:module]
|
42
69
|
|
43
|
-
write_header file,
|
70
|
+
write_header file, modname
|
44
71
|
|
45
|
-
|
46
|
-
|
47
|
-
|
72
|
+
config[:countries].keys.each do |name|
|
73
|
+
v4mod = config[:ipv4] ? name.to_s.upcase + 'v4' : 'nil'
|
74
|
+
v6mod = config[:ipv6] ? name.to_s.upcase + 'v6' : 'nil'
|
75
|
+
write_method file, name, v4mod, v6mod
|
48
76
|
end
|
49
77
|
file.write "end\n\n"
|
50
78
|
|
@@ -57,28 +85,40 @@ module Geolocal
|
|
57
85
|
end
|
58
86
|
|
59
87
|
|
60
|
-
def write_header file,
|
88
|
+
def write_header file, modname
|
61
89
|
file.write <<EOL
|
62
|
-
# This file is autogenerated
|
63
|
-
|
64
|
-
|
65
|
-
|
90
|
+
# This file is autogenerated by the Geolocal gem
|
91
|
+
|
92
|
+
module #{modname}
|
93
|
+
|
94
|
+
def self.search address, family=nil, v4module, v6module
|
95
|
+
address = IPAddr.new(address) if address.is_a?(String)
|
96
|
+
family = address.family unless family
|
97
|
+
num = address.to_i
|
98
|
+
case family
|
99
|
+
when Socket::AF_INET then mod = v4module
|
100
|
+
when Socket::AF_INET6 then mod = v6module
|
101
|
+
else raise "Unknown family \#{family} for address \#{address}"
|
102
|
+
end
|
103
|
+
raise "ipv\#{family == 2 ? 4 : 6} was not compiled in" unless mod
|
104
|
+
true if mod.bsearch { |range| num > range.max ? 1 : num < range.min ? -1 : 0 }
|
105
|
+
end
|
66
106
|
|
67
107
|
EOL
|
68
108
|
end
|
69
109
|
|
70
|
-
def write_method file, name
|
110
|
+
def write_method file, name, v4mod, v6mod
|
71
111
|
file.write <<EOL
|
72
|
-
def self.in_#{name}?
|
73
|
-
|
74
|
-
#{name.upcase}.bsearch { |range| num > range.max ? 1 : num < range.min ? -1 : 0 }
|
112
|
+
def self.in_#{name}? address, family=nil
|
113
|
+
search address, family, #{v4mod}, #{v6mod}
|
75
114
|
end
|
115
|
+
|
76
116
|
EOL
|
77
117
|
end
|
78
118
|
|
79
119
|
def write_ranges file, modname, name, body
|
80
120
|
file.write <<EOL
|
81
|
-
#{modname}::#{name
|
121
|
+
#{modname}::#{name} = [
|
82
122
|
#{body}]
|
83
123
|
|
84
124
|
EOL
|
@@ -3,8 +3,6 @@ require 'net/http'
|
|
3
3
|
require 'fileutils'
|
4
4
|
require 'zlib'
|
5
5
|
|
6
|
-
require 'nokogiri'
|
7
|
-
|
8
6
|
|
9
7
|
class Geolocal::Provider::DB_IP < Geolocal::Provider::Base
|
10
8
|
START_URL = 'https://db-ip.com/db/download/country'
|
@@ -34,9 +32,19 @@ class Geolocal::Provider::DB_IP < Geolocal::Provider::Base
|
|
34
32
|
end
|
35
33
|
|
36
34
|
def download_files
|
35
|
+
# they update the file every month but no idea which day they upload it
|
36
|
+
return if up_to_date?(csv_file, 86400)
|
37
|
+
|
37
38
|
page = Net::HTTP.get(URI START_URL)
|
38
|
-
|
39
|
-
|
39
|
+
|
40
|
+
# if we used Nokogiri: (we don't since we don't want to force the dependency)
|
41
|
+
# doc = Nokogiri::HTML(page)
|
42
|
+
# href = URI doc.css('a.btn-primary').attr('href').to_s
|
43
|
+
|
44
|
+
elem = page.match(/<a\b[^>]*class=['"][^'"]*btn-primary[^>]*>/) or
|
45
|
+
raise "no <a class='btn-primary'> element found in #{START_URL}"
|
46
|
+
attr = elem.to_s.match(/href=['"]([^'"]+)['"]/) or raise "no href found in #{elem}"
|
47
|
+
href = URI attr[1]
|
40
48
|
|
41
49
|
# stream result because it's large
|
42
50
|
FileUtils.mkdir_p(config[:tmpdir])
|
data/lib/geolocal/version.rb
CHANGED
data/spec/configuration_spec.rb
CHANGED
@@ -15,7 +15,7 @@ describe "configuration" do
|
|
15
15
|
expect(defaults.module).to eq 'Geolocal'
|
16
16
|
expect(defaults.file).to eq 'lib/geolocal.rb'
|
17
17
|
expect(defaults.tmpdir).to eq 'tmp/geolocal'
|
18
|
-
expect(defaults.
|
18
|
+
expect(defaults.ipv4).to eq true
|
19
19
|
expect(defaults.ipv6).to eq true
|
20
20
|
expect(defaults.quiet).to eq false
|
21
21
|
expect(defaults.countries).to eq({})
|
@@ -67,7 +67,7 @@ describe "configuration" do
|
|
67
67
|
module: 'Geolocal',
|
68
68
|
file: 'lib/geolocal.rb',
|
69
69
|
tmpdir: 'tmp/geolocal',
|
70
|
-
|
70
|
+
ipv4: true,
|
71
71
|
ipv6: true,
|
72
72
|
quiet: true,
|
73
73
|
countries: {}
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'geolocal/provider/base'
|
3
|
+
|
4
|
+
|
5
|
+
describe Geolocal::Provider::Base do
|
6
|
+
let(:it) { described_class }
|
7
|
+
let(:provider) { it.new }
|
8
|
+
|
9
|
+
let(:example_results) { {
|
10
|
+
'USv4' => "0..16777215,\n34603520..34604031,\n34605568..34606591,\n",
|
11
|
+
'USv6' => "0..42540528726795050063891204319802818559,\n" +
|
12
|
+
"42540569291614257367232052214305390592..42540570559264857595461453711008595967,\n"
|
13
|
+
} }
|
14
|
+
|
15
|
+
before do
|
16
|
+
Geolocal.configure do |config|
|
17
|
+
config.countries = { 'us': 'US' }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'can generate both ipv4 and ipv6' do
|
22
|
+
Geolocal.configure do |config|
|
23
|
+
config.module = 'Geolocal_v4v6'
|
24
|
+
end
|
25
|
+
|
26
|
+
io = StringIO.new
|
27
|
+
provider.output io, example_results
|
28
|
+
expect(io.string).to include '0..42540528726795050063891204319802818559'
|
29
|
+
expect(io.string).to include '34605568..34606591'
|
30
|
+
|
31
|
+
eval io.string
|
32
|
+
expect(Geolocal_v4v6.in_us? '2.16.13.255').to eq true
|
33
|
+
expect(Geolocal_v4v6.in_us? IPAddr.new('2.16.13.255')).to eq true
|
34
|
+
expect(Geolocal_v4v6.in_us? 34606591, Socket::AF_INET).to eq true
|
35
|
+
expect(Geolocal_v4v6.in_us? 34606592, Socket::AF_INET).to eq nil
|
36
|
+
|
37
|
+
expect(Geolocal_v4v6.in_us? '2001:400::').to eq true
|
38
|
+
expect(Geolocal_v4v6.in_us? IPAddr.new('2001:400::')).to eq true
|
39
|
+
expect(Geolocal_v4v6.in_us? 42540570559264857595461453711008595967, Socket::AF_INET6).to eq true
|
40
|
+
expect(Geolocal_v4v6.in_us? 42540570559264857595461453711008595968, Socket::AF_INET6).to eq nil
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'can turn off ipv4' do
|
44
|
+
Geolocal.configure do |config|
|
45
|
+
config.ipv4 = false
|
46
|
+
config.module = 'Geolocal_v6'
|
47
|
+
end
|
48
|
+
|
49
|
+
io = StringIO.new
|
50
|
+
provider.output io, example_results.tap { |h| h.delete('USv4') }
|
51
|
+
expect(io.string).to include '0..42540528726795050063891204319802818559'
|
52
|
+
expect(io.string).not_to include '34605568..34606591'
|
53
|
+
|
54
|
+
eval io.string
|
55
|
+
expect{ Geolocal_v6.in_us? '2.16.13.255' }.to raise_error(/ipv4 was not compiled in/)
|
56
|
+
expect( Geolocal_v6.in_us? '2001:400::' ).to eq true
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'can turn off ipv6' do
|
60
|
+
Geolocal.configure do |config|
|
61
|
+
config.ipv6 = false
|
62
|
+
config.module = 'Geolocal_v4'
|
63
|
+
end
|
64
|
+
|
65
|
+
io = StringIO.new
|
66
|
+
provider.output io, example_results.tap { |h| h.delete('USv6') }
|
67
|
+
expect(io.string).to include '34605568..34606591'
|
68
|
+
expect(io.string).not_to include '0..42540528726795050063891204319802818559'
|
69
|
+
|
70
|
+
eval io.string
|
71
|
+
expect{ Geolocal_v4.in_us? '2001:400::' }.to raise_error(/ipv6 was not compiled in/)
|
72
|
+
expect( Geolocal_v4.in_us? '2.16.13.255' ).to eq true
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'can turn off both ipv4 and ipv6' do
|
76
|
+
Geolocal.configure do |config|
|
77
|
+
config.ipv4 = false
|
78
|
+
config.ipv6 = false
|
79
|
+
end
|
80
|
+
|
81
|
+
io = StringIO.new
|
82
|
+
provider.output io, example_results.tap { |h| h.delete('USv4'); h.delete('USv6') }
|
83
|
+
expect(io.string).not_to include '34605568..34606591'
|
84
|
+
expect(io.string).not_to include '0..42540528726795050063891204319802818559'
|
85
|
+
end
|
86
|
+
end
|
data/spec/provider/db_ip_spec.rb
CHANGED
@@ -36,6 +36,10 @@ describe Geolocal::Provider::DB_IP do
|
|
36
36
|
end
|
37
37
|
|
38
38
|
it 'can download the csv' do
|
39
|
+
Geolocal.configure do |config|
|
40
|
+
config.tmpdir = 'tmp/geolocal-test'
|
41
|
+
end
|
42
|
+
|
39
43
|
# wow!! can't do this in an around hook because it gets the ordering completely wrong.
|
40
44
|
# since around hooks wrap ALL before hooks, they end up using the previous test's config.
|
41
45
|
if File.exist?(provider.csv_file)
|
@@ -53,30 +57,47 @@ describe Geolocal::Provider::DB_IP do
|
|
53
57
|
describe 'generating' do
|
54
58
|
let(:example_output) {
|
55
59
|
<<EOL
|
56
|
-
# This file is autogenerated
|
57
|
-
|
58
|
-
# Defines Geolocal.in_us?, Geolocal.in_au?
|
59
|
-
# and Geolocal::US, Geolocal::AU
|
60
|
+
# This file is autogenerated by the Geolocal gem
|
60
61
|
|
61
62
|
module Geolocal
|
62
|
-
|
63
|
-
|
64
|
-
|
63
|
+
|
64
|
+
def self.search address, family=nil, v4module, v6module
|
65
|
+
address = IPAddr.new(address) if address.is_a?(String)
|
66
|
+
family = address.family unless family
|
67
|
+
num = address.to_i
|
68
|
+
case family
|
69
|
+
when Socket::AF_INET then mod = v4module
|
70
|
+
when Socket::AF_INET6 then mod = v6module
|
71
|
+
else raise "Unknown family \#{family} for address \#{address}"
|
72
|
+
end
|
73
|
+
raise "ipv\#{family == 2 ? 4 : 6} was not compiled in" unless mod
|
74
|
+
true if mod.bsearch { |range| num > range.max ? 1 : num < range.min ? -1 : 0 }
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.in_us? address, family=nil
|
78
|
+
search address, family, USv4, USv6
|
65
79
|
end
|
66
|
-
|
67
|
-
|
68
|
-
|
80
|
+
|
81
|
+
def self.in_au? address, family=nil
|
82
|
+
search address, family, AUv4, AUv6
|
69
83
|
end
|
84
|
+
|
70
85
|
end
|
71
86
|
|
72
|
-
Geolocal::
|
87
|
+
Geolocal::USv4 = [
|
73
88
|
0..16777215,
|
74
89
|
]
|
75
90
|
|
76
|
-
Geolocal::
|
91
|
+
Geolocal::USv6 = [
|
92
|
+
]
|
93
|
+
|
94
|
+
Geolocal::AUv4 = [
|
77
95
|
16777216..16777471,
|
78
96
|
]
|
79
97
|
|
98
|
+
Geolocal::AUv6 = [
|
99
|
+
]
|
100
|
+
|
80
101
|
EOL
|
81
102
|
}
|
82
103
|
|
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: geolocal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 0.6.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Scott Bronson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-02-
|
11
|
+
date: 2015-02-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: nokogiri
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0'
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: bundler
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -107,6 +93,7 @@ files:
|
|
107
93
|
- lib/tasks/geolocal.rake
|
108
94
|
- spec/configuration_spec.rb
|
109
95
|
- spec/data/dbip-country.csv.gz
|
96
|
+
- spec/provider/base_spec.rb
|
110
97
|
- spec/provider/db_ip_spec.rb
|
111
98
|
- spec/spec_helper.rb
|
112
99
|
homepage: http://github.com/bronson/geolocal
|
@@ -136,5 +123,6 @@ summary: Generates plain Ruby if statements to geocode IP addresses
|
|
136
123
|
test_files:
|
137
124
|
- spec/configuration_spec.rb
|
138
125
|
- spec/data/dbip-country.csv.gz
|
126
|
+
- spec/provider/base_spec.rb
|
139
127
|
- spec/provider/db_ip_spec.rb
|
140
128
|
- spec/spec_helper.rb
|