phantom-manager 0.0.7 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- YjQ1YmY5NmYzMDgwMjAxN2MzMGU3OTcyMDZkM2NiNmQzNjNlMzRiZA==
4
+ MjUzNTIwOGY1MmNjYjRlNTAxYmZjZjJhNDllMjYxNjVkN2JiOGZmZg==
5
5
  data.tar.gz: !binary |-
6
- NzNiNWE1MTgxYTI1N2RlMDJhZjc1YmE2YWM5ZWQ2M2RmNGRjYjg2NA==
6
+ MzRjOWYwZTBmN2JmMWY4NTc0M2UxY2UwMGQwYmJmOWQyZmRjNmQwOA==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- NzA2YzFlMTM1NGUxZGVlMDdiNmEwYjMxYjVkNWE0MzdiZTAyMzMyMmI2NGI2
10
- ODNlMzljM2IxN2RhMzBmMGY1NzNjMmViMTVlZDZlMWMzZmU4ZDUyOTA0OTA0
11
- N2MwMjFkNDE0ZjhlYjdmYWMzN2M3MDkxYjVjMTdiMjQyMDllMzM=
9
+ MjEzMTA0NjNkOTIxMGMxY2YzM2E1NDE0OWI4YzZkYzRlYzY2ZGYxYTYzMmIz
10
+ MWVlNjIxOTA4MzZhNWQ3ZGUwMGM5MDMxYzg3NDQ5MThiMDk5YWViMWEwY2M4
11
+ NWFlMmQ2YTFjNWQ2MDlhNTQzYzUxYTBiMTJlZDM3ODQ2YzcwNmI=
12
12
  data.tar.gz: !binary |-
13
- ODg4OGQ5YzNlZmJhNjMxZTA0NmQxNGNkM2E2MTUzYjhjZWI4NDVlMmFkYmYz
14
- MjliOWM3YWI2MjA3MTI1ZTg3OTlkMDdiNzVjYWIyYWFlOTVmODQ4NzJjNjFl
15
- Y2E1MGJhNmQ1NDAzYjAyNWQ0NWVmNDYyZDVjYTJhYWMxZDdjZmI=
13
+ MzEzNDM5NGE4YjZiNzJjYmQxY2IyN2FmNTAxYTkwZWNjNTc4MzdhYWY0ZWM0
14
+ MWQ0ZjhkMjZiYmJiNTJhYTUyZjJjOGYxY2FlNWY1NjU0OWVkNzhiNzQ0MTA5
15
+ ZWNkNjA1ODZkMjBmZjUzMmI3M2U2NDhkZmI5NGVkNjMzY2NiNDk=
data/Gemfile CHANGED
@@ -5,3 +5,4 @@ gemspec
5
5
 
6
6
  gem 'rspec'
7
7
  gem 'debugger'
8
+
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- phantom-manager (0.0.5)
4
+ phantom-manager (0.0.7)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,10 +1,25 @@
1
1
  # Phantom::Manager
2
2
 
