Преглед изворни кода

Merge pull request #2452 from dsander/winter-cleaning

Winter cleaning
Dominik Sander пре 6 година
родитељ
комит
4d9ce16d04

+ 7 - 30
.travis.yml

@@ -11,7 +11,6 @@ before_install:
 env:
   global:
     - APP_SECRET_TOKEN=b2724973fd81c2f4ac0f92ac48eb3f0152c4a11824c122bcf783419a4c51d8b9bba81c8ba6a66c7de599677c7f486242cf819775c433908e77c739c5c8ae118d
-    - RSPEC_TASK=spec:nofeatures
     - COVERALLS_PARALLEL=true
     - secure: fzmSI7PQz6CJiIJNAtLAuy3TMmYCrK4bUil3uufh8JkHfpSGWOZt2i6fZ8yZ7pzwG5Aw7eZDgdFsNcEPJlgUDJhlwjg+QxCJslhotTQ9qI3Ieo85peWlU9dZFTOZcrCu0net/hY2FE4ZpTRb5r8A/DRv9ukA8P8tShhePCjckgg=
     - secure: YjW07LpRSiC9xB6PhLQ4LVv2VphvF3IacV43PLfvzdagjy14yAwKXTUlSadgRaMbndB2dlCTe3YcY11a/xtX/2HDrF14NHPXQdL7e2dJUS3CDLSKZK26x1SOiaaDIrl1jO1xr5kOUd+564MAcNUzDTJQR4CrWl/5t6EwW4iYQVc=
@@ -19,49 +18,27 @@ env:
   matrix:
     - DATABASE_ADAPTER=mysql2
     - DATABASE_ADAPTER=postgresql DATABASE_USERNAME=postgres
-    - DOCKER_IMAGE=cantino/huginn-single-process DOCKERFILE=docker/single-process/Dockerfile
-    - DOCKER_IMAGE=cantino/huginn DOCKERFILE=docker/multi-process/Dockerfile
     - DOCKER_IMAGE=huginn/huginn-single-process DOCKERFILE=docker/single-process/Dockerfile
     - DOCKER_IMAGE=huginn/huginn DOCKERFILE=docker/multi-process/Dockerfile
-    - RSPEC_TASK=spec:features
 matrix:
   exclude:
-    - env: DOCKER_IMAGE=cantino/huginn-single-process DOCKERFILE=docker/single-process/Dockerfile
-    - env: DOCKER_IMAGE=cantino/huginn DOCKERFILE=docker/multi-process/Dockerfile
     - env: DOCKER_IMAGE=huginn/huginn-single-process DOCKERFILE=docker/single-process/Dockerfile
     - env: DOCKER_IMAGE=huginn/huginn DOCKERFILE=docker/multi-process/Dockerfile
-    - env: RSPEC_TASK=spec:features
   include:
-    - rvm: 2.4.4
-      env: DATABASE_ADAPTER=mysql2 DOCKER_IMAGE=cantino/huginn-single-process DOCKERFILE=docker/single-process/Dockerfile
-    - rvm: 2.4.4
-      env: DATABASE_ADAPTER=mysql2 DOCKER_IMAGE=cantino/huginn DOCKERFILE=docker/multi-process/Dockerfile
-    - rvm: 2.4.4
+    - rvm: 2.6.0
       env: DATABASE_ADAPTER=mysql2 DOCKER_IMAGE=huginn/huginn-single-process DOCKERFILE=docker/single-process/Dockerfile
-    - rvm: 2.4.4
+    - rvm: 2.6.0
       env: DATABASE_ADAPTER=mysql2 DOCKER_IMAGE=huginn/huginn DOCKERFILE=docker/multi-process/Dockerfile
-    - rvm: 2.5.1
-      env: RSPEC_TASK=spec:features DATABASE_ADAPTER=mysql2
-    - rvm: 2.5.1
-      env: RSPEC_TASK=spec:features DATABASE_ADAPTER=postgresql DATABASE_USERNAME=postgres
-    - rvm: 2.4.4
-      env: RSPEC_TASK=spec:features DATABASE_ADAPTER=mysql2
-    - rvm: 2.4.4
-      env: RSPEC_TASK=spec:features DATABASE_ADAPTER=postgresql DATABASE_USERNAME=postgres
-    - rvm: 2.3.7
-      env: RSPEC_TASK=spec:features DATABASE_ADAPTER=mysql2
-    - rvm: 2.3.7
-      env: RSPEC_TASK=spec:features DATABASE_ADAPTER=postgresql DATABASE_USERNAME=postgres
 rvm:
