From 8712842fc7209ef366a5e263a559574e97a1614d Mon Sep 17 00:00:00 2001 From: Misha Merkushin Date: Tue, 5 Nov 2019 15:40:12 +0300 Subject: [PATCH] tests: cover setup generator --- .dockerdev/docker-compose.yml | 8 +- .rubocop.yml | 4 + anycable-rails.gemspec | 1 + .../anycable/setup/docker/docker_generator.rb | 36 ---- .../anycable/setup/local/local_generator.rb | 96 --------- .../anycable/setup/setup_generator.rb | 188 +++++++++++++++++- spec/fixtures/basic_rails_app/Procfile.dev | 3 + .../config/environments/development.rb | 8 + .../config/environments/production.rb | 16 ++ spec/generators/setup/setup_generator_spec.rb | 87 ++++++++ spec/spec_helper.rb | 5 + 11 files changed, 305 insertions(+), 147 deletions(-) delete mode 100644 lib/rails/generators/anycable/setup/docker/docker_generator.rb delete mode 100644 lib/rails/generators/anycable/setup/local/local_generator.rb create mode 100644 spec/fixtures/basic_rails_app/Procfile.dev create mode 100644 spec/fixtures/basic_rails_app/config/environments/development.rb create mode 100644 spec/fixtures/basic_rails_app/config/environments/production.rb create mode 100644 spec/generators/setup/setup_generator_spec.rb diff --git a/.dockerdev/docker-compose.yml b/.dockerdev/docker-compose.yml index 62aa264..159eb4b 100644 --- a/.dockerdev/docker-compose.yml +++ b/.dockerdev/docker-compose.yml @@ -1,16 +1,14 @@ -version: '3.4' +version: '2.4' services: app: image: ruby:2.6 environment: - - GEM_HOME=/bundle - - BUNDLE_PATH=/bundle - HISTFILE=/app/tmp/.bash_history working_dir: /app volumes: - ..:/app - - bundle:/bundle + - gems:/usr/local/bundle volumes: - bundle: + gems: diff --git a/.rubocop.yml b/.rubocop.yml index 6c94423..da5934d 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -67,6 +67,10 @@ Metrics/BlockLength: - 'anycable-rails.gemspec' - 'lib/rails/generators/**/*.rb' +Metrics/ClassLength: + Exclude: + - 'lib/rails/generators/**/*.rb' + Metrics/CyclomaticComplexity: Exclude: - 'spec/**/*.rb' diff --git a/anycable-rails.gemspec b/anycable-rails.gemspec index 5d78d77..17d3e2a 100644 --- a/anycable-rails.gemspec +++ b/anycable-rails.gemspec @@ -30,6 +30,7 @@ Gem::Specification.new do |spec| spec.add_dependency "anycable", "~> 0.6.0" spec.add_dependency "rails", ">= 5" + spec.add_development_dependency "ammeter", "~> 1.1" spec.add_development_dependency "bundler", ">= 1.10" spec.add_development_dependency "pry-byebug" spec.add_development_dependency "rake", "~> 10.0" diff --git a/lib/rails/generators/anycable/setup/docker/docker_generator.rb b/lib/rails/generators/anycable/setup/docker/docker_generator.rb deleted file mode 100644 index 4f13466..0000000 --- a/lib/rails/generators/anycable/setup/docker/docker_generator.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -module AnyCableRailsGenerators - module Setup - # Generator to help set up AnyCable in Docker environment - class DockerGenerator < ::Rails::Generators::Base - namespace "anycable:setup:docker" - - def info - say "Docker development configuration could vary." - say "Here is an example snippet for docker-compose.yml:" - say <<~YML - ───────────────────────────────────────── - anycable-ws: - image: anycable/anycable-go:v0.6.4 - ports: - - '3334:3334' - environment: - PORT: 3334 - REDIS_URL: redis://redis:6379/0 - ANYCABLE_RPC_HOST: anycable-rpc:50051 - depends_on: - - anycable-rpc - - redis - - anycable-rpc: - <<: *backend - command: bundle exec anycable - ports: - - '50051' - ───────────────────────────────────────── - YML - end - end - end -end diff --git a/lib/rails/generators/anycable/setup/local/local_generator.rb b/lib/rails/generators/anycable/setup/local/local_generator.rb deleted file mode 100644 index 02bbcba..0000000 --- a/lib/rails/generators/anycable/setup/local/local_generator.rb +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -module AnyCableRailsGenerators - module Setup - # Generator to help set up AnyCable locally - class LocalGenerator < ::Rails::Generators::Base - SERVER_VERSION = "v0.6.4" - OS_NAMES = %w[linux darwin freebsd win].freeze - CPU_NAMES = %w[amd64 arm64 386 arm].freeze - - namespace "anycable:setup:local" - source_root File.expand_path("../templates", __dir__) - - def server - answer = nil - - until [1, 2, 3].include?(answer.to_i) - answer = ask "How do you want to install AnyCable-Go WebSocket server? (1) Homebrew, (2) Download binary, (3) Skip" - end - - case answer.to_i - when 1 - run "brew install anycable-go", abort_on_failure: true - run "anycable-go -v", abort_on_failure: true - when 2 - return unless download_binary - when 3 - say_status :help, "⚠️ Please, read this guide on how to install AnyCable-Go server 👉 https://docs.anycable.io/#/anycable-go/getting_started", :yellow - return - else - raise ArgumentError, "Unknown answer: #{answer}" - end - - say_status :info, "✅ AnyCable-Go WebSocket server has been successfully installed" - end - - def proc_files - file_name = "Procfile.dev" - - if File.exist?(file_name) - append_file file_name, 'anycable: bundle exec anycable --server-command "anycable-go --port 3334"' - else - say_status :help, "💡 We recommend using Hivemind to manage multiple processes in development 👉 https://github.com/DarthSim/hivemind", :yellow - - template file_name if yes? "Do you want to create a '#{file_name}' file?" - end - end - - private - - def download_binary - out_path = ask "Please, enter the path to download the AnyCable-Go binary to", default: "/usr/local/bin", path: true - file_name = File.join(out_path, "anycable-go") - - sudo = "" - unless File.writable?(out_path) - if yes? "Path is not writable 😕. Do you have sudo privileges?" - sudo = "sudo " - else - say_status :error, "❌ Failed to install AnyCable-Go WebSocket server", :red - return false - end - end - - os_name = OS_NAMES.find(&Gem::Platform.local.os.method(:==)) || - ask("What is your OS name?", limited_to: OS_NAMES) - - cpu_name = CPU_NAMES.find(¤t_cpu.method(:==)) || - ask("What is your CPU architecture?", limited_to: CPU_NAMES) - - run "#{sudo}curl -L https://github.com/anycable/anycable-go/releases/download/#{SERVER_VERSION}/" \ - "anycable-go-#{SERVER_VERSION}-#{os_name}-#{cpu_name} -o #{file_name}", abort_on_failure: true - - run "#{sudo}chmod +x #{file_name}", abort_on_failure: true - run "#{file_name} -v", abort_on_failure: true - - true - end - - def current_cpu - case Gem::Platform.local.cpu - when "x86_64", "x64" - "amd64" - when "x86_32", "x86", "i386", "i486", "i686" - "i386" - when "aarch64", "aarch64_be", /armv8/ - "arm64" - when "arm", /armv7/, /armv6/ - "arm" - else - "unknown" - end - end - end - end -end diff --git a/lib/rails/generators/anycable/setup/setup_generator.rb b/lib/rails/generators/anycable/setup/setup_generator.rb index 544d82c..7bea642 100644 --- a/lib/rails/generators/anycable/setup/setup_generator.rb +++ b/lib/rails/generators/anycable/setup/setup_generator.rb @@ -6,6 +6,35 @@ class SetupGenerator < ::Rails::Generators::Base namespace "anycable:setup" source_root File.expand_path("templates", __dir__) + METHODS = %w[skip local docker].freeze + SERVER_VERSION = "v0.6.4" + OS_NAMES = %w[linux darwin freebsd win].freeze + CPU_NAMES = %w[amd64 arm64 386 arm].freeze + SERVER_SOURCES = %w[skip brew binary].freeze + DEFAULT_BIN_PATH = "/usr/local/bin" + + class_option :method, + type: :string, + desc: "Select your development environment to run particular installation method (options: #{METHODS.join(', ')})" + class_option :source, + type: :string, + desc: "Install AnyCable-Go server from particular source (options: #{SERVER_SOURCES.join(', ')})" + class_option :bin_path, + type: :string, + desc: "Install AnyCable-Go server binary to directory (default: #{DEFAULT_BIN_PATH})" + class_option :os, + type: :string, + desc: "Download AnyCable-Go server binary for particular OS (options: #{OS_NAMES.join(', ')})" + class_option :cpu, + type: :string, + desc: "Download AnyCable-Go server binary for particular CPU architecture (options: #{CPU_NAMES.join(', ')})" + class_option :skip_heroku, + type: :boolean, + desc: "Do not copy Heroku configs" + class_option :skip_procfile_dev, + type: :boolean, + desc: "Do not create Procfile.dev" + def welcome say "👋 Welcome to AnyCable interactive installer." end @@ -37,22 +66,26 @@ def cable_url end def development_method - answer = nil + answer = METHODS.index(options[:method]) || 99 - until [1, 2, 3].include?(answer.to_i) - answer = ask "Which environment do you use for development? (1) Local, (2) Docker, (3) Skip" + until METHODS[answer.to_i] + answer = ask "Which environment do you use for development? (1) Local, (2) Docker, (0) Skip" end - env = [nil, "local", "docker"][answer.to_i] - - return if env.nil? - - require "rails/generators/anycable/setup/#{env.underscore}/#{env.underscore}_generator" - "AnyCableRailsGenerators::Setup::#{env.camelize}Generator".constantize.new.invoke_all + case env = METHODS[answer.to_i] + when "skip" + say_status :help, "⚠️ Please, read this guide on how to install AnyCable-Go server 👉 https://docs.anycable.io/#/anycable-go/getting_started", :yellow + else + send "install_for_#{env}" + end end def heroku - return unless yes? "Do you use Heroku for deployment?" + if options[:skip_heroku].nil? + return unless yes? "Do you use Heroku for deployment?" + elsif options[:skip_heroku] + return + end template "Procfile" inside("bin") { template "heroku-web" } @@ -63,5 +96,140 @@ def heroku def finish say_status :info, "✅ AnyCable has been configured successfully!" end + + private + + def install_for_docker + say_status :help, "️️⚠️ Docker development configuration could vary", :yellow + + say "Here is an example snippet for docker-compose.yml:" + say <<~YML + ───────────────────────────────────────── + anycable-ws: + image: anycable/anycable-go:v0.6.4 + ports: + - '3334:3334' + environment: + PORT: 3334 + REDIS_URL: redis://redis:6379/0 + ANYCABLE_RPC_HOST: anycable-rpc:50051 + depends_on: + - anycable-rpc + - redis + + anycable-rpc: + <<: *backend + command: bundle exec anycable + ports: + - '50051' + ───────────────────────────────────────── + YML + end + + def install_for_local + install_server + template_proc_files + end + + def install_server + answer = SERVER_SOURCES.index(options[:source]) || 99 + + until SERVER_SOURCES[answer.to_i] + answer = ask "How do you want to install AnyCable-Go WebSocket server? (1) Homebrew, (2) Download binary, (0) Skip" + end + + case answer.to_i + when 0 + say_status :help, "⚠️ Please, read this guide on how to install AnyCable-Go server 👉 https://docs.anycable.io/#/anycable-go/getting_started", :yellow + return + else + return unless send("install_from_#{SERVER_SOURCES[answer.to_i]}") + end + + say_status :info, "✅ AnyCable-Go WebSocket server has been successfully installed" + end + + def template_proc_files + file_name = "Procfile.dev" + + if File.exist?(file_name) + append_file file_name, 'anycable: bundle exec anycable --server-command "anycable-go --port 3334"' + else + say_status :help, "💡 We recommend using Hivemind to manage multiple processes in development 👉 https://github.com/DarthSim/hivemind", :yellow + + if options[:skip_procfile_dev].nil? + return unless yes? "Do you want to create a '#{file_name}' file?" + elsif options[:skip_procfile_dev] + return + end + + template file_name + end + end + + def install_from_brew + run "brew install anycable-go", abort_on_failure: true + run "anycable-go -v", abort_on_failure: true + end + + def install_from_binary + out = options[:bin_path] if options[:bin_path] + out ||= "/usr/local/bin" if options[:method] # User don't want interactive mode + out ||= ask "Please, enter the path to download the AnyCable-Go binary to", default: DEFAULT_BIN_PATH, path: true + + os_name = OS_NAMES.find(&Gem::Platform.local.os.method(:==)) || + options[:os] || + ask("What is your OS name?", limited_to: OS_NAMES) + + cpu_name = CPU_NAMES.find(¤t_cpu.method(:==)) || + options[:cpu] || + ask("What is your CPU architecture?", limited_to: CPU_NAMES) + + download_exe( + "https://github.com/anycable/anycable-go/releases/download/#{SERVER_VERSION}/" \ + "anycable-go-#{SERVER_VERSION}-#{os_name}-#{cpu_name}", + to: out, + file_name: "anycable-go" + ) + + true + end + + def download_exe(url, to:, file_name:) + file_path = File.join(to, file_name) + + run "#{sudo(to)}curl -L #{url} -o #{file_path}", abort_on_failure: true + run "#{sudo(to)}chmod +x #{file_path}", abort_on_failure: true + run "#{file_path} -v", abort_on_failure: true + end + + def sudo!(path) + sudo = "" + unless File.writable?(path) + if yes? "Path is not writable 😕. Do you have sudo privileges?" + sudo = "sudo " + else + say_status :error, "❌ Failed to install AnyCable-Go WebSocket server", :red + raise StandardError, "Path #{path} is not writable!" + end + end + + sudo + end + + def current_cpu + case Gem::Platform.local.cpu + when "x86_64", "x64" + "amd64" + when "x86_32", "x86", "i386", "i486", "i686" + "i386" + when "aarch64", "aarch64_be", /armv8/ + "arm64" + when "arm", /armv7/, /armv6/ + "arm" + else + "unknown" + end + end end end diff --git a/spec/fixtures/basic_rails_app/Procfile.dev b/spec/fixtures/basic_rails_app/Procfile.dev new file mode 100644 index 0000000..432a2fd --- /dev/null +++ b/spec/fixtures/basic_rails_app/Procfile.dev @@ -0,0 +1,3 @@ +server: bin/rails server +assets: bin/webpack-dev-server +anycable: bundle exec anycable --server-command "anycable-go --port 3334" diff --git a/spec/fixtures/basic_rails_app/config/environments/development.rb b/spec/fixtures/basic_rails_app/config/environments/development.rb new file mode 100644 index 0000000..cc529e4 --- /dev/null +++ b/spec/fixtures/basic_rails_app/config/environments/development.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +Rails.application.configure do + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false +end diff --git a/spec/fixtures/basic_rails_app/config/environments/production.rb b/spec/fixtures/basic_rails_app/config/environments/production.rb new file mode 100644 index 0000000..265088f --- /dev/null +++ b/spec/fixtures/basic_rails_app/config/environments/production.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + config.active_record.dump_schema_after_migration = false +end diff --git a/spec/generators/setup/setup_generator_spec.rb b/spec/generators/setup/setup_generator_spec.rb new file mode 100644 index 0000000..921b5f2 --- /dev/null +++ b/spec/generators/setup/setup_generator_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require "spec_helper" +require_relative "../../../lib/rails/generators/anycable/setup/setup_generator" + +describe AnyCableRailsGenerators::SetupGenerator, type: :generator do + destination File.expand_path("../../../tmp/basic_rails_app", __dir__) + + let(:gen) { generator } + let(:base_root) { File.expand_path("../../../tmp/basic_rails_app", __dir__) } + let(:removed_files) { [] } + + before do + prepare_destination + + FileUtils.cp_r File.expand_path("../../fixtures/basic_rails_app", __dir__), + File.expand_path("../../../tmp", __dir__) + + FileUtils.rm(removed_files.map { |f| File.join(base_root, f) }) if removed_files.any? + end + + context "when skip install environment" do + before { run_generator %w[--method skip --skip-heroku] } + + it "copies config files" do + expect(file("config/cable.yml")).to exist + expect(file("config/anycable.yml")).to exist + end + + it "patch environment configs" do + expect(file("config/environments/development.rb")) + .to contain('config.action_cable.url = ENV.fetch("CABLE_URL", "ws://localhost:3334/cable").presence') + + expect(file("config/environments/production.rb")) + .to contain('config.action_cable.url = ENV["CABLE_URL"].presence') + end + end + + context "when docker environment" do + it "shows a Docker Compose snippet" do + gen = generator(%w[--method docker --skip-heroku]) + expect(gen).to receive(:install_for_docker) + silence_stream(STDOUT) { gen.invoke_all } + end + end + + context "when local environment" do + context "when do not install the server" do + before { run_generator %w[--method local --source skip --skip-heroku --skip-procfile-dev false] } + + context "when Procfile.dev exists" do + it "patches" do + expect(file("Procfile.dev")) + .to contain('anycable: bundle exec anycable --server-command "anycable-go --port 3334"') + end + end + + context "when Procfile.dev absents" do + let(:removed_files) { %w[Procfile.dev] } + + it "creates" do + expect(file("Procfile.dev")) + .to contain('anycable: bundle exec anycable --server-command "anycable-go --port 3334"') + end + end + end + + context "when downloading binary" do + it "runs curl with valid url" do + gen = generator(%w[--method local --source binary --os linux --cpu amd64 --skip-heroku --skip-procfile-dev false]) + expect(gen) + .to receive(:download_exe).with(/releases\/download\/v\d+\.\d+\.\d+\/anycable-go-v\d+\.\d+\.\d+-linux-amd64/, + to: "/usr/local/bin", + file_name: "anycable-go") + silence_stream(STDOUT) { gen.invoke_all } + end + end + + context "when installin from Homebrew" do + it "runs commands" do + gen = generator(%w[--method local --source brew --skip-heroku --skip-procfile-dev false]) + expect(gen).to receive(:install_from_brew) + silence_stream(STDOUT) { gen.invoke_all } + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 324c389..514938c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,9 +6,14 @@ Rails.application.eager_load! +require "active_support/testing/stream" +require "ammeter/init" + Dir["#{__dir__}/support/**/*.rb"].each { |f| require f } RSpec.configure do |config| + include ActiveSupport::Testing::Stream + config.after(:each) do ApplicationCable::Connection.events_log.clear User.delete_all