dca 0.1.0
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.
- data/.document +5 -0
- data/.rspec +2 -0
- data/Gemfile +48 -0
- data/Gemfile.lock +126 -0
- data/Guardfile +8 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +48 -0
- data/VERSION +1 -0
- data/bin/dca +5 -0
- data/dca.gemspec +160 -0
- data/lib/dca.rb +64 -0
- data/lib/dca/cli.rb +32 -0
- data/lib/dca/commands/area.rb +133 -0
- data/lib/dca/commands/templates/area/analyzer.rb.erb +34 -0
- data/lib/dca/commands/templates/area/area.rb.erb +2 -0
- data/lib/dca/commands/templates/area/models.rb.erb +2 -0
- data/lib/dca/commands/templates/area/page.rb.erb +17 -0
- data/lib/dca/commands/templates/area/position.rb.erb +8 -0
- data/lib/dca/commands/templates/config.yml.erb +38 -0
- data/lib/dca/commands/templates/spec/analyzer_spec.rb.erb +15 -0
- data/lib/dca/commands/templates/spec/spec_helper.rb.erb +2 -0
- data/lib/dca/config.rb +20 -0
- data/lib/dca/helpers.rb +2 -0
- data/lib/dca/helpers/logger.rb +50 -0
- data/lib/dca/jobs.rb +3 -0
- data/lib/dca/jobs/analyzer_job.rb +119 -0
- data/lib/dca/jobs/job.rb +62 -0
- data/lib/dca/models.rb +5 -0
- data/lib/dca/models/base_model.rb +73 -0
- data/lib/dca/models/binder.rb +68 -0
- data/lib/dca/models/binder_helper.rb +48 -0
- data/lib/dca/models/nokogiri_binder.rb +43 -0
- data/lib/dca/models/position.rb +15 -0
- data/lib/dca/net.rb +1 -0
- data/lib/dca/net/browser_helper.rb +20 -0
- data/lib/dca/notifier.rb +2 -0
- data/lib/dca/notifier/notifier.rb +11 -0
- data/lib/dca/notifier/redis/models/analyze_notify.rb +12 -0
- data/lib/dca/notifier/redis/models/failure_notify.rb +8 -0
- data/lib/dca/notifier/redis/models/fetch_notify.rb +15 -0
- data/lib/dca/notifier/redis/models/session.rb +52 -0
- data/lib/dca/notifier/redis/notifier.rb +25 -0
- data/lib/dca/notifier/redis_notifier.rb +9 -0
- data/lib/dca/storage.rb +3 -0
- data/lib/dca/storage/elasticsearch_storage.rb +80 -0
- data/lib/dca/storage/mongo_storage.rb +51 -0
- data/lib/dca/storage/storage.rb +55 -0
- data/spec/analyzer_spec.rb +64 -0
- data/spec/area_task_spec.rb +45 -0
- data/spec/base_model_spec.rb +34 -0
- data/spec/binder_spec.rb +69 -0
- data/spec/config.yml +18 -0
- data/spec/elasticsearch_storage_spec.rb +28 -0
- data/spec/fixtures/page.html +12 -0
- data/spec/fixtures/positions.yml +13 -0
- data/spec/fixtures/positions_with_error.yml +14 -0
- data/spec/fixtures/states.yml +3 -0
- data/spec/job_spec.rb +31 -0
- data/spec/mock/analyzer_job.rb +30 -0
- data/spec/mock/file_storage.rb +28 -0
- data/spec/mock/notify_object.rb +13 -0
- data/spec/mock/page.rb +13 -0
- data/spec/mock/position.rb +40 -0
- data/spec/mock/web_notifier.rb +30 -0
- data/spec/mongo_storage_spec.rb +20 -0
- data/spec/redis_notifier_spec.rb +98 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/storage_examples.rb +103 -0
- metadata +408 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
module DCA
|
2
|
+
module Redis
|
3
|
+
class Notifier
|
4
|
+
def initialize config
|
5
|
+
Ohm.connect :url => "redis://#{config[:host]}:#{config[:port]}"
|
6
|
+
end
|
7
|
+
|
8
|
+
def push(object, event, options = {})
|
9
|
+
session = Session.find(:uid => object.session, :project => DCA.project_name, :area => object.class.queue).first
|
10
|
+
unless session.present?
|
11
|
+
session = Session.create :uid => object.session, :created => Time.now, :project => DCA.project_name,
|
12
|
+
:area => object.class.queue
|
13
|
+
end
|
14
|
+
|
15
|
+
if event == :analyze
|
16
|
+
session.inc_analyze options[:state]
|
17
|
+
elsif event == :fetch
|
18
|
+
session.inc_fetch options[:state], options[:result] ? :success : :failure
|
19
|
+
elsif event == :failure
|
20
|
+
session.add_failure options[:exception]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require File.expand_path('../redis/models/failure_notify', __FILE__)
|
2
|
+
require File.expand_path('../redis/models/analyze_notify', __FILE__)
|
3
|
+
require File.expand_path('../redis/models/fetch_notify', __FILE__)
|
4
|
+
require File.expand_path('../redis/models/session', __FILE__)
|
5
|
+
require File.expand_path('../redis/notifier', __FILE__)
|
6
|
+
|
7
|
+
module DCA
|
8
|
+
class RedisNotifier < Redis::Notifier; end
|
9
|
+
end
|
data/lib/dca/storage.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
module DCA
|
2
|
+
class ElasticSearchStorage
|
3
|
+
attr_reader :type
|
4
|
+
|
5
|
+
def initialize(connection, context, options = {})
|
6
|
+
@connection = connection
|
7
|
+
@index = options[:index] || DCA.project_name.underscore
|
8
|
+
@type = options[:type] || context.to_s.demodulize.downcase.pluralize
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.establish_connection(config)
|
12
|
+
RestClient.get("http://#{config[:host]}:#{config[:port]}")
|
13
|
+
Tire.configure { url "http://#{config[:host]}:#{config[:port]}" }
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
def state position
|
18
|
+
item = find position
|
19
|
+
return :create if item.nil?
|
20
|
+
|
21
|
+
position.id = item[:id]
|
22
|
+
return :unmodified if item[:checksum] == position.checksum
|
23
|
+
|
24
|
+
:update
|
25
|
+
end
|
26
|
+
|
27
|
+
def find position
|
28
|
+
return nil if position.base_id.nil?
|
29
|
+
query = Tire.search(@index, :type => type) { query { term :base_id, position.base_id } }
|
30
|
+
query.results.first
|
31
|
+
end
|
32
|
+
|
33
|
+
def refresh(item, state)
|
34
|
+
send state, item
|
35
|
+
end
|
36
|
+
|
37
|
+
def create(item)
|
38
|
+
item.updated_at = item.created_at = Time.now.utc
|
39
|
+
data = item.to_hash
|
40
|
+
data[:type] = type
|
41
|
+
|
42
|
+
result = Tire.index(@index).store data
|
43
|
+
Tire.index(@index).refresh
|
44
|
+
|
45
|
+
item.id = result['_id']
|
46
|
+
end
|
47
|
+
|
48
|
+
def update(item)
|
49
|
+
data = item.to_hash
|
50
|
+
data[:type] = type
|
51
|
+
|
52
|
+
Tire.index(@index) do
|
53
|
+
store data
|
54
|
+
refresh
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def remove(item)
|
59
|
+
data = item.to_hash
|
60
|
+
data[:type] = type
|
61
|
+
|
62
|
+
Tire.index(@index) do
|
63
|
+
remove data
|
64
|
+
refresh
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def index(&block)
|
69
|
+
Tire.index @index do
|
70
|
+
instance_eval(&block) if block_given?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def context object
|
75
|
+
result = self.clone
|
76
|
+
result.instance_variable_set :@type, object.to_s.demodulize.downcase.pluralize
|
77
|
+
result
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module DCA
|
2
|
+
class MongoStorage
|
3
|
+
attr_reader :database, :collection
|
4
|
+
|
5
|
+
def initialize(connection, context, options = {})
|
6
|
+
@database = connection.db(options[:database] || DCA.project_name.underscore)
|
7
|
+
@connection = connection
|
8
|
+
@collection = database.collection(options[:collection] || context.to_s.demodulize.downcase.pluralize)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.establish_connection(config)
|
12
|
+
Mongo::Connection.new config[:host], config[:port]
|
13
|
+
end
|
14
|
+
|
15
|
+
def state(position)
|
16
|
+
item = find position
|
17
|
+
return :create if item.nil?
|
18
|
+
|
19
|
+
position.id = item['_id']
|
20
|
+
return :unmodified if item['checksum'] == position.checksum
|
21
|
+
|
22
|
+
return :update
|
23
|
+
end
|
24
|
+
|
25
|
+
def find position
|
26
|
+
collection.find_one base_id: position.base_id unless position.base_id.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
def refresh(item, state)
|
30
|
+
send state, item
|
31
|
+
end
|
32
|
+
|
33
|
+
def create(item)
|
34
|
+
item.id = collection.insert item.to_hash
|
35
|
+
end
|
36
|
+
|
37
|
+
def update(item)
|
38
|
+
collection.update({_id: item.id}, item.to_hash)
|
39
|
+
end
|
40
|
+
|
41
|
+
def remove(item)
|
42
|
+
collection.remove _id: item.id
|
43
|
+
end
|
44
|
+
|
45
|
+
def context object
|
46
|
+
result = self.clone
|
47
|
+
result.instance_variable_set :@collection, result.database.collection(object.to_s.demodulize.downcase.pluralize)
|
48
|
+
result
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module DCA
|
2
|
+
module Storage
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def establish_connection config_name = :db
|
7
|
+
@config = APP_CONFIG[config_name]
|
8
|
+
end
|
9
|
+
|
10
|
+
def root_storage
|
11
|
+
return @storage if @storage
|
12
|
+
|
13
|
+
return storage if @config
|
14
|
+
|
15
|
+
superclass.root_storage if superclass != Object
|
16
|
+
end
|
17
|
+
|
18
|
+
def storage
|
19
|
+
return @storage unless @storage.nil?
|
20
|
+
|
21
|
+
if @config
|
22
|
+
driver_class = "DCA::#{@config[:driver]}Storage".constantize
|
23
|
+
connection = driver_class.establish_connection @config
|
24
|
+
@storage = driver_class.new connection, self, @config
|
25
|
+
else
|
26
|
+
@storage = superclass.root_storage.context self
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def state
|
32
|
+
self.class.storage.state self
|
33
|
+
end
|
34
|
+
|
35
|
+
def save
|
36
|
+
return false unless valid?
|
37
|
+
|
38
|
+
current_state = state
|
39
|
+
|
40
|
+
callback = "before_#{state}"
|
41
|
+
send callback if self.respond_to? callback
|
42
|
+
|
43
|
+
self.class.storage.refresh self, current_state
|
44
|
+
|
45
|
+
callback = "after_#{state}"
|
46
|
+
send callback if self.respond_to? callback
|
47
|
+
|
48
|
+
current_state
|
49
|
+
end
|
50
|
+
|
51
|
+
def destroy
|
52
|
+
self.class.storage.refresh self, :remove
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
require File.expand_path('../mock/position', __FILE__)
|
3
|
+
require File.expand_path('../mock/analyzer_job', __FILE__)
|
4
|
+
require File.expand_path('../mock/file_storage', __FILE__)
|
5
|
+
require File.expand_path('../mock/web_notifier', __FILE__)
|
6
|
+
|
7
|
+
include DCA
|
8
|
+
|
9
|
+
describe 'Analyzer jobs' do
|
10
|
+
before :all do
|
11
|
+
Notifier.create APP_CONFIG[:notifier]
|
12
|
+
end
|
13
|
+
|
14
|
+
after :all do
|
15
|
+
Tire.index('test') do
|
16
|
+
delete
|
17
|
+
refresh
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should notice about analyzed positions" do
|
22
|
+
Mock::WebNotifier.clean
|
23
|
+
Mock::AnalyzerJob.create :fixture => './spec/fixtures/positions.yml'
|
24
|
+
Mock::WebNotifier.queue[:analyze][:create].should equal 2
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should notice about fetch positions" do
|
28
|
+
Mock::WebNotifier.clean
|
29
|
+
Mock::AnalyzerJob.create :fixture => './spec/fixtures/positions.yml'
|
30
|
+
Mock::WebNotifier.queue[:fetch][:create].should equal 2
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should notice about failed fetch positions" do
|
34
|
+
Mock::WebNotifier.clean
|
35
|
+
Mock::AnalyzerJob.create :fixture => './spec/fixtures/positions.yml'
|
36
|
+
Mock::WebNotifier.queue[:failed][:create].should equal 1
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should fetch only newly created and modified position" do
|
40
|
+
Mock::WebNotifier.clean
|
41
|
+
Mock::AnalyzerJob.create :fixture => './spec/fixtures/positions.yml'
|
42
|
+
Mock::WebNotifier.queue[:analyze][:unmodified].should equal 1
|
43
|
+
Mock::WebNotifier.queue[:fetch][:create].should equal 2
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should fetch next position even if exception occurs" do
|
47
|
+
Mock::WebNotifier.clean
|
48
|
+
Mock::AnalyzerJob.create :fixture => './spec/fixtures/positions_with_error.yml'
|
49
|
+
Mock::WebNotifier.queue[:failed][:create].should equal 1
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should analyzed only limit count of posiotions" do
|
53
|
+
Mock::WebNotifier.clean
|
54
|
+
Mock::AnalyzerJob.create :limit => 1, :fixture => './spec/fixtures/positions.yml'
|
55
|
+
Mock::WebNotifier.queue[:analyze][:create].should equal 1
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should support distribute analyze" do
|
59
|
+
Mock::WebNotifier.clean
|
60
|
+
Mock::AnalyzerJob.create :fixture => './spec/fixtures/positions.yml', :distributed => true
|
61
|
+
Mock::WebNotifier.queue[:analyze][:unmodified].should equal 1
|
62
|
+
Mock::WebNotifier.queue[:fetch][:create].should equal 2
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
require File.expand_path('../mock/analyzer_job', __FILE__)
|
3
|
+
|
4
|
+
describe 'Area rake task' do
|
5
|
+
def workers_clean
|
6
|
+
`ps aux | grep -P "resque-\\d"`.split("\n").each do |line|
|
7
|
+
pid = line.split(' ')[1].to_i
|
8
|
+
Process.kill 'TERM', pid
|
9
|
+
end
|
10
|
+
|
11
|
+
sleep 1 while workers_count > 0
|
12
|
+
end
|
13
|
+
|
14
|
+
before :each do
|
15
|
+
Resque.inline = false
|
16
|
+
workers_clean
|
17
|
+
end
|
18
|
+
|
19
|
+
after :all do
|
20
|
+
Resque.remove_queue 'Mock'
|
21
|
+
Resque.inline = true
|
22
|
+
workers_clean
|
23
|
+
#FileUtils.rm_rf File.join(DCA.root, 'log')
|
24
|
+
end
|
25
|
+
|
26
|
+
def workers_count
|
27
|
+
`ps aux | grep -P "resque-\\d"`.split("\n").count
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should stop area analyze work' do
|
31
|
+
current_count = workers_count + 1
|
32
|
+
`rake resque:work BACKGROUND=1 QUEUE=Mock`
|
33
|
+
sleep 1
|
34
|
+
workers_count.should equal current_count
|
35
|
+
|
36
|
+
DCA::CLI.new.area 'stop', 'Mock'
|
37
|
+
workers_count.should equal 0
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should start area analyze work' do
|
41
|
+
DCA::CLI.new.area 'start', 'Mock'
|
42
|
+
sleep 2
|
43
|
+
workers_count.should equal 1
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
require File.expand_path('../mock/position', __FILE__)
|
3
|
+
|
4
|
+
include DCA
|
5
|
+
|
6
|
+
describe Models::BaseModel do
|
7
|
+
let(:position) { Mock::RootPosition.new }
|
8
|
+
let(:child_position) { Mock::ChildPosition.new }
|
9
|
+
describe 'when validate has_one associations and model invalid' do
|
10
|
+
before do
|
11
|
+
position.one_child = child_position
|
12
|
+
position.valid?
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should contains associations errors' do
|
16
|
+
position.errors[:one_child].should_not be_nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'when validate has_many associations and model invalid' do
|
21
|
+
before do
|
22
|
+
position.child_position = [child_position, child_position]
|
23
|
+
position.valid?
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should contains associations errors' do
|
27
|
+
position.errors[:child_position].should_not be_nil
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should contains associations errors array' do
|
31
|
+
position.errors[:child_position].count.should equal 2
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/spec/binder_spec.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
require File.expand_path('../mock/position', __FILE__)
|
3
|
+
require File.expand_path('../mock/page', __FILE__)
|
4
|
+
|
5
|
+
include DCA
|
6
|
+
|
7
|
+
describe 'Binder' do
|
8
|
+
it 'should bind model fields' do
|
9
|
+
page = Mock::Page.new
|
10
|
+
page.bind Nokogiri::HTML open('./spec/fixtures/page.html')
|
11
|
+
|
12
|
+
page.positions.should have(3).items
|
13
|
+
page.positions.each_with_index do |position, index|
|
14
|
+
position.base_id.should == index + 1
|
15
|
+
position.title.should == "Position #{index + 1}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should bind polymorphic association' do
|
20
|
+
page = Mock::PageExt.new :category => :full
|
21
|
+
page.bind Nokogiri::HTML open('./spec/fixtures/page.html')
|
22
|
+
|
23
|
+
page.positions.should have(3).items
|
24
|
+
page.positions.each { |position| position.is_a?(Mock::FullPosition).should be_true }
|
25
|
+
|
26
|
+
|
27
|
+
page = Mock::PageExt.new :category => :ext
|
28
|
+
page.bind Nokogiri::HTML open('./spec/fixtures/page.html')
|
29
|
+
|
30
|
+
page.positions.should have(3).items
|
31
|
+
page.positions.each { |position| position.is_a?(Mock::ExtPosition).should be_true }
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should inherit binding options' do
|
35
|
+
Mock::ExtPosition.associations.should have(4).items
|
36
|
+
Mock::ExtPosition.associations.keys.should include(:base_id, :title, :description)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should override binding options' do
|
40
|
+
Mock::FullPosition.associations.should have(2).items
|
41
|
+
Mock::FullPosition.associations[:base_id][:type].should == :string
|
42
|
+
Mock::FullPosition.associations[:base_id][:options].should == { :selector => 'a' }
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should serializable' do
|
46
|
+
position = Mock::ChildPosition.new name: 'position'
|
47
|
+
position.to_hash.should have_key 'name'
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should serializable with child' do
|
51
|
+
position = Mock::RootPosition.new
|
52
|
+
position.one_child = Mock::ChildPosition.new(name: 'child_position')
|
53
|
+
position.child_position = [Mock::ChildPosition.new(name: 'position')]
|
54
|
+
|
55
|
+
hash = position.to_hash
|
56
|
+
hash['one_child'].is_a?(Hash).should == true
|
57
|
+
|
58
|
+
hash['child_position'][0].is_a?(Hash).should == true
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'should append to has many association' do
|
62
|
+
page = Mock::PageExt.new :category => :full
|
63
|
+
page.bind Nokogiri::HTML open('./spec/fixtures/page.html')
|
64
|
+
page.bind Nokogiri::HTML open('./spec/fixtures/page.html')
|
65
|
+
|
66
|
+
page.positions.should have(6).items
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|