-- 2.2.10
-- 2.3.7
-- 2.4.4
-- 2.5.1
+- 2.3.8
+- 2.4.5
+- 2.5.3
+- 2.6.0
 cache: bundler
 bundler_args: --without development production
 script:
   - if [ -z "${DOCKER_IMAGE}" ]; then bundle exec rake db:create db:migrate; else true; fi
-  - if [ -z "${DOCKER_IMAGE}" ]; then bundle exec rake $RSPEC_TASK; else ./build_docker_image.sh; fi
+  - if [ -z "${DOCKER_IMAGE}" ]; then bundle exec rake; else ./build_docker_image.sh; fi
 notifications:
   irc:
     channels:

+ 2 - 3
Gemfile

@@ -156,8 +156,7 @@ group :development do
     gem 'coveralls', '~> 0.8.12', require: false
     gem 'capybara', '~> 2.18'
     gem 'capybara-screenshot'
-    gem 'capybara-select2', require: false
-    gem 'delorean'
+    gem 'capybara-select-2', github: 'Hirurg103/capybara_select2', ref: 'fbf22fb74dec10fa0edcd26da7c5184ba8fa2c76', require: false
     gem 'poltergeist'
     gem 'pry-rails'
     gem 'pry-byebug'
@@ -169,7 +168,7 @@ group :development do
     gem 'rails-controller-testing'
     gem 'shoulda-matchers'
     gem 'vcr'
-    gem 'webmock', '~> 2.3'
+    gem 'webmock', '~> 3.5.1'
   end
 end
 

+ 14 - 14
Gemfile.lock

@@ -1,3 +1,10 @@
+GIT
+  remote: https://github.com/Hirurg103/capybara_select2.git
+  revision: fbf22fb74dec10fa0edcd26da7c5184ba8fa2c76
+  ref: fbf22fb74dec10fa0edcd26da7c5184ba8fa2c76
+  specs:
+    capybara-select-2 (0.3.2)
+
 GIT
   remote: https://github.com/albertsun/tumblr_client.git
   revision: e046fe6e39291c173add0a49081630c7b60a36c7
@@ -178,10 +185,6 @@ GEM
     capybara-screenshot (1.0.17)
       capybara (>= 1.0, < 3)
       launchy
-    capybara-select2 (1.0.1)
-      capybara
-      rspec
-    chronic (0.10.2)
     cliver (0.3.2)
     coderay (1.1.2)
     coffee-rails (4.2.2)
@@ -208,8 +211,6 @@ GEM
     declarative-option (0.1.0)
     delayed_job (4.1.5)
       activesupport (>= 3.0, < 5.3)
-    delorean (2.1.0)
-      chronic
     devise (4.4.3)
       bcrypt (~> 3.0)
       orm_adapter (~> 0.1)
@@ -253,7 +254,7 @@ GEM
       faraday_middleware (>= 0.9)
       loofah (>= 2.0)
       sax-machine (>= 1.0)
-    ffi (1.9.18)
+    ffi (1.9.25)
     font-awesome-sass (4.7.0)
       sass (>= 3.2)
     forecast_io (2.0.1)
@@ -313,7 +314,7 @@ GEM
       guard (~> 2.1)
       guard-compat (~> 1.1)
       rspec (>= 2.99.0, < 4.0)
-    hashdiff (0.3.2)
+    hashdiff (0.3.8)
     hashie (3.5.6)
     haversine (0.3.0)
     hipchat (1.2.0)
@@ -647,7 +648,7 @@ GEM
       activemodel (>= 5.0)
       debug_inspector
       railties (>= 5.0)
-    webmock (2.3.2)
+    webmock (3.5.1)
       addressable (>= 2.3.6)
       crack (>= 0.3.2)
       hashdiff
@@ -678,13 +679,12 @@ DEPENDENCIES
   capistrano-rails (~> 1.1)
   capybara (~> 2.18)
   capybara-screenshot
-  capybara-select2
+  capybara-select-2!
   coffee-rails (~> 4.2)
   coveralls (~> 0.8.12)
   daemons (~> 1.1.9)
   delayed_job (~> 4.1.5)
   delayed_job_active_record!
-  delorean
   devise (~> 4.4.3)
   dotenv!
   dotenv-rails!
@@ -772,13 +772,13 @@ DEPENDENCIES
   unicorn (~> 5.1.0)
   vcr
   web-console (>= 3.3.0)
-  webmock (~> 2.3)
+  webmock (~> 3.5.1)
   weibo_2!
   wunderground (~> 1.2.0)
   xmpp4r (~> 0.5.6)
 
 RUBY VERSION
-   ruby 2.5.1p57
+   ruby 2.6.0p0
 
 BUNDLED WITH
-   1.16.3
+   1.17.2

+ 1 - 1
README.md

@@ -142,4 +142,4 @@ Huginn is provided under the MIT License.
 
 Huginn was originally created by [@cantino](https://github.com/cantino) in 2013. Since then, many people's dedicated contributions have made it what it is today.
 
-[![Build Status](https://travis-ci.org/huginn/huginn.svg)](https://travis-ci.org/huginn/huginn) [![Coverage Status](https://coveralls.io/repos/cantino/huginn/badge.svg)](https://coveralls.io/r/cantino/huginn) [![Dependency Status](https://gemnasium.com/huginn/huginn.svg)](https://gemnasium.com/huginn/huginn) [![Bountysource](https://www.bountysource.com/badge/tracker?tracker_id=282580)](https://www.bountysource.com/trackers/282580-huginn?utm_source=282580&utm_medium=shield&utm_campaign=TRACKER_BADGE)
+[![Build Status](https://travis-ci.org/huginn/huginn.svg)](https://travis-ci.org/huginn/huginn) [![Coverage Status](https://coveralls.io/repos/huginn/huginn/badge.svg)](https://coveralls.io/r/huginn/huginn) [![Dependency Status](https://gemnasium.com/huginn/huginn.svg)](https://gemnasium.com/huginn/huginn) [![Bountysource](https://www.bountysource.com/badge/tracker?tracker_id=282580)](https://www.bountysource.com/trackers/282580-huginn?utm_source=282580&utm_medium=shield&utm_campaign=TRACKER_BADGE)

+ 0 - 5
app/controllers/application_controller.rb

@@ -30,7 +30,6 @@ class ApplicationController < ActionController::Base
     return unless current_user
     twitter_oauth_check
     basecamp_auth_check
-    outdated_docker_image_namespace_check
     outdated_google_auth_check
   end
 
@@ -65,10 +64,6 @@ class ApplicationController < ActionController::Base
     end
   end
 
-  def outdated_docker_image_namespace_check
-    @outdated_docker_image_namespace = ENV['OUTDATED_DOCKER_IMAGE_NAMESPACE'] == 'true'
-  end
-
   def outdated_google_auth_check
     @outdated_google_cal_agents = current_user.agents.of_type('Agents::GoogleCalendarPublishAgent').select do |agent|
       agent.options['google']['key_secret'].present?

+ 34 - 33
app/models/agents/mqtt_agent.rb

@@ -94,16 +94,16 @@ module Agents
     end
 
     def mqtt_client
-      @client ||= MQTT::Client.new(interpolated['uri'])
-
-      if interpolated['ssl']
-        @client.ssl = interpolated['ssl'].to_sym
-        @client.ca_file = interpolated['ca_file']
-        @client.cert_file = interpolated['cert_file']
-        @client.key_file = interpolated['key_file']
+      @client ||= begin
+        MQTT::Client.new(interpolated['uri']).tap do |c|
+          if interpolated['ssl']
+            c.ssl = interpolated['ssl'].to_sym
+            c.ca_file = interpolated['ca_file']
+            c.cert_file = interpolated['cert_file']
+            c.key_file = interpolated['key_file']
+          end
+        end
       end
-
-      @client
     end
 
     def receive(incoming_events)
@@ -117,35 +117,36 @@ module Agents
 
     def check
       last_message = memory['last_message']
+      mqtt_client.connect
 
-      mqtt_client.connect do |c|
-        begin
-          Timeout.timeout((interpolated['max_read_time'].presence || 15).to_i) {
-            c.get_packet(interpolated['topic']) do |packet|
-              topic, payload = message = [packet.topic, packet.payload]
-
-              # Ignore a message if it is previously received
-              next if (packet.retain || packet.duplicate) && message == last_message
-
-              last_message = message
-
-              # A lot of services generate JSON, so try that.
-              begin
-                payload = JSON.parse(payload)
-              rescue
-              end
-
-              create_event payload: {
-                'topic' => topic,
-                'message' => payload,
-                'time' => Time.now.to_i
-              }
-            end
+      poll_thread = Thread.new do
+        mqtt_client.get_packet(interpolated['topic']) do |packet|
+          topic, payload = message = [packet.topic, packet.payload]
+
+          # Ignore a message if it is previously received
+          next if (packet.retain || packet.duplicate) && message == last_message
+
+          last_message = message
+
+          # A lot of services generate JSON, so try that.
+          begin
+            payload = JSON.parse(payload)
+          rescue
+          end
+
+          create_event payload: {
+            'topic' => topic,
+            'message' => payload,
+            'time' => Time.now.to_i
           }
-        rescue Timeout::Error
         end
       end
 
+      sleep (interpolated['max_read_time'].presence || 15).to_f
+
+      mqtt_client.disconnect
+      poll_thread.kill
+
       # Remember the last original (non-retain, non-duplicate) message
       self.memory['last_message'] = last_message
       save!

+ 1 - 1
app/models/agents/weather_agent.rb

@@ -119,7 +119,7 @@ module Agents
     def validate_location
       errors.add(:base, "location is required") unless location.present?
       return if wunderground?
-      if location.match? VALID_COORDS_REGEX
+      if location =~ VALID_COORDS_REGEX
         lat, lon = coordinates
         errors.add :base, "too low of a latitude" unless lat > -90
         errors.add :base, "too big of a latitude" unless lat < 90

+ 0 - 17
app/views/application/_upgrade_warning.html.erb

@@ -45,20 +45,3 @@ TWITTER_OAUTH_SECRET=<%= @twitter_oauth_secret %>
     </div>
   </dir>
 <% end -%>
-<% if @outdated_docker_image_namespace %>
-  <dir class="container">
-    <div class="alert alert-danger" role="alert">
-      <p>
-        <b>Warning!</b> You need to change the namespace of the Huginn Docker image you are using.
-      </p>
-      <br/>
-      <p>
-        Since Huginn moved to a GitHub organization we also changed the namespace of the Docker images. Please change your configuration to the new namespace:
-        <ul>
-          <li><code>cantino/huginn</code> is now <code>huginn/huginn</code></li>
-          <li><code>cantino/huginn-single-process</code> is now <code>huginn/huginn-single-process</code></li>
-        </ul>
-      </p>
-    </div>
-  </dir>
-<% end -%>

+ 7 - 3
bin/spring

@@ -7,9 +7,13 @@ unless defined?(Spring)
   require 'rubygems'
   require 'bundler'
 
-  lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
-  spring = lockfile.specs.detect { |spec| spec.name == "spring" }
-  if spring
+  require File.join(File.dirname(__FILE__), '../lib/gemfile_helper.rb')
+  GemfileHelper.load_dotenv
+
+  if ENV['SPRING']
+    lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
+    spring = lockfile.specs.find { |spec| spec.name == "spring" }
+
     Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
     gem 'spring', spring.version
     require 'spring/binstub'

+ 1 - 5
build_docker_image.sh

@@ -3,11 +3,7 @@ set -ev
 
 docker pull $DOCKER_IMAGE
 
-if [[ $DOCKER_IMAGE =~ "cantino" ]]; then
-  bin/docker_wrapper build --build-arg OUTDATED_DOCKER_IMAGE_NAMESPACE='true' -t $DOCKER_IMAGE -f $DOCKERFILE .
-else
-  bin/docker_wrapper build -t $DOCKER_IMAGE -f $DOCKERFILE .
-fi
+bin/docker_wrapper build -t $DOCKER_IMAGE -f $DOCKERFILE .
 
 if [[ -n "${DOCKER_USER}" && "${TRAVIS_PULL_REQUEST}" = 'false' && "${TRAVIS_BRANCH}" = "master" ]]; then
   docker login -u $DOCKER_USER -p $DOCKER_PASS

+ 1 - 1
doc/manual/installation.md

@@ -79,7 +79,7 @@ Download Ruby and compile it:
 
 Install the bundler and foreman gems:
 
-    sudo gem install rake bundler foreman --no-ri --no-rdoc
+    sudo gem install rake bundler foreman --no-document
 
 ## 3. System Users
 

+ 1 - 1
docker/scripts/prepare

@@ -33,7 +33,7 @@ $minimal_apt_get_install build-essential checkinstall git-core \
   ruby2.5 ruby2.5-dev
 locale-gen en_US.UTF-8
 update-locale LANG=en_US.UTF-8 LC_CTYPE=en_US.UTF-8
-gem install --no-ri --no-rdoc bundler
+gem install --no-document bundler -v 1.17.3
 
 apt-get purge -y python3* rsyslog rsync manpages
 apt-get -y clean

+ 2 - 2
lib/gemfile_helper.rb

@@ -13,7 +13,7 @@ class GemfileHelper
     def load_dotenv
       dotenv_dir = Dir[File.join(File.dirname(__FILE__), '../vendor/gems/dotenv-[0-9]*')].sort.last
 
-      yield dotenv_dir
+      yield dotenv_dir if block_given?
 
       return if ENV['ON_HEROKU'] == 'true'
 
@@ -72,4 +72,4 @@ Capistrano 2 users: Make sure shared files are symlinked before bundle runs: bef
 EOF
     end
   end
-end
+end

+ 0 - 80
lib/utils.rb

@@ -171,87 +171,7 @@ module Utils
     end
   end
 
-  module HTMLTransformer
-    SINGLE = 1
-    MULTIPLE = 2
-    COMMA_SEPARATED = 3
-    SRCSET = 4
 
-    URI_ATTRIBUTES = {
-      'a' => { 'href' => SINGLE },
-      'applet' => { 'archive' => COMMA_SEPARATED, 'codebase' => SINGLE },
-      'area' => { 'href' => SINGLE },
-      'audio' => { 'src' => SINGLE },
-      'base' => { 'href' => SINGLE },
-      'blockquote' => { 'cite' => SINGLE },
-      'body' => { 'background' => SINGLE },
-      'button' => { 'formaction' => SINGLE },
-      'command' => { 'icon' => SINGLE },
-      'del' => { 'cite' => SINGLE },
-      'embed' => { 'src' => SINGLE },
-      'form' => { 'action' => SINGLE },
-      'frame' => { 'longdesc' => SINGLE, 'src' => SINGLE },
-      'head' => { 'profile' => SINGLE },
-      'html' => { 'manifest' => SINGLE },
-      'iframe' => { 'longdesc' => SINGLE, 'src' => SINGLE },
-      'img' => { 'longdesc' => SINGLE, 'src' => SINGLE, 'srcset' => SRCSET, 'usemap' => SINGLE },
-      'input' => { 'formaction' => SINGLE, 'src' => SINGLE, 'usemap' => SINGLE },
-      'ins' => { 'cite' => SINGLE },
-      'link' => { 'href' => SINGLE },
-      'object' => { 'archive' => MULTIPLE, 'classid' => SINGLE, 'codebase' => SINGLE, 'data' => SINGLE, 'usemap' => SINGLE },
-      'q' => { 'cite' => SINGLE },
-      'script' => { 'src' => SINGLE },
-      'source' => { 'src' => SINGLE, 'srcset' => SRCSET },
-      'video' => { 'poster' => SINGLE, 'src' => SINGLE },
-    }
-
-    URI_ELEMENTS_XPATH = '//*[%s]' % URI_ATTRIBUTES.keys.map { |name| "name()='#{name}'" }.join(' or ')
-
-    module_function
-
-    def transform(html, &block)
-      block or raise ArgumentError, 'block must be given'
-
-      case html
-      when /\A\s*(?:<\?xml[\s?]|<!DOCTYPE\s)/i
-        doc = Nokogiri.parse(html)
-        yield doc
-        doc.to_s
-      when /\A\s*<(html|head|body)[\s>]/i
-        # Libxml2 automatically adds DOCTYPE and <html>, so we need to
-        # skip them.
-        element_name = $1
-        doc = Nokogiri::HTML::Document.parse(html)
-        yield doc
-        doc.at_xpath("//#{element_name}").xpath('self::node() | following-sibling::node()').to_s
-      else
-        doc = Nokogiri::HTML::Document.parse("<html><body>#{html}")
-        yield doc
-        doc.xpath("/html/body/node()").to_s
-      end
-    end
-
-    def replace_uris(html, &block)
-      block or raise ArgumentError, 'block must be given'
-
-      transform(html) { |doc|
-        doc.xpath(URI_ELEMENTS_XPATH).each { |element|
-          uri_attrs = URI_ATTRIBUTES[element.name] or next
-          uri_attrs.each { |name, format|
-            attr = element.attribute(name) or next
-            case format
-            when SINGLE
-              attr.value = block.call(attr.value.strip)
-            when MULTIPLE
-              attr.value = attr.value.gsub(/(\S+)/) { block.call($1) }
-            when COMMA_SEPARATED, SRCSET
-              attr.value = attr.value.gsub(/((?:\A|,)\s*)(\S+)/) { $1 + block.call($2) }
-            end
-          }
-        }
-      }
-    end
-  end
 
   def self.rebase_hrefs(html, base_uri)
     base_uri = normalize_uri(base_uri)

+ 83 - 0
lib/utils/html_transformer.rb

@@ -0,0 +1,83 @@
+module Utils
+  module HTMLTransformer
+    SINGLE = 1
+    MULTIPLE = 2
+    COMMA_SEPARATED = 3
+    SRCSET = 4
+
+    URI_ATTRIBUTES = {
+      'a' => { 'href' => SINGLE },
+      'applet' => { 'archive' => COMMA_SEPARATED, 'codebase' => SINGLE },
+      'area' => { 'href' => SINGLE },
+      'audio' => { 'src' => SINGLE },
+      'base' => { 'href' => SINGLE },
+      'blockquote' => { 'cite' => SINGLE },
+      'body' => { 'background' => SINGLE },
+      'button' => { 'formaction' => SINGLE },
+      'command' => { 'icon' => SINGLE },
+      'del' => { 'cite' => SINGLE },
+      'embed' => { 'src' => SINGLE },
+      'form' => { 'action' => SINGLE },
+      'frame' => { 'longdesc' => SINGLE, 'src' => SINGLE },
+      'head' => { 'profile' => SINGLE },
+      'html' => { 'manifest' => SINGLE },
+      'iframe' => { 'longdesc' => SINGLE, 'src' => SINGLE },
+      'img' => { 'longdesc' => SINGLE, 'src' => SINGLE, 'srcset' => SRCSET, 'usemap' => SINGLE },
+      'input' => { 'formaction' => SINGLE, 'src' => SINGLE, 'usemap' => SINGLE },
+      'ins' => { 'cite' => SINGLE },
+      'link' => { 'href' => SINGLE },
+      'object' => { 'archive' => MULTIPLE, 'classid' => SINGLE, 'codebase' => SINGLE, 'data' => SINGLE, 'usemap' => SINGLE },
+      'q' => { 'cite' => SINGLE },
+      'script' => { 'src' => SINGLE },
+      'source' => { 'src' => SINGLE, 'srcset' => SRCSET },
+      'video' => { 'poster' => SINGLE, 'src' => SINGLE },
+    }
+
+    URI_ELEMENTS_XPATH = '//*[%s]' % URI_ATTRIBUTES.keys.map { |name| "name()='#{name}'" }.join(' or ')
+
+    module_function
+
+    def transform(html, &block)
+      block or raise ArgumentError, 'block must be given'
+
+      case html
+      when /\A\s*(?:<\?xml[\s?]|<!DOCTYPE\s)/i
+        doc = Nokogiri.parse(html)
+        yield doc
+        doc.to_s
+      when /\A\s*<(html|head|body)[\s>]/i
+        # Libxml2 automatically adds DOCTYPE and <html>, so we need to
+        # skip them.
+        element_name = $1
+        doc = Nokogiri::HTML::Document.parse(html)
+        yield doc
+        doc.at_xpath("//#{element_name}").xpath('self::node() | following-sibling::node()').to_s
+      else
+        doc = Nokogiri::HTML::Document.parse("<html><body>#{html}")
+        yield doc
+        doc.xpath("/html/body/node()").to_s
+      end
+    end
+
+    def replace_uris(html, &block)
+      block or raise ArgumentError, 'block must be given'
+
+      transform(html) { |doc|
+        doc.xpath(URI_ELEMENTS_XPATH).each { |element|
+          uri_attrs = URI_ATTRIBUTES[element.name] or next
+          uri_attrs.each { |name, format|
+            attr = element.attribute(name) or next
+            case format
+            when SINGLE
+              attr.value = block.call(attr.value.strip)
+            when MULTIPLE
+              attr.value = attr.value.gsub(/(\S+)/) { block.call($1) }
+            when COMMA_SEPARATED, SRCSET
+              attr.value = attr.value.gsub(/((?:\A|,)\s*)(\S+)/) { $1 + block.call($2) }
+            end
+          }
+        }
+      }
+    end
+  end
+end

+ 1 - 1
spec/capybara_helper.rb

@@ -2,7 +2,7 @@ require 'rails_helper'
 require 'capybara/rails'
 require 'capybara/poltergeist'
 require 'capybara-screenshot/rspec'
-require 'capybara-select2'
+require 'capybara-select-2'
 
 CAPYBARA_TIMEOUT = ENV['CI'] == 'true' ? 60 : 5
 

+ 7 - 1
spec/db/seeds/admin_and_default_scenario_spec.rb

@@ -7,6 +7,11 @@ describe Seeder do
   end
 
   describe '.seed' do
+    before(:each) do
+      User.delete_all
+      expect(User.count).to eq(0)
+    end
+
     it 'imports a default scenario' do
       expect { Seeder.seed }.to change(Agent, :count).by(7)
     end
@@ -18,7 +23,8 @@ describe Seeder do
 
     it 'can be run multiple times and exit normally' do
       Seeder.seed
-      expect { Seeder.seed }.to raise_error(SystemExit)
+      mock(Seeder).exit
+      Seeder.seed
     end
   end
 

+ 1 - 1
spec/features/form_configurable_feature_spec.rb

@@ -5,6 +5,6 @@ describe "form configuring agents", js: true do
     login_as(users(:bob))
     visit edit_agent_path(agents(:bob_csv_agent))
     check('Propagate immediately')
-    select2("serialize", from: "Mode")
+    select2("serialize", from: "Mode", match: :first)
   end
 end

+ 2 - 2
spec/lib/location_spec.rb

@@ -3,8 +3,8 @@ require 'rails_helper'
 describe Location do
   let(:location) {
     Location.new(
-      lat: BigDecimal.new('2.0'),
-      lng: BigDecimal.new('3.0'),
+      lat: BigDecimal('2.0'),
+      lng: BigDecimal('3.0'),
       radius: 300,
       speed: 2,
       course: 30)

+ 2 - 2
spec/models/agent_spec.rb

@@ -653,7 +653,7 @@ describe Agent do
     describe "cleaning up now-expired events" do
       before do
         @time = "2014-01-01 01:00:00 +00:00"
-        time_travel_to @time do
+        travel_to @time do
           @agent = Agents::SomethingSource.new(:name => "something")
           @agent.keep_events_for = 5.days
           @agent.user = users(:bob)
@@ -678,7 +678,7 @@ describe Agent do
 
       describe "when keep_events_for is changed" do
         it "updates events' expires_at" do
-          time_travel_to @time do
+          travel_to @time do
             expect {
                 @agent.options[:foo] = "bar1"
                 @agent.keep_events_for = 3.days

+ 1 - 1
spec/models/agents/attribute_difference_agent_spec.rb

@@ -66,7 +66,7 @@ describe Agents::AttributeDifferenceAgent do
     it 'isnt when event created outside :expected_update_period_in_days' do
       @checker.options[:expected_update_period_in_days] = 2
 
-      time_travel_to 2.days.from_now do
+      travel 49.hours do
         expect(@checker).not_to be_working
       end
     end

+ 2 - 2
spec/models/agents/change_detector_agent_spec.rb

@@ -55,8 +55,8 @@ describe Agents::ChangeDetectorAgent do
     it "isnt when event created outside :expected_update_period_in_days" do
       @checker.options[:expected_update_period_in_days] = 2
 
-      time_travel_to 2.days.from_now do
-          expect(@checker).not_to be_working
+      travel 49.hours do
+        expect(@checker).not_to be_working
       end
     end
   end

+ 1 - 1
spec/models/agents/de_duplication_agent_spec.rb

@@ -68,7 +68,7 @@ describe Agents::DeDuplicationAgent do
     it "isnt when event created outside :expected_update_period_in_days" do
       @checker.options[:expected_update_period_in_days] = 2
 
-      time_travel_to 2.days.from_now do
+      travel 49.hours do
           expect(@checker).not_to be_working
       end
     end

+ 2 - 2
spec/models/agents/email_agent_spec.rb

@@ -36,8 +36,8 @@ describe Agents::EmailAgent do
       expect(ActionMailer::Base.deliveries.count).to eq(2)
       expect(ActionMailer::Base.deliveries.last.to).to eq(["bob@example.com"])
       expect(ActionMailer::Base.deliveries.last.subject).to eq("something interesting")
-      expect(get_message_part(ActionMailer::Base.deliveries.last, /plain/).strip).to eq("Event\n  data: Something else you should know about")
-      expect(get_message_part(ActionMailer::Base.deliveries.first, /plain/).strip).to eq("hi!\n  data: Something you should know about")
+      expect(get_message_part(ActionMailer::Base.deliveries.last, /plain/).strip).to eq("Event\r\n  data: Something else you should know about")
+      expect(get_message_part(ActionMailer::Base.deliveries.first, /plain/).strip).to eq("hi!\r\n  data: Something you should know about")
     end
 
     it "logs and re-raises any mailer errors" do

+ 1 - 1
spec/models/agents/email_digest_agent_spec.rb

@@ -65,7 +65,7 @@ describe Agents::EmailDigestAgent do
 
       expect(ActionMailer::Base.deliveries.last.to).to eq(["bob@example.com"])
       expect(ActionMailer::Base.deliveries.last.subject).to eq("something interesting")
-      expect(get_message_part(ActionMailer::Base.deliveries.last, /plain/).strip).to eq("Event\n  data: Something you should know about\n\nFoo\n  bar: 2\n  url: http://google.com\n\nhi\n  woah: there\n\nEvent\n  test: 2")
+      expect(get_message_part(ActionMailer::Base.deliveries.last, /plain/).strip).to eq("Event\r\n  data: Something you should know about\r\n\r\nFoo\r\n  bar: 2\r\n  url: http://google.com\r\n\r\nhi\r\n  woah: there\r\n\r\nEvent\r\n  test: 2")
       expect(@checker.reload.memory[:events]).to be_empty
     end
 

+ 1 - 1
spec/models/agents/mqtt_agent_spec.rb

@@ -15,7 +15,7 @@ describe Agents::MqttAgent do
     @valid_params = {
       'uri' => "mqtt://#{@server.address}:#{@server.port}",
       'topic' => '/#',
-      'max_read_time' => '1',
+      'max_read_time' => '0.1',
       'expected_update_period_in_days' => "2"
     }
 

+ 3 - 3
spec/models/agents/public_transport_agent_spec.rb

@@ -26,7 +26,7 @@ describe Agents::PublicTransportAgent do
     end
 
     it "should add 4 items to memory" do
-      time_travel_to Time.parse("2014-01-14 20:21:30 +0500") do
+      travel_to Time.parse("2014-01-14 20:21:30 +0500") do
         expect(@agent.memory).to eq({})
         @agent.check
         @agent.save
@@ -46,10 +46,10 @@ describe Agents::PublicTransportAgent do
     end
 
     it "should reset memory after 2 hours" do
-      time_travel_to Time.parse("2014-01-14 20:21:30 +0500") do
+      travel_to Time.parse("2014-01-14 20:21:30 +0500") do
         expect { @agent.check }.to change {@agent.events.count}.by(4)
       end
-      time_travel_to "2014-01-14 23:21:30 +0500".to_time do
+      travel_to "2014-01-14 23:21:30 +0500".to_time do
         @agent.cleanup_old_memory
         expect { @agent.check }.to change {@agent.events.count}.by(4)
       end

+ 3 - 3
spec/models/agents/weather_agent_spec.rb

@@ -84,7 +84,7 @@ describe Agents::WeatherAgent do
   describe "Agents::WeatherAgent::VALID_COORDS_REGEX" do
     it "matches 37.779329,-122.41915" do
       expect(
-        "37.779329,-122.41915".match? Agents::WeatherAgent::VALID_COORDS_REGEX
+        "37.779329,-122.41915" =~ Agents::WeatherAgent::VALID_COORDS_REGEX
       ).to be_truthy
     end
     it "matches a dozen random valid values" do
@@ -92,8 +92,8 @@ describe Agents::WeatherAgent do
       valid_latitude_range = -90.0..90.0
       12.times do
         expect(
-          "#{rand valid_latitude_range},#{rand valid_longitude_range}".match? Agents::WeatherAgent::VALID_COORDS_REGEX
-        ).to be true
+          "#{rand valid_latitude_range},#{rand valid_longitude_range}" =~ Agents::WeatherAgent::VALID_COORDS_REGEX
+        ).not_to be_nil
       end
     end
   end

+ 3 - 2
spec/models/agents/website_agent_spec.rb

@@ -126,9 +126,10 @@ describe Agents::WebsiteAgent do
 
     describe "#check" do
       it "should check for changes (and update Event.expires_at)" do
-        expect { @checker.check }.to change { Event.count }.by(1)
+        travel(-2.seconds) do
+          expect { @checker.check }.to change { Event.count }.by(1)
+        end
         event = Event.last
-        sleep 2
         expect { @checker.check }.not_to change { Event.count }
         update_event = Event.last
         expect(update_event.expires_at).not_to eq(event.expires_at)

+ 1 - 0
spec/models/agents/weibo_publish_agent_spec.rb

@@ -26,6 +26,7 @@ describe Agents::WeiboPublishAgent do
     @sent_pictures = []
     stub.any_instance_of(Agents::WeiboPublishAgent).publish_tweet { |message| @sent_messages << message}
     stub.any_instance_of(Agents::WeiboPublishAgent).publish_tweet_with_pic { |message, picture| @sent_pictures << picture}
+    stub.any_instance_of(Agents::WeiboPublishAgent).sleep
   end
 
   describe '#receive' do

+ 2 - 2
spec/rails_helper.rb

@@ -3,7 +3,7 @@ ENV["RAILS_ENV"] ||= 'test'
 if ENV['COVERAGE']
   require 'simplecov'
   SimpleCov.start 'rails'
-else
+elsif ENV['CI'] == 'true'
   require 'coveralls'
   Coveralls.wear!('rails')
 end
@@ -70,7 +70,7 @@ RSpec.configure do |config|
 
   config.include Devise::Test::ControllerHelpers, type: :controller
   config.include SpecHelpers
-  config.include Delorean
+  config.include ActiveSupport::Testing::TimeHelpers
 end
 
 if ENV['RSPEC_TASK'] != 'spec:nofeatures'