3
+ The architecture behind phantom-manager is:
4
+
5
+ ![Phantom-Architecture](http://i39.tinypic.com/2gxnz3d.png)
6
+
3
7
  phantom-manager allows you to use multiple phantom-js processes behind an Nginx
4
8
  server. It will manage both presence and memory consumption of those processes
5
9
  and kill them when appropriate, all this in sync with the Nginx configuration
6
10
  so that all requests will get answered.
7
11
 
12
+ If you've got a singlepage application and you want to:
13
+ * Render full page for GoogleBot or other web crawlers.
14
+ * Render full page to be cached by your CDN.
15
+
16
+ While:
17
+
18
+ * Keeping your phantom-js processes running.
19
+ * Preventing your phantom-js processes from memory-bloat.
20
+
21
+ This is a good way to achieve it.
22
+
8
23
  ## Installation
9
24
 
10
25
  Add this line to your application's Gemfile:
@@ -21,7 +36,88 @@ Or install it yourself as:
21
36
 
22
37
  ## Usage
23
38
 
24
- TODO: Write usage instructions here
39
+ 1. You will need Nginx which will load balance requests between the phantom-js
40
+ processes.
41
+ Its conf must include a "upstream phantomjs" directive with the corresponding
42
+ settings. For example:
43
+
44
+ ```
45
+ upstream phantomjs {
46
+ server 127.0.0.1:8002;
47
+ server 127.0.0.1:8003;
48
+ server 127.0.0.1:8004;
49
+ server 127.0.0.1:8005;
50
+ }
51
+ ```
52
+
53
+ 2. A customized [rndr.me js](https://github.com/jed/rndr.me) file that will fit your configuration. There is an
54
+ example rndrme.js [here](lib/utils/rndrme.js).
55
+
56
+ The host configuration is where phantom-js requests the page from, so be
57
+ sure to point it to your backend server.
58
+ Also, set the readyEvent to be the event you'r raising so that phantom-js
59
+ identify it should start rendering.
60
+
61
+ 3. Create a config.yml file to set the variables for phantom-manager. There's
62
+ an [example config](config/config.yml) with a documentation of each attribute and its meaning.
63
+
64
+ 4. You're ready to run phantom-manager:
65
+ Just run `phantom_monitor` from anywhere in your system to get the usage
66
+ instructions for the command line tool.
67
+
68
+ Usually, you would just `phantom_monitor -c YOUR_CONF_FILE -e YOUR_ENV`
69
+ The env option is there to allow your config.yml to have multiple
70
+ environments settings.
71
+
72
+ 5. The phantom_monitor process listens for USR2 signals. Once such a signal is
73
+ sent it will restart all processes one by one.
74
+
75
+ ## How Does It Work?
76
+
77
+ Phantom manager will check for both presence and memory consumption of your
78
+ phantom-js processes under the configuration you have defined.
79
+
80
+ ### Presence
81
+
82
+ Assuming configuration:
83
+
84
+ ```
85
+ phantom_base_port: 8002
86
+ phantom_processes_number: 3
87
+ ```
88
+ The monitor will keep phantom-js processes up on ports 8002, 8003, 8004
89
+ If a phantom-js crashes the monitor will bring it back up.
90
+
91
+ ### Memory Consumption
92
+
93
+ Assuming configuration:
94
+ ```
95
+ memory_limit: 100_000
96
+ memory_retries: 3
97
+ memory_check_interval: 5
98
+ ```
99
+ The monitor sample all phantom-js processes each 5 seconds and restart those
100
+ which their memory exceeded 100MB for 3 straight samples.
101
+
102
+ ### Restarting Processes
103
+
104
+ To restart a phantom-js process the monitor performs the following actions:
105
+
106
+ 1. Remove the process from nginx upstream config.
107
+
108
+ 2. Reload Nginx.
109
+
110
+ 3. Sleeps for phantom_termination_grace seconds to allow this phantom to
111
+ respond to all requests in its request queue.
112
+
113
+ 4. Kill the phantom-js process.
114
+
115
+ 5. Start the phantom-js process on the same port.
116
+
117
+ 6. Add it to the Nginx upstream configuration.
118
+
119
+ 7. Reload Nginx.
120
+
25
121
 
26
122
  ## Contributing
27
123
 
data/bin/phantom_monitor CHANGED
@@ -42,9 +42,10 @@ lib = File.expand_path('../../lib', __FILE__)
42
42
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
43
43
 
44
44
  require 'phantom/manager/version'
45
- require 'monitors/memory.rb'
46
- require 'monitors/processes.rb'
47
- require 'monitors/restart_listener.rb'
45
+ require 'monitors/memory'
46
+ require 'monitors/processes'
47
+ require 'monitors/response_time'
48
+ require 'monitors/restart_listener'
48
49
 
49
50
 
50
51
  $logger.info "Starting for environment #{$options[:env]} with config file #{$options[:config]}... "
@@ -57,5 +58,8 @@ Thread.new {Monitors::Memory.run}
57
58
 
58
59
  $logger.info "Running processes monitor for phantom processes"
59
60
 
60
- Thread.new {Monitors::Processes.run}.join
61
+ Thread.new {Monitors::Processes.run}
61
62
 
63
+ $logger.info "Running processes monitor for response time"
64
+
65
+ Thread.new {Monitors::ResponseTime.run}.join
data/config/config.yml CHANGED
@@ -28,3 +28,11 @@ development:
28
28
 
29
29
  #Command to issue when launching phantomjs
30
30
  phantom_command: 'phantomjs rndrme.js'
31
+
32
+
33
+ #Response Time Monitor
34
+ #
35
+ # If a phantomjs process excceeds this time in response it will be killed
36
+ response_time_threshold: 2
37
+ response_time_check_retries: 2
38
+ response_time_check_interval: 40
data/lib/monitors/base.rb CHANGED
@@ -5,12 +5,14 @@ require 'phantom/collector'
5
5
 
6
6
  module Monitors
7
7
  class Base
8
+ extend Logging
8
9
 
9
10
  class << self
10
11
  def run(custom_logger = $logger)
11
12
  @logger = custom_logger
12
13
 
13
14
  loop do
15
+ log "Performing Check..."
14
16
  perform_check
15
17
  sleep check_interval
16
18
  end
@@ -30,9 +32,6 @@ module Monitors
30
32
  Phantom::Collector.get_running_instances
31
33
  end
32
34
 
33
- def logger
34
- @logger ||= $logger
35
- end
36
35
  end
37
36
 
38
37
  end
@@ -7,11 +7,9 @@ module Monitors
7
7
  class << self
8
8
 
9
9
  def perform_check
10
- logger.info "Perfoming memory check..."
11
-
12
10
  running_instances.each do |p|
13
11
  if ViolationsRecorders::Memory.is_violating?(p)
14
- logger.info "process #{p.pid} was found bad!"
12
+ log "process #{p.pid} had a memory violation"
15
13
  Phantom::Manager.restart(p)
16
14
  end
17
15
  end
@@ -6,8 +6,6 @@ module Monitors
6
6
 
7
7
  class << self
8
8
  def perform_check
9
- logger.info "Perfoming processes check..."
10
-
11
9
  missing_processes = Phantom::Collector.missing_ports.map do |port|
12
10
  p = Phantom::Process.new
13
11
  p.port = port
@@ -16,12 +14,12 @@ module Monitors
16
14
 
17
15
  missing_processes.each do |p|
18
16
  if ViolationsRecorders::Processes.is_violating?(p)
19
- logger.info "found missing phantom on port #{p.port}"
17
+ log "found missing phantom on port #{p.port}"
20
18
  Phantom::Manager.start(p)
21
19
  end
22
20
  end
23
21
 
24
- logger.info "All processes are running OK" if missing_processes.empty?
22
+ log "All processes are running OK" if missing_processes.empty?
25
23
  end
26
24
 
27
25
  def check_interval
@@ -0,0 +1,27 @@
1
+ require 'monitors/base'
2
+ require 'monitors/violations_recorders/response_time'
3
+
4
+ module Monitors
5
+ class ResponseTime < Base
6
+
7
+ class << self
8
+ def perform_check
9
+ all_processes_ok = true
10
+ running_instances.each do |p|
11
+ if ViolationsRecorders::ResponseTime.is_violating?(p)
12
+ all_processes_ok = false
13
+ log "process #{p} had a response time violation"
14
+ Phantom::Manager.restart(p)
15
+ end
16
+ end
17
+
18
+ log "All response time are ok" if all_processes_ok
19
+ end
20
+
21
+ def check_interval
22
+ Cfg.response_time_check_interval
23
+ end
24
+ end
25
+
26
+ end
27
+ end
@@ -5,6 +5,7 @@ require 'phantom/process'
5
5
  module Monitors
6
6
  module ViolationsRecorders
7
7
  class Base
8
+ extend Logging
8
9
 
9
10
  class << self
10
11
 
@@ -19,9 +20,9 @@ module Monitors
19
20
  end
20
21
 
21
22
  def is_violating?(process)
22
- $logger.debug "checking #{process}"
23
+ log "checking #{process}", :debug
23
24
  update_violations_count(process)
24
- $logger.debug "#{@violations}"
25
+ log "#{@violations}", :debug
25
26
  violating = @violations[process.send(process_attr)] == retries_limit
26
27
  @violations[process.send(process_attr)] = 0 if violating
27
28
  violating
@@ -0,0 +1,41 @@
1
+ require 'monitors/violations_recorders/base'
2
+ require 'timeout'
3
+
4
+ module Monitors
5
+ module ViolationsRecorders
6
+ class ResponseTime < Base
7
+ class << self
8
+ def retries_limit
9
+ Cfg.response_time_check_retries
10
+ end
11
+
12
+ def process_attr
13
+ :pid
14
+ end
15
+
16
+ def process_is_violating?(process)
17
+ time = Cfg.response_time_threshold
18
+ begin
19
+ Timeout.timeout(Cfg.response_time_threshold) do
20
+ time = check_response_time(process)
21
+ end
22
+ rescue Timeout::Error
23
+ return true
24
+ end
25
+ time > Cfg.response_time_threshold
26
+ end
27
+
28
+ private
29
+
30
+ def check_response_time(process)
31
+ res = `curl -o /dev/null -s -w %{time_total}@%{http_code} #{process_url(process)}`
32
+ res.split("@").first.to_f
33
+ end
34
+
35
+ def process_url(process)
36
+ "http://localhost:#{process.port}"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,5 @@
1
1
  module Phantom
2
2
  module Manager
3
- VERSION = "0.0.7"
3
+ VERSION = "0.0.8"
4
4
  end
5
5
  end
data/lib/utils/logger.rb CHANGED
@@ -1,3 +1,13 @@
1
1
  require 'logger'
2
2
  $logger = Logger.new(STDOUT)
3
3
  $logger.level = Logger::INFO
4
+
5
+ module Logging
6
+ def logger
7
+ @logger ||= $logger
8
+ end
9
+
10
+ def log(str, severity = :info)
11
+ logger.send(severity,"[#{self.name}] #{str}")
12
+ end
13
+ end
@@ -0,0 +1,111 @@
1
+ var config = {
2
+ host: "http://localhost:8001",
3
+ maxTime: 30000,
4
+ maxBytes: 0x100000,
5
+ readyEvent: "renderReady",
6
+ loadImages: false
7
+ }
8
+
9
+
10
+ var system = require("system")
11
+ var webserver = require("webserver")
12
+ var webpage = require("webpage")
13
+
14
+ var port = system.args[1];
15
+
16
+ if (!port) {
17
+ console.error("No port specified in " + configPath)
18
+ phantom.exit(1)
19
+ }
20
+
21
+ var server = webserver.create()
22
+ var listening = server.listen(port, onRequest)
23
+
24
+ if (!listening) {
25
+ console.error("Could not bind to port " + port)
26
+ phantom.exit(1)
27
+ }
28
+
29
+ function onRequest(req, res) {
30
+ var page = webpage.create()
31
+ var bytesConsumed = 0
32
+
33
+ if (req.method != "GET") {
34
+ return send(405, toHTML("Method not accepted."))
35
+ }
36
+
37
+ var url = parse(req.url)
38
+
39
+ var query = url.query
40
+ var href = decodeURIComponent(config.host + req.url)
41
+
42
+ if (!href) {
43
+ return send(400, toHTML("`href` parameter is missing."))
44
+ }
45
+
46
+ var maxTime = Number(query.max_time) || config.maxTime
47
+ var maxBytes = Number(query.max_bytes) || config.maxBytes
48
+ var readyEvent = query.ready_event || config.readyEvent
49
+ var loadImages = "load_images" in query || config.loadImages
50
+
51
+ page.settings.loadImages = loadImages
52
+
53
+ page.onInitialized = function() {
54
+ page.evaluate(onInit, readyEvent)
55
+
56
+ function onInit(readyEvent) {
57
+ window.rndrme = true;
58
+ window.addEventListener(readyEvent, function() {
59
+ setTimeout(window.callPhantom, 0)
60
+ })
61
+ }
62
+ }
63
+
64
+ page.onCallback = function() {
65
+ send(200, page.content)
66
+ }
67
+
68
+ var timeout = setTimeout(page.onCallback, maxTime)
69
+
70
+ console.log("(" + port + ") opening page " + href);
71
+
72
+ page.open(href)
73
+
74
+ function send(statusCode, data) {
75
+ clearTimeout(timeout)
76
+
77
+ res.statusCode = statusCode
78
+
79
+ res.setHeader("Content-Type", "text/html")
80
+ res.setHeader("Content-Length", byteLength(data))
81
+ res.setHeader("X-Rndrme-Bytes-Consumed", bytesConsumed.toString())
82
+
83
+ res.write(data)
84
+ res.close()
85
+
86
+ page.close()
87
+ }
88
+ }
89
+
90
+ function byteLength(str) {
91
+ return encodeURIComponent(str).match(/%..|./g).length
92
+ }
93
+
94
+ function toHTML(message) {
95
+ return "<!DOCTYPE html><body>" + message + "</body>\n"
96
+ }
97
+
98
+ function parse(url) {
99
+ var anchor = document.createElement("a")
100
+
101
+ anchor.href = url
102
+ anchor.query = {}
103
+
104
+ anchor.search.slice(1).split("&").forEach(function(pair) {
105
+ pair = pair.split("=").map(decodeURIComponent)
106
+ anchor.query[pair[0]] = pair[1]
107
+ })
108
+
109
+ return anchor
110
+ }
111
+
@@ -3,7 +3,10 @@ upstream unicorn {
3
3
  }
4
4
 
5
5
  upstream phantomjs {
6
- server 127.0.0.1:8020 fail_timeout=0; # 2013-08-01 16:06:02 +0300
6
+ server 127.0.0.1:8000 fail_timeout=0; # 2013-10-21 10:44:21 +0200
7
+ server 127.0.0.1:8000 fail_timeout=0; # 2013-10-21 10:44:21 +0200
8
+ server 127.0.0.1:8012 fail_timeout=0; # 2013-10-21 10:44:21 +0200
9
+ server 127.0.0.1:8013 fail_timeout=0; # 2013-10-21 10:44:21 +0200
7
10
  server 127.0.0.1:8002;
8
11
  server 127.0.0.1:8003;
9
12
  server 127.0.0.1:8004;
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+ require 'monitors/response_time'
3
+
4
+ module Monitors
5
+
6
+ describe ResponseTime do
7
+
8
+ subject {ResponseTime}
9
+
10
+ before do
11
+ @process1 = Phantom::Process.new(1, 1, "", 1)
12
+ @process2 = Phantom::Process.new(3, 3, "", 3)
13
+ subject.stub running_instances: [@process1, @process2]
14
+ Cfg.response_time_threshold = 2
15
+ Cfg.response_time_check_retries = 2
16
+ end
17
+
18
+ describe :perform_check do
19
+ context "violating process" do
20
+ before do
21
+ ViolationsRecorders::ResponseTime.stub(:is_violating?).with(@process1).and_return(true)
22
+ ViolationsRecorders::ResponseTime.stub(:is_violating?).with(@process2).and_return(false)
23
+ end
24
+
25
+ it "should restart violating process" do
26
+ Phantom::Manager.should_receive(:restart).with(@process1).once
27
+ subject.perform_check
28
+ end
29
+
30
+ it "should not restart non violating process" do
31
+ Phantom::Manager.should_not_receive(:restart).with(@process2)
32
+ subject.perform_check
33
+ end
34
+ end
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+ require 'monitors/violations_recorders/response_time'
3
+
4
+ module Monitors
5
+ module ViolationsRecorders
6
+
7
+ RESPONSE_TIME_THRESHOLD = 2
8
+
9
+ describe ResponseTime do
10
+
11
+ subject {ResponseTime}
12
+
13
+ describe :process_is_violating? do
14
+
15
+ before do
16
+ Cfg.stub(:response_time_threshold).and_return(RESPONSE_TIME_THRESHOLD)
17
+ end
18
+
19
+ context "timeout" do
20
+ before do
21
+ subject.stub(:check_response_time).and_raise(Timeout::Error)
22
+ end
23
+
24
+ it "should return true" do
25
+ subject.process_is_violating?(stub).should be_true
26
+ end
27
+ end
28
+
29
+ context "fast response time" do
30
+ before do
31
+ subject.stub(:check_response_time).and_return(RESPONSE_TIME_THRESHOLD - 1)
32
+ end
33
+
34
+ it "should return false" do
35
+ subject.process_is_violating?(stub).should be_false
36
+ end
37
+ end
38
+
39
+ context "slow response time" do
40
+ before do
41
+ subject.stub(:check_response_time).and_return(RESPONSE_TIME_THRESHOLD + 1)
42
+ end
43
+
44
+ it "should return true" do
45
+ subject.process_is_violating?(stub).should be_true
46
+ end
47
+ end
48
+ end
49
+
50
+ end
51
+ end
52
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phantom-manager
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erez Rabih
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-08-01 00:00:00.000000000 Z
11
+ date: 2013-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -73,10 +73,12 @@ files:
73
73
  - lib/monitors/base.rb
74
74
  - lib/monitors/memory.rb
75
75
  - lib/monitors/processes.rb
76
+ - lib/monitors/response_time.rb
76
77
  - lib/monitors/restart_listener.rb
77
78
  - lib/monitors/violations_recorders/base.rb
78
79
  - lib/monitors/violations_recorders/memory.rb
79
80
  - lib/monitors/violations_recorders/processes.rb
81
+ - lib/monitors/violations_recorders/response_time.rb
80
82
  - lib/nginx/manager.rb
81
83
  - lib/phantom/.DS_Store
82
84
  - lib/phantom/collector.rb
@@ -87,6 +89,7 @@ files:
87
89
  - lib/utils/limited_array.rb
88
90
  - lib/utils/lock.rb
89
91
  - lib/utils/logger.rb
92
+ - lib/utils/rndrme.js
90
93
  - lib/utils/shell.rb
91
94
  - phantom-manager.gemspec
92
95
  - spec/files/config.yml
@@ -94,10 +97,12 @@ files:
94
97
  - spec/lib/monitors/base_spec.rb
95
98
  - spec/lib/monitors/memory_spec.rb
96
99
  - spec/lib/monitors/processes_spec.rb
100
+ - spec/lib/monitors/response_time_spec.rb
97
101
  - spec/lib/monitors/restart_listener_spec.rb
98
102
  - spec/lib/monitors/violations_recorders/base_spec.rb
99
103
  - spec/lib/monitors/violations_recorders/memory_spec.rb
100
104
  - spec/lib/monitors/violations_recorders/processes_spec.rb
105
+ - spec/lib/monitors/violations_recorders/response_time_spec.rb
101
106
  - spec/lib/nginx/manager_spec.rb
102
107
  - spec/lib/phantom/collector_spec.rb
103
108
  - spec/lib/phantom/manager_spec.rb
@@ -138,10 +143,12 @@ test_files:
138
143
  - spec/lib/monitors/base_spec.rb
139
144
  - spec/lib/monitors/memory_spec.rb
140
145
  - spec/lib/monitors/processes_spec.rb
146
+ - spec/lib/monitors/response_time_spec.rb
141
147
  - spec/lib/monitors/restart_listener_spec.rb
142
148
  - spec/lib/monitors/violations_recorders/base_spec.rb
143
149
  - spec/lib/monitors/violations_recorders/memory_spec.rb
144
150
  - spec/lib/monitors/violations_recorders/processes_spec.rb
151
+ - spec/lib/monitors/violations_recorders/response_time_spec.rb
145
152
  - spec/lib/nginx/manager_spec.rb
146
153
  - spec/lib/phantom/collector_spec.rb
147
154
  - spec/lib/phantom/manager_spec.rb