diff --git a/Pavel_Borisov/2/.gitignore b/Pavel_Borisov/2/.gitignore new file mode 100644 index 00000000..fb6d573a --- /dev/null +++ b/Pavel_Borisov/2/.gitignore @@ -0,0 +1,3 @@ +*.sublime-project +*.sublime-workspace + diff --git a/Pavel_Borisov/2/checker.rb b/Pavel_Borisov/2/checker.rb new file mode 100644 index 00000000..fe8c5a6f --- /dev/null +++ b/Pavel_Borisov/2/checker.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require_relative 'lib/machinery' + +command_line = CliParser.new do |opts| + opts.banner = "Usage: #{__FILE__} [options] FILENAME" + opts.separator '' + opts.separator 'Available options:' + opts.on('--no-subdomains', 'Exclude entries with subdomains', TrueClass) + opts.on('--filter=WORD', 'Filter out results containing WORD in page body', String) + opts.on('--exclude-solutions', 'Exclude common open-source solutions', TrueClass) +end + +command_line.parse! + +domain_list_filename = command_line.args.first + +if domain_list_filename.nil? || !File.exist?(domain_list_filename) + print command_line.usage + exit +end + +domains = DomainsList.new(domain_list_filename, command_line.options) + +domains.process! do |percent, domain| + print "\e[1000D\e[0K#{percent}% complete – now checking #{domain}" +end +print "\e[1000D\e[0K" + +domains.results.each { |r| puts r } +puts '' +puts domains.stats diff --git a/Pavel_Borisov/2/lib/machinery.rb b/Pavel_Borisov/2/lib/machinery.rb new file mode 100644 index 00000000..2dd3a92e --- /dev/null +++ b/Pavel_Borisov/2/lib/machinery.rb @@ -0,0 +1,145 @@ +# frozen_string_literal: true + +require 'optparse' +require 'csv' +require 'net/http' +require 'nokogiri' + +# Uses OptionParser to collect options into a hash +class CliParser + attr_reader :options, :args + + def initialize(&block) + @options = {} + @args = [] + @opt_parser = OptionParser.new(&block) + end + + def parse! + @opt_parser.parse!(into: @options) + @args = @opt_parser.instance_variable_get(:@default_argv) + end + + def to_s + @opt_parser.to_s + end + + def usage + to_s + end +end + +# Runs check for a single domain and contains the result +class DomainChecker + attr_reader :domain, :code, :response_time, :body, :status + + def initialize(domain) + @domain = domain + @status = :unchecked + end + + def check! + begin + timed_http_request + rescue StandardError => e + @status = :errored + cause = ('Timeout' if e.is_a? Timeout::Error) || e.cause + @error_message = "ERROR (#{cause})" + end + self + end + + def result + case @status + when :got_response + "#{@domain} - #{@code} (#{@response_time})" + when :errored + "#{@domain} - #{@error_message}" + when :unchecked + "#{@domain} - hasn't been checked" + end + end + + private + + def timed_http_request + http = Net::HTTP.new(@domain) + http.open_timeout = 1 + http.read_timeout = 1 + start_time = Time.now.utc + response = http.get('/') + end_time = Time.now.utc + @response_time = "#{((end_time - start_time) * 1000).round}ms" + @code = response.code.to_i + @body = response.body + @status = :got_response + end +end + +# Reads domains list from a file, filters the list according to the supplied options. +# Checks the domains using DomainChecker, filters the results if --filter option is specified. +# Provides methods to format results and stats for output. +class DomainsList + attr_reader :list + + def initialize(filename, options) + @initial_list = CSV.read(filename).map(&:first) + + reject_subdomains! if options[:'no-subdomains'] + reject_solutions! if options[:'exclude-solutions'] + + @filtered_word = options[:filter] + @list = @initial_list.map { |domain| DomainChecker.new(domain) } + end + + def process! + total_count = @list.count + processed_count = 0 + @list.each do |item| + item.check! + processed_count += 1 + completion_percentage = (processed_count / total_count.to_f * 100).to_i + yield(completion_percentage, item.domain) if block_given? + end + keep_results_with_word!(@filtered_word) if @filtered_word + self + end + + def results + @list.map(&:result) + end + + def stats + total = @list.count + errored = @list.count { |item| item.status == :errored } + + success = @list.count do |item| + item.status == :got_response && (200..399).cover?(item.code) + end + + failed = @list.count do |item| + item.status == :got_response && (400..599).cover?(item.code) + end + + "Total: #{total}, Success: #{success}, Failed: #{failed}, Errored: #{errored}" + end + + private + + def reject_subdomains! + @initial_list.filter! { |domain| domain.count('.') == 1 } + end + + def reject_solutions! + @initial_list.filter! do |domain| + !domain.match(/(\bgitlab\b|\bredmine\b|\bgit\b|\brepo\b|\brm\b|\bsource\b|\bsvn\b)/) + end + end + + def keep_results_with_word!(word) + @list.keep_if do |response| + body_text = Nokogiri::HTML.parse(response.body).css('body').text + body_text.downcase.match? word.downcase + end + end +end diff --git a/Pavel_Borisov/2/test/fixtures/domains.csv b/Pavel_Borisov/2/test/fixtures/domains.csv new file mode 100644 index 00000000..7ae5bf43 --- /dev/null +++ b/Pavel_Borisov/2/test/fixtures/domains.csv @@ -0,0 +1,3 @@ +example.com +subdomain.example.com +gitlab.com diff --git a/Pavel_Borisov/2/test/machinery_test.rb b/Pavel_Borisov/2/test/machinery_test.rb new file mode 100644 index 00000000..6eee4977 --- /dev/null +++ b/Pavel_Borisov/2/test/machinery_test.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'minitest/autorun' +require_relative '../lib/machinery' + +class DomainCheckerTest < Minitest::Test + def test_initial_status_is_unchecked + assert_equal :unchecked, DomainChecker.new('example.com').status + end + + def test_unchecked_result_is_correct + assert_equal "example.com - hasn't been checked", + DomainChecker.new('example.com').result + end + + def test_valid_domain_checked_status_is_got_response + assert_equal :got_response, DomainChecker.new('example.com').check!.status + end + + def test_valid_domain_checked_result_is_correct + assert_match(/example.com - 200 \(\d+ms\)/, + DomainChecker.new('example.com').check!.result) + end + + def test_invalid_domain_checked_status_is_errored + assert_equal :errored, DomainChecker.new('example.com.invld').check!.status + end + + def test_invalid_domain_checked_result_is_correct + assert_match(/example.com.invld - ERROR/, + DomainChecker.new('example.com.invld').check!.result) + end +end + +class DomainsListTest < Minitest::Test + TEST_DOMAINS_FILE = "#{File.dirname(__FILE__)}/fixtures/domains.csv" + + def test_csv_file_is_read_correctly + assert_equal ['example.com', 'subdomain.example.com', 'gitlab.com'], + DomainsList.new(TEST_DOMAINS_FILE, {}).list.map(&:domain) + end + + def test_no_subdomains_option_rejects_subdomains + assert_equal ['example.com', 'gitlab.com'], + DomainsList.new(TEST_DOMAINS_FILE, { 'no-subdomains': true }).list.map(&:domain) + end + + def test_exclude_solutions_rejects_gitlab + assert_equal ['example.com', 'subdomain.example.com'], + DomainsList.new(TEST_DOMAINS_FILE, { 'exclude-solutions': true }).list.map(&:domain) + end + + def test_filter_word_keeps_only_results_with_word_in_body + assert_equal ['example.com'], + DomainsList.new(TEST_DOMAINS_FILE, { filter: 'example' }).process!.list.map(&:domain) + end +end