├── .github
└── workflows
│ └── dynamic-security.yml
├── .gitignore
├── .rspec
├── .travis.yml
├── Appraisals
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── SECURITY.md
├── capybara_discoball.gemspec
├── gemfiles
├── capybara_2.gemfile
└── capybara_3.gemfile
├── lib
├── capybara_discoball.rb
└── capybara_discoball
│ ├── retryable.rb
│ ├── runner.rb
│ ├── server.rb
│ └── version.rb
└── spec
├── black_box
└── rails_app_spec.rb
├── lib
├── capybara_discoball
│ └── runner_spec.rb
└── capybara_discoball_spec.rb
└── spec_helper.rb
/.github/workflows/dynamic-security.yml:
--------------------------------------------------------------------------------
1 | name: update-security
2 |
3 | on:
4 | push:
5 | paths:
6 | - SECURITY.md
7 | branches:
8 | - main
9 | workflow_dispatch:
10 |
11 | jobs:
12 | update-security:
13 | permissions:
14 | contents: write
15 | pull-requests: write
16 | pages: write
17 | uses: thoughtbot/templates/.github/workflows/dynamic-security.yaml@main
18 | secrets:
19 | token: ${{ secrets.GITHUB_TOKEN }}
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | .bundle
3 | Gemfile.lock
4 | pkg/*
5 | tmp/*
6 | gemfiles/*.lock
7 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --require spec_helper
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - "2.3"
4 | - "2.4"
5 | - "2.5"
6 | sudo: false
7 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | appraise "capybara-2" do
2 | gem "capybara", "~> 2.7"
3 | end
4 |
5 | appraise "capybara-3" do
6 | gem "capybara", "~> 3.0"
7 | end
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## v0.1.0
4 |
5 | - Runner retries finding an unused port [#15]
6 |
7 | ## v0.0.3
8 |
9 | - Add Capybara 3.x support [#19]
10 |
11 | ## v0.0.2
12 |
13 | - Use `Capybara.register_server`, introduced in Capybara 2.7.0
14 | - Require Capybara ~> 2.7
15 |
16 | ## v0.0.1
17 |
18 | - Initial release
19 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | We love pull requests from everyone. Follow the thoughtbot [code of conduct]
2 | while contributing.
3 |
4 | [code of conduct]: https://thoughtbot.com/open-source-code-of-conduct
5 |
6 | ## Contributing
7 |
8 | 1. Fork the repo.
9 | 2. Run the tests. We only take pull requests with passing tests, and it's
10 | great to know that you have a clean slate.
11 | 3. Add a test for your change. Only refactoring and documentation changes
12 | require no new tests. If you are adding functionality or fixing a bug, we
13 | need a test!
14 | 4. Make the test pass.
15 | 5. Push to your fork and submit a pull request.
16 |
17 | At this point you're waiting on us. We like to at least comment on, if not
18 | accept, pull requests within three business days (and, typically, one business
19 | day). We may suggest some changes or improvements or alternatives.
20 |
21 | Some things that will increase the chance that your pull request is accepted,
22 |
23 | * Include tests that fail without your code, and pass with it
24 | * Update the documentation, the surrounding one, examples elsewhere, guides,
25 | whatever is affected by your contribution
26 | * Follow the existing style of the project
27 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Specify your gem's dependencies in capybara_discoball.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2012-2015 thoughtbot, inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | capybara_discoball
2 | ==================
3 |
4 | [](https://travis-ci.org/thoughtbot/capybara_discoball)
5 | [](https://codeclimate.com/github/thoughtbot/capybara_discoball)
6 |
7 | Spin up a rack app just for Capybara.
8 |
9 | This is useful for when ShamRack won't cut it: when your JavaScript hits
10 | an external service, or you need to load an image or iframe from
11 | elsewhere, or in general something outside of your Ruby code needs to
12 | talk with an API.
13 |
14 | Synopsis
15 | --------
16 |
17 | ```ruby
18 | # Use Sinatra, for example
19 | require 'sinatra/base'
20 | require 'capybara_discoball'
21 |
22 | # Define a quick Rack app
23 | class FakeMusicDB < Sinatra::Base
24 | cattr_reader :albums
25 |
26 | get '/musicians/:musician/albums' do |musician|
27 | <<-XML
28 |
29 | #{@albums.map { |album| "#{album}" }.join}
30 |
31 | XML
32 | end
33 | end
34 |
35 | # Spin up the Rack app, then update the imaginary library we're
36 | # using to point to the new URL.
37 | Capybara::Discoball.spin(FakeMusicDB) do |server|
38 | MusicDB.endpoint_url = server.url
39 | end
40 | ```
41 |
42 | More details
43 | ------------
44 |
45 | You can instantiate a `Capybara::Discoball::Runner`, passing in a
46 | factory which will create a Rack app:
47 |
48 | ```ruby
49 | FakeMusicDBRunner = Capybara::Discoball::Runner.new(FakeMusicDB) do
50 | # tests to perform after server boot
51 | end
52 | ```
53 |
54 | This gives you back a runner, which you can boot from your features,
55 | specs, tests, console, whatever:
56 |
57 | ```ruby
58 | FakeMusicDBRunner.boot
59 | ```
60 |
61 | These two steps can be merged with the `spin` class method:
62 |
63 | ```ruby
64 | Capybara::Discoball.spin(FakeMusicDB) do
65 | # tests to perform while server is spinning
66 | end
67 | ```
68 |
69 | It is always the case that you need to know the URL for the external
70 | API. We provide a way to access that URL; in fact, we offer the whole
71 | `Capybara::Server` for you to play with. In this example, we are using
72 | some `MusicDB` library in the code that knows to hit the
73 | `.endpoint_url`:
74 |
75 | ```ruby
76 | FakeMusicDBRunner = Capybara::Discoball::Runner.new(FakeMusicDB) do |server|
77 | MusicDB.endpoint_url = server.url
78 | end
79 | ```
80 |
81 | If no block is provided, the URL is also returned by `#spin`:
82 |
83 | ```ruby
84 | MusicDB.endpoint_url = Capybara::Discoball.spin(FakeMusicDB)
85 | ```
86 |
87 | Integrating into your app
88 | -------------------------
89 |
90 | All of this means that you must be able to set the endpoint URL. There
91 | are two tricky cases:
92 |
93 | *When the third-party library does not have hooks to set the endpoint
94 | URL*.
95 |
96 | Open the class and add the hooks yourself. This requires understanding
97 | the source of the library. Here's an example where the library uses
98 | `@@endpoint_url` everywhere to refer to the endpoint URL:
99 |
100 | ```ruby
101 | class MusicDB
102 | def self.endpoint_url=(endpoint_url)
103 | @@endpoint_url = endpoint_url
104 | end
105 | end
106 | ```
107 |
108 | *When your JavaScript needs to talk to the endpoint URL*.
109 |
110 | For this you must thread the URL through your app so that the JavaScript
111 | can find it:
112 |
113 | ```ruby
114 | <% content_for :javascript do %>
115 | <% javascript_tag do %>
116 | albumShower = new AlbumShower(<%= MusicDB.endpoint_url.to_json %>);
117 | albumShower.show();
118 | <% end %>
119 | <% end %>
120 |
121 | class @AlbumShower
122 | constructor: (@endpointUrl) ->
123 | show: ->
124 | $.get(@endpointUrl, (data) -> $('#albums').html(data))
125 | ```
126 |
127 | Contributing
128 | ------------
129 |
130 | See the [CONTRIBUTING] document. Thank you, [contributors]!
131 |
132 | [CONTRIBUTING]: /CONTRIBUTING.md
133 | [contributors]: https://github.com/thoughtbot/capybara_discoball/graphs/contributors
134 |
135 | License
136 | -------
137 |
138 | capybara_discoball is Copyright (c) 2012-2018 thoughtbot, inc. It is free software,
139 | and may be redistributed under the terms specified in the [LICENSE] file.
140 |
141 | [LICENSE]: /LICENSE
142 |
143 | About
144 | -----
145 |
146 | 
147 |
148 | capybara_discoball is maintained and funded by thoughtbot, inc.
149 | The names and logos for thoughtbot are trademarks of thoughtbot, inc.
150 |
151 | We love open source software!
152 | See [our other projects][community]
153 | or [hire us][hire] to help build your product.
154 |
155 | [community]: https://thoughtbot.com/community?utm_source=github
156 | [hire]: https://thoughtbot.com/hire-us?utm_source=github
157 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 |
3 | desc "Appraisals: Test different dependency versions"
4 | task :run_appraisals do
5 | sh("bundle exec appraisal install")
6 | sh("bundle exec appraisal rspec --tag ~type:black_box")
7 | end
8 |
9 | begin
10 | require "rspec/core/rake_task"
11 | RSpec::Core::RakeTask.new(:spec)
12 | task default: [:spec, :run_appraisals]
13 | rescue LoadError
14 | end
15 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 | # Security Policy
3 |
4 | ## Supported Versions
5 |
6 | Only the the latest version of this project is supported at a given time. If
7 | you find a security issue with an older version, please try updating to the
8 | latest version first.
9 |
10 | If for some reason you can't update to the latest version, please let us know
11 | your reasons so that we can have a better understanding of your situation.
12 |
13 | ## Reporting a Vulnerability
14 |
15 | For security inquiries or vulnerability reports, visit
16 | .
17 |
18 | If you have any suggestions to improve this policy, visit .
19 |
20 |
21 |
--------------------------------------------------------------------------------
/capybara_discoball.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path("../lib", __FILE__)
3 | require "capybara_discoball/version"
4 |
5 | Gem::Specification.new do |s|
6 | s.name = "capybara_discoball"
7 | s.version = Capybara::Discoball::VERSION
8 | s.authors = ["Mike Burns"]
9 | s.email = ["mburns@thoughtbot.com"]
10 | s.homepage = ""
11 | s.summary = %q{Spin up an external server just for Capybara}
12 | s.description = <<-DESC
13 | When ShamRack doesn't quite cut it; when your JavaScript and non-Ruby
14 | code needs to hit an external API for your tests; when you're excited
15 | about spinning up a full server instead of faking out Net::HTTP: we
16 | present the Discoball.
17 | DESC
18 |
19 | s.rubyforge_project = "capybara_discoball"
20 |
21 | s.files = `git ls-files`.split("\n")
22 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
23 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
24 | s.require_paths = ["lib"]
25 |
26 | s.add_dependency 'capybara', '>= 2.7', '< 4'
27 |
28 | s.add_development_dependency 'appraisal'
29 | s.add_development_dependency 'jet_black', '~> 0.2'
30 | s.add_development_dependency 'pry'
31 | s.add_development_dependency 'rake'
32 | s.add_development_dependency 'rspec'
33 | s.add_development_dependency 'sinatra'
34 | end
35 |
--------------------------------------------------------------------------------
/gemfiles/capybara_2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "http://rubygems.org"
4 |
5 | gem "capybara", "~> 2.7"
6 |
7 | gemspec path: "../"
8 |
--------------------------------------------------------------------------------
/gemfiles/capybara_3.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "http://rubygems.org"
4 |
5 | gem "capybara", "~> 3.0"
6 |
7 | gemspec path: "../"
8 |
--------------------------------------------------------------------------------
/lib/capybara_discoball.rb:
--------------------------------------------------------------------------------
1 | require "capybara_discoball/version"
2 | require "capybara_discoball/server"
3 | require "capybara_discoball/runner"
4 |
5 | module Capybara
6 | module Discoball
7 | def self.spin(app, &block)
8 | Runner.new(app, &block).boot
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/capybara_discoball/retryable.rb:
--------------------------------------------------------------------------------
1 | module Capybara
2 | module Discoball
3 | module Retryable
4 | def with_retries(retry_count, *rescuable_exceptions)
5 | yield
6 | rescue *rescuable_exceptions => e
7 | if retry_count > 0
8 | retry_count -= 1
9 | puts e.inspect if ENV.key?("DEBUG")
10 | retry
11 | else
12 | raise
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/capybara_discoball/runner.rb:
--------------------------------------------------------------------------------
1 | require "capybara"
2 | require_relative "retryable"
3 |
4 | module Capybara
5 | module Discoball
6 | class Runner
7 | include Capybara::Discoball::Retryable
8 |
9 | RETRY_COUNT = 3
10 |
11 | def initialize(server_factory, &block)
12 | @server_factory = server_factory
13 | @after_server = block || Proc.new {}
14 | end
15 |
16 | def boot
17 | with_webrick_runner do
18 | @server = Server.new(@server_factory.new)
19 | @server.boot
20 | end
21 |
22 | @after_server.call(@server)
23 | @server.url
24 | end
25 |
26 | private
27 |
28 | def with_webrick_runner
29 | default_server_process = Capybara.server
30 | Capybara.server = :webrick
31 |
32 | with_retries(RETRY_COUNT, Errno::EADDRINUSE) { yield }
33 | ensure
34 | Capybara.server = default_server_process
35 | end
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/lib/capybara_discoball/server.rb:
--------------------------------------------------------------------------------
1 | require "capybara/server"
2 |
3 | module Capybara
4 | module Discoball
5 | class Server < ::Capybara::Server
6 | def url
7 | "http://#{host}:#{port}"
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/capybara_discoball/version.rb:
--------------------------------------------------------------------------------
1 | module Capybara
2 | module Discoball
3 | VERSION = "0.1.0"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/spec/black_box/rails_app_spec.rb:
--------------------------------------------------------------------------------
1 | require "jet_black"
2 |
3 | RSpec.describe "Using Discoball in a Rails app" do
4 | let(:session) do
5 | JetBlack::Session.new(options: { clean_bundler_env: true })
6 | end
7 |
8 | it "works with a block" do
9 | create_rails_application
10 | setup_discoball
11 | setup_rspec_rails
12 | install_success_api
13 |
14 | setup_controller_action(<<~RUBY)
15 | render plain: SuccessAPI.get
16 | RUBY
17 |
18 | setup_integration_spec(<<~RUBY)
19 | visit "/successes"
20 | expect(page).to have_content("success")
21 | RUBY
22 |
23 | setup_spec_supporter(<<-RUBY)
24 | require "sinatra/base"
25 | require "capybara_discoball"
26 | require "success_api"
27 |
28 | class FakeSuccess < Sinatra::Base
29 | get("/") { "success" }
30 | end
31 |
32 | Capybara::Discoball.spin(FakeSuccess) do |server|
33 | SuccessAPI.endpoint_url = server.url
34 | end
35 | RUBY
36 |
37 | expect(run_integration_test).
38 | to be_a_success.and have_stdout(/1 example, 0 failures/)
39 | end
40 |
41 | it "works without a block" do
42 | create_rails_application
43 | setup_discoball
44 | setup_rspec_rails
45 | install_success_api
46 |
47 | setup_controller_action(<<~RUBY)
48 | render :plain => SuccessAPI.get
49 | RUBY
50 |
51 | setup_integration_spec(<<~RUBY)
52 | visit "/successes"
53 | expect(page).to have_content("success")
54 | RUBY
55 |
56 | setup_spec_supporter(<<-RUBY)
57 | require "sinatra/base"
58 | require "capybara_discoball"
59 | require "success_api"
60 |
61 | class FakeSuccess < Sinatra::Base
62 | get("/") { "success" }
63 | end
64 |
65 | SuccessAPI.endpoint_url = Capybara::Discoball.spin(FakeSuccess)
66 | RUBY
67 |
68 | expect(run_integration_test).
69 | to be_a_success.and have_stdout("1 example, 0 failures")
70 | end
71 |
72 | private
73 |
74 | def create_rails_application
75 | session.create_file("Gemfile", <<~RUBY)
76 | source "http://rubygems.org"
77 |
78 | gem "rails"
79 | RUBY
80 |
81 | session.run("bundle install")
82 |
83 | rails_new_cmd = [
84 | "bundle exec rails new .",
85 | "--skip-bundle",
86 | "--skip-test",
87 | "--skip-coffee",
88 | "--skip-turbolinks",
89 | "--skip-spring",
90 | "--skip-bootsnap",
91 | "--force",
92 | ].join(" ")
93 |
94 | expect(session.run(rails_new_cmd)).
95 | to be_a_success.and have_stdout("force Gemfile")
96 | end
97 |
98 | def setup_discoball
99 | discoball_path = File.expand_path("../../", __dir__)
100 |
101 | session.append_to_file "Gemfile", <<~RUBY
102 | gem "capybara_discoball", path: "#{discoball_path}"
103 | gem "sinatra"
104 | RUBY
105 |
106 | expect(session.run("bundle install")).
107 | to be_a_success.and have_stdout(/capybara_discoball .* from source at/)
108 | end
109 |
110 | def setup_rspec_rails
111 | session.append_to_file("Gemfile", <<~RUBY)
112 | gem "rspec-rails"
113 | RUBY
114 |
115 | session.run("bundle install")
116 |
117 | expect(session.run("bundle exec rails g rspec:install")).
118 | to be_a_success.and have_stdout("create spec/rails_helper.rb")
119 | end
120 |
121 | def install_success_api
122 | session.create_file("app/models/success_api.rb", <<~RUBY)
123 | require "net/http"
124 | require "uri"
125 |
126 | class SuccessAPI
127 | @@endpoint_url = "http://yahoo.com/"
128 |
129 | def self.endpoint_url=(endpoint_url)
130 | @@endpoint_url = endpoint_url
131 | end
132 |
133 | def self.get
134 | Net::HTTP.get(uri)
135 | end
136 |
137 | private
138 |
139 | def self.uri
140 | URI.parse(@@endpoint_url)
141 | end
142 | end
143 | RUBY
144 | end
145 |
146 | def setup_controller_action(content)
147 | session.create_file("app/controllers/whatever_controller.rb", <<~RUBY)
148 | class WhateverController < ApplicationController
149 | def the_action
150 | #{content}
151 | end
152 | end
153 | RUBY
154 |
155 | session.create_file("config/routes.rb", <<~RUBY)
156 | Rails.application.routes.draw do
157 | get "/successes", to: "whatever#the_action"
158 | end
159 | RUBY
160 | end
161 |
162 | def setup_integration_spec(spec_content)
163 | session.create_file("spec/integration/whatever_spec.rb", <<~RUBY)
164 | require "rails_helper"
165 | require "capybara/rspec"
166 | require "support/whatever"
167 |
168 | RSpec.describe "whatever", type: :feature do
169 | it "does the thing" do
170 | #{spec_content}
171 | end
172 | end
173 | RUBY
174 | end
175 |
176 | def setup_spec_supporter(support_content)
177 | session.create_file("spec/support/whatever.rb", support_content)
178 | end
179 |
180 | def run_integration_test
181 | session.run("bundle exec rspec spec/integration")
182 | end
183 | end
184 |
--------------------------------------------------------------------------------
/spec/lib/capybara_discoball/runner_spec.rb:
--------------------------------------------------------------------------------
1 | require "capybara_discoball"
2 | require "sinatra/base"
3 |
4 | RSpec.describe Capybara::Discoball::Runner do
5 | describe "when Capybara fails to find an unused port" do
6 | it "retries up to 3 times" do
7 | expected_url = "http://localhost:9999"
8 |
9 | allow(Capybara::Discoball::Server).to receive(:new).and_return(
10 | unbootable_server,
11 | unbootable_server,
12 | unbootable_server,
13 | bootable_server(url: expected_url),
14 | )
15 |
16 | runner = described_class.new(Sinatra::Base)
17 |
18 | expect(runner.boot).to eq expected_url
19 | end
20 | end
21 |
22 | private
23 |
24 | def bootable_server(url:)
25 | instance_double(Capybara::Discoball::Server, boot: nil, url: url)
26 | end
27 |
28 | def unbootable_server
29 | server = instance_double(Capybara::Discoball::Server)
30 | allow(server).to receive(:boot).and_raise(Errno::EADDRINUSE)
31 | server
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/lib/capybara_discoball_spec.rb:
--------------------------------------------------------------------------------
1 | require "capybara_discoball"
2 | require "net/http"
3 | require "sinatra/base"
4 |
5 | RSpec.describe Capybara::Discoball do
6 | it "spins up a server" do
7 | example_discoball_app = Class.new(Sinatra::Base) do
8 | get("/") { "success" }
9 | end
10 |
11 | server_url = described_class.spin(example_discoball_app)
12 | response = Net::HTTP.get(URI(server_url))
13 |
14 | expect(response).to eq "success"
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require "jet_black/rspec"
2 |
3 | RSpec.configure do |config|
4 | config.expect_with :rspec do |expectations|
5 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
6 | end
7 |
8 | config.mock_with :rspec do |mocks|
9 | mocks.verify_partial_doubles = true
10 | end
11 |
12 | config.shared_context_metadata_behavior = :apply_to_host_groups
13 | config.disable_monkey_patching!
14 | config.warnings = true
15 |
16 | config.order = :random
17 | Kernel.srand config.seed
18 | end
19 |
--------------------------------------------------------------------------------