diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 06896ae..9a226b5 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -36,7 +36,7 @@ Metrics/MethodLength: # Offense count: 1 # Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: - Max: 114 + Max: 124 # Offense count: 1 # Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms. diff --git a/lib/morph-cli.rb b/lib/morph-cli.rb index 2f9d4ab..6a22d81 100644 --- a/lib/morph-cli.rb +++ b/lib/morph-cli.rb @@ -31,6 +31,15 @@ def self.execute(directory, _development, env_config, skip_data: false) file = MorphCLI.create_tar(directory, all_paths) + scraper_output = run(file, env_config) + + puts "Scraper didn't output anything, but it ran successfully." unless scraper_output + end + + # Uploads the code to the server, streams the run output to the local + # stdout/stderr and returns whether the scraper itself wrote anything + # to stdout or stderr + def self.run(file, env_config) connection = Faraday.new(url: env_config[:base_url]) do |f| f.request :multipart f.response :raise_error @@ -38,6 +47,7 @@ def self.execute(directory, _development, env_config, skip_data: false) end buffer = +"" + scraper_output = false connection.post("/run") do |req| req.body = { api_key: env_config[:api_key], @@ -52,12 +62,18 @@ def self.execute(directory, _development, env_config, skip_data: false) before, match, after = chunk.rpartition("\n") buffer << before << match - buffer.split("\n").each { |l| log(l) } + buffer.split("\n").each do |l| + stream = log(l) + scraper_output = true if %w[stdout stderr].include?(stream) + end buffer = after end end + scraper_output end + # Writes the line to the local stdout/stderr and returns the name of the + # stream it came from def self.log(line) return if line.empty? @@ -72,6 +88,7 @@ def self.log(line) end s.puts a["text"] + a["stream"] end def self.config_path diff --git a/spec/morph_cli_spec.rb b/spec/morph_cli_spec.rb index 392f438..93b7e26 100644 --- a/spec/morph_cli_spec.rb +++ b/spec/morph_cli_spec.rb @@ -1,5 +1,7 @@ require "fileutils" require "tmpdir" +require "stringio" +require "zlib" require "spec_helper" @@ -20,6 +22,23 @@ def with_scraper_directory end end + # Extracts tar entry names from a multipart request body that contains + # a gzip-compressed tar as one of its parts. + def tar_entry_names(body) + binary = body.b + gzip_start = binary.index("\x1f\x8b".b) + return [] unless gzip_start + + gzip_io = StringIO.new(binary[gzip_start..]) + names = [] + Zlib::GzipReader.wrap(gzip_io) do |gz| + Minitar::Input.open(gz) { |tar| tar.each { |entry| names << entry.full_name } } + end + names + rescue StandardError + [] + end + it "uploads the scraper and streams the run output to stdout" do stub_request(:post, "https://morph.io/run") .to_return(status: 200, body: %({"stream":"stdout","text":"hello from morph"}\n)) @@ -30,6 +49,38 @@ def with_scraper_directory end end + it "tells the user the run succeeded when the scraper produces no output" do + stub_request(:post, "https://morph.io/run") + .to_return(status: 200, body: %({"stream":"internalout","text":"Injecting configuration"}\n)) + + with_scraper_directory do |dir| + expect { described_class.execute(dir, false, env_config) } + .to output(/Scraper didn't output anything, but it ran successfully\./).to_stdout + end + end + + it "doesn't add a message when the scraper writes to stdout" do + stub_request(:post, "https://morph.io/run") + .to_return(status: 200, body: %({"stream":"stdout","text":"hello from morph"}\n)) + + with_scraper_directory do |dir| + expect { described_class.execute(dir, false, env_config) } + .not_to output(/ran successfully/).to_stdout + end + end + + it "doesn't add a message when the scraper writes to stderr" do + stub_request(:post, "https://morph.io/run") + .to_return(status: 200, body: %({"stream":"stderr","text":"oops"}\n)) + + with_scraper_directory do |dir| + expect do + expect { described_class.execute(dir, false, env_config) } + .not_to output(/ran successfully/).to_stdout + end.to output("oops\n").to_stderr + end + end + it "posts the API key and the gzipped code as multipart form data" do stub_request(:post, "https://morph.io/run").to_return(status: 200, body: "") @@ -52,11 +103,11 @@ def with_scraper_directory File.write(File.join(dir, "data.sqlite"), "sqlite data") expect { described_class.execute(dir, false, env_config) } - .to output(/\AUploading 21\.00 B \(including data\.sqlite\)\.\.\.\n\z/).to_stdout + .to output(/\AUploading 21\.00 B \(including data\.sqlite\)\.\.\.\n/).to_stdout end expect(WebMock).to(have_requested(:post, "https://morph.io/run").with do |req| - req.body.include?("data.sqlite") + tar_entry_names(req.body).include?("data.sqlite") end) end @@ -67,11 +118,11 @@ def with_scraper_directory File.write(File.join(dir, "data.sqlite"), "sqlite data") expect { described_class.execute(dir, false, env_config, skip_data: true) } - .to output(/\AUploading 10\.00 B\.\.\.\n\z/).to_stdout + .to output(/\AUploading 10\.00 B\.\.\.\n/).to_stdout end expect(WebMock).to(have_requested(:post, "https://morph.io/run").with do |req| - !req.body.include?("data.sqlite") + !tar_entry_names(req.body).include?("data.sqlite") end) end