From 34647ea562544c3b1d1b7a8d3d79972b42fe74a6 Mon Sep 17 00:00:00 2001 From: BugDiver Date: Thu, 11 Jan 2018 13:59:44 +0530 Subject: [PATCH] parse source files statically getgauge/gauge-vscode#85 #47 * parsing source files statically to build method cahce * reloading steps on cache file request * executing source only for execution phase * removed step_text_map and recoverable_steps_map. step holds required info --- lib/code_parser.rb | 10 +-- lib/executor.rb | 13 ++-- lib/gauge.rb | 33 +++++----- lib/gauge_runtime.rb | 9 +-- lib/message_processor.rb | 2 + lib/method_cache.rb | 62 +++++++++++-------- lib/processors/cache_file_processor.rb | 38 ++++++++++++ .../execute_step_request_processor.rb | 2 +- lib/processors/execution_hook_processors.rb | 5 +- .../refactor_step_request_processor.rb | 6 +- .../step_validation_request_processor.rb | 11 ++-- lib/static_loader.rb | 59 ++++++++++++++++++ spec/code_parser_spec.rb | 3 +- spec/method_cache_spec.rb | 5 +- spec/refactor_processor_spec.rb | 8 +-- spec/static_loader_spec.rb | 58 +++++++++++++++++ spec/validate_processor_spec.rb | 8 +-- 17 files changed, 253 insertions(+), 79 deletions(-) create mode 100644 lib/processors/cache_file_processor.rb create mode 100644 lib/static_loader.rb create mode 100644 spec/static_loader_spec.rb diff --git a/lib/code_parser.rb b/lib/code_parser.rb index 1ff63d6..18c232e 100644 --- a/lib/code_parser.rb +++ b/lib/code_parser.rb @@ -1,4 +1,4 @@ -# Copyright 2015 ThoughtWorks, Inc. +# Copyright 2018 ThoughtWorks, Inc. # # This file is part of Gauge-Ruby. # @@ -46,17 +46,17 @@ def self.refactor_args(code, param_positions, new_param_values, new_step_text) } buffer = Parser::Source::Buffer.new '(rewriter)' buffer.source=code - + ast = code_to_ast(code) new_params_string = "|#{new_params.join(', ')}|".gsub("||", "") # no params = empty string - + rewriter = Parser::Source::Rewriter.new(buffer) .replace(ast.children[0].location.expression, "step '#{new_step_text}'") - + # hack, could not find an easy way to manipulate the ast to include arguments, when none existed originally. # it's just easy to add arguments via string substitution. return include_args(rewriter.process, new_params_string) if ast.children[1].location.expression.nil? - + #insert new arguments rewriter.replace(ast.children[1].location.expression, new_params_string).process end diff --git a/lib/executor.rb b/lib/executor.rb index 062a560..3d734cc 100644 --- a/lib/executor.rb +++ b/lib/executor.rb @@ -1,4 +1,4 @@ -# Copyright 2015 ThoughtWorks, Inc. +# Copyright 2018 ThoughtWorks, Inc. # This file is part of Gauge-Ruby. @@ -21,21 +21,22 @@ module Gauge # @api private module Executor def self.load_steps(steps_implementation_dir) - Dir["#{steps_implementation_dir}/**/*.rb"].each { |x| + Dir["#{steps_implementation_dir}/**/*.rb"].each do |x| begin + ENV['GAUGE_STEP_FILE'] = x require x rescue Exception => e puts "[ERROR] Cannot import #{x}. Reason: #{e.message}" end - } + end end def self.execute_step(step, args) - block = MethodCache.get_step step + si = MethodCache.get_step_info step if args.size == 1 - block.call(args[0]) + si[:block].call(args[0]) else - block.call(args) + si[:block].call(args) end end diff --git a/lib/gauge.rb b/lib/gauge.rb index 2a95720..0a492ed 100644 --- a/lib/gauge.rb +++ b/lib/gauge.rb @@ -1,4 +1,4 @@ -# Copyright 2015 ThoughtWorks, Inc. +# Copyright 2018 ThoughtWorks, Inc. # This file is part of Gauge-Ruby. @@ -32,7 +32,7 @@ class << self # puts "I am the $1 hook" # end def hook(hook) - define_method hook do |options={}, &block| + define_method hook do |options = {}, &block| Gauge::MethodCache.send("add_#{hook}_hook".to_sym, options, &block) end end @@ -104,31 +104,32 @@ def tagged_hook(hook) # @param block [block] the implementation block for given step. def step(*args, &block) opts = args.select {|x| x.is_a? Hash} - step_texts = args-opts - opts = {:continue_on_failure => false}.merge opts.reduce({}, :merge) + step_texts = args - opts + opts = { continue_on_failure: false }.merge opts.reduce({}, :merge) step_texts.each do |text| - parameterized_step_text = Gauge::Connector.step_value(text) - Gauge::MethodCache.add_step(parameterized_step_text, &block) - Gauge::MethodCache.add_step_text(parameterized_step_text, text) - Gauge::MethodCache.set_recoverable(parameterized_step_text) if opts[:continue_on_failure] + step_value = Gauge::Connector.step_value(text) + si = { location: { file: ENV['GAUGE_STEP_FILE'], span: {} }, + block: block, step_text: text, + recoverable: opts[:continue_on_failure] } + Gauge::MethodCache.add_step(step_value, si) end Gauge::MethodCache.add_step_alias(*step_texts) end # Invoked before execution of every step. - tagged_hook "before_step" + tagged_hook 'before_step' # Invoked after execution of every step. - tagged_hook "after_step" + tagged_hook 'after_step' # Invoked before execution of every specification. - tagged_hook "before_spec" + tagged_hook 'before_spec' # Invoked after execution of every specification. - tagged_hook "after_spec" + tagged_hook 'after_spec' # Invoked before execution of every scenario. - tagged_hook "before_scenario" + tagged_hook 'before_scenario' # Invoked after execution of every scenario. - tagged_hook "after_scenario" + tagged_hook 'after_scenario' # Invoked before execution of the entire suite. - hook "before_suite" + hook 'before_suite' # Invoked after execution of the entire suite. - hook "after_suite" + hook 'after_suite' end diff --git a/lib/gauge_runtime.rb b/lib/gauge_runtime.rb index 3425813..075a271 100644 --- a/lib/gauge_runtime.rb +++ b/lib/gauge_runtime.rb @@ -1,4 +1,4 @@ -# Copyright 2015 ThoughtWorks, Inc. +# Copyright 2018 ThoughtWorks, Inc. # This file is part of Gauge-Ruby. @@ -20,6 +20,7 @@ require_relative 'messages.pb' require_relative 'executor' +require_relative 'static_loader' require_relative 'connector' require_relative 'message_processor' @@ -27,7 +28,7 @@ module Gauge # @api private module Runtime - DEFAULT_IMPLEMENTATIONS_DIR_PATH = File.join(Dir.pwd, 'step_implementations') + DEFAULT_IMPLEMENTATIONS_DIR_PATH = File.join(ENV["GAUGE_PROJECT_ROOT"], 'step_implementations') def self.dispatch_messages(socket) while (!socket.eof?) @@ -51,7 +52,7 @@ def self.handle_message(socket, message) write_message(socket, message) else response = MessageProcessor.process_message message - write_message(socket, response) + write_message(socket, response) if response end end @@ -72,7 +73,7 @@ def self.portFromEnvVariable(envVariable) STDOUT.sync = true Connector.make_connections() - Executor.load_steps(DEFAULT_IMPLEMENTATIONS_DIR_PATH) + StaticLoader.load_files(DEFAULT_IMPLEMENTATIONS_DIR_PATH) dispatch_messages(Connector.executionSocket) exit(0) end diff --git a/lib/message_processor.rb b/lib/message_processor.rb index 9ba1934..0e937ba 100644 --- a/lib/message_processor.rb +++ b/lib/message_processor.rb @@ -43,6 +43,8 @@ class MessageProcessor @processors[Messages::Message::MessageType::ScenarioDataStoreInit] = method(:process_datastore_init) @processors[Messages::Message::MessageType::StepNameRequest] = method(:process_step_name_request) @processors[Messages::Message::MessageType::RefactorRequest] = method(:refactor_step) + @processors[Messages::Message::MessageType::CacheFileRequest] = method(:process_cache_file_request) + # @processors[Messages::Message::MessageType::StepPositionsRequest] = method(:process_step_positions_request) def self.is_valid_message(message) return @processors.has_key? message.messageType diff --git a/lib/method_cache.rb b/lib/method_cache.rb index 172d5d6..213215e 100644 --- a/lib/method_cache.rb +++ b/lib/method_cache.rb @@ -1,4 +1,4 @@ -# Copyright 2015 ThoughtWorks, Inc. +# Copyright 2018 ThoughtWorks, Inc. # This file is part of Gauge-Ruby. @@ -19,39 +19,44 @@ module Gauge # @api private class MethodCache - ["before_step", "after_step", "before_spec", "after_spec", "before_scenario", "after_scenario", "before_suite", "after_suite"].each { |hook| - define_singleton_method "add_#{hook}_hook" do |options={}, &block| + %w[before_step after_step before_spec after_spec before_scenario after_scenario before_suite after_suite].each { |hook| + define_singleton_method "add_#{hook}_hook" do |options = {}, &block| options = {operator: "AND", tags: []}.merge options - self.class_variable_get("@@#{hook}_hooks").push :block=> block, :options=> options + class_variable_get("@@#{hook}_hooks").push :block => block, :options => options end define_singleton_method "get_#{hook}_hooks" do - self.class_variable_get("@@#{hook}_hooks") + class_variable_get("@@#{hook}_hooks") end } def self.clear_hooks(hook) - self.class_variable_get("@@#{hook}_hooks").clear + class_variable_get("@@#{hook}_hooks").clear end - def self.add_step(parameterized_step_text, &block) - @@steps_map[parameterized_step_text] = @@steps_map[parameterized_step_text] || [] - @@steps_map[parameterized_step_text].push(block) + def self.clear() + class_variable_get("@@steps_map").clear end - def self.get_step(parameterized_step_text) - @@steps_map[parameterized_step_text][0] + def self.add_step(step_value, step_info) + if @@steps_map.key? step_value + @@steps_map[step_value][:locations].push(step_info[:location]) + else + @@steps_map[step_value] = { + locations: [step_info[:location]], + block: step_info[:block], + step_text: step_info[:step_text], + recoverable: step_info[:recoverable] + } + end end - def self.get_steps(parameterized_step_text) - @@steps_map[parameterized_step_text] || [] + def self.get_step_info(step_value) + @@steps_map[step_value] end - def self.add_step_text(parameterized_step_text, step_text) - @@steps_text_map[parameterized_step_text] = step_text - end - def self.get_step_text(parameterized_step_text) - @@steps_text_map[parameterized_step_text] + def self.get_step_text(step_value) + @@steps_map[step_value][:step_text] end def self.add_step_alias(*step_texts) @@ -63,26 +68,31 @@ def self.has_alias?(step_text) end def self.valid_step?(step) - @@steps_map.has_key? step + @@steps_map.key? step end def self.all_steps - @@steps_text_map.values + @@steps_map.values.map { |si| si[:step_text] } end - def self.is_recoverable?(parameterized_step_text) - @@recoverable_steps.include? parameterized_step_text + def self.recoverable?(step_value) + @@steps_map[step_value][:recoverable] + end + + def self.remove_steps(file) + @@steps_map.each_pair do |step, info| + l = info[:locations].reject { |loc| loc[:file] == file } + l.empty? ? @@steps_map.delete(step) : @@steps_map[step][:locations] = l + end end - def self.set_recoverable(parameterized_step_text) - @@recoverable_steps.push parameterized_step_text + def self.multiple_implementation?(step_value) + @@steps_map[step_value][:locations].length > 1 end private @@steps_map = Hash.new - @@steps_text_map = Hash.new @@steps_with_aliases = [] - @@recoverable_steps = [] @@before_suite_hooks = [] @@after_suite_hooks = [] @@before_spec_hooks = [] diff --git a/lib/processors/cache_file_processor.rb b/lib/processors/cache_file_processor.rb new file mode 100644 index 0000000..bfcf9f2 --- /dev/null +++ b/lib/processors/cache_file_processor.rb @@ -0,0 +1,38 @@ +# Copyright 2018 ThoughtWorks, Inc. + +# This file is part of Gauge-Ruby. + +# Gauge-Ruby is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# Gauge-Ruby is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with Gauge-Ruby. If not, see . + +require_relative '../static_loader' +require_relative '../method_cache' +module Gauge + # @api private + module Processors + def process_cache_file_request(message) + if !message.cacheFileRequest.isClosed + ast = CodeParser.code_to_ast(message.cacheFileRequest.content) + StaticLoader.reload_steps(message.cacheFileRequest.filePath, ast) + else + load_from_disk message.cacheFileRequest.filePath + end + nil + end + + def load_from_disk(f) + ast = CodeParser.code_to_ast File.read(f) + File.file? f ? StaticLoader.reload_steps(f, ast) : StaticLoader.remove_steps(f) + end + end +end \ No newline at end of file diff --git a/lib/processors/execute_step_request_processor.rb b/lib/processors/execute_step_request_processor.rb index 581e2a8..184d868 100644 --- a/lib/processors/execute_step_request_processor.rb +++ b/lib/processors/execute_step_request_processor.rb @@ -29,7 +29,7 @@ def process_execute_step_request(message) begin Executor.execute_step step_text, args rescue Exception => e - return handle_failure message, e, time_elapsed_since(start_time), MethodCache.is_recoverable?(step_text) + return handle_failure message, e, time_elapsed_since(start_time), MethodCache.recoverable?(step_text) end handle_pass message, time_elapsed_since(start_time) end diff --git a/lib/processors/execution_hook_processors.rb b/lib/processors/execution_hook_processors.rb index ac249ad..fa4884f 100644 --- a/lib/processors/execution_hook_processors.rb +++ b/lib/processors/execution_hook_processors.rb @@ -17,12 +17,15 @@ require_relative "execution_handler" require_relative "../gauge_messages" - +require_relative "../executor" +require_relative "../method_cache" module Gauge module Processors include ExecutionHandler def process_execution_start_request(message) + Gauge::MethodCache.clear + Executor.load_steps(File.join(ENV["GAUGE_PROJECT_ROOT"], 'step_implementations')) handle_hooks_execution(MethodCache.get_before_suite_hooks, message, message.executionStartingRequest.currentExecutionInfo,false) end diff --git a/lib/processors/refactor_step_request_processor.rb b/lib/processors/refactor_step_request_processor.rb index 2ae78d9..1a79c55 100644 --- a/lib/processors/refactor_step_request_processor.rb +++ b/lib/processors/refactor_step_request_processor.rb @@ -36,9 +36,9 @@ def refactor_step(message) end def get_step(step_text) - blocks = MethodCache.get_steps step_text - raise "Multiple step implementations found for => '#{step_text}'" if blocks.length > 1 - blocks[0] + md = MethodCache.multiple_implementation? step_text + raise "Multiple step implementations found for => '#{step_text}'" if md + MethodCache.get_step_info(step_text)[:block] end end end \ No newline at end of file diff --git a/lib/processors/step_validation_request_processor.rb b/lib/processors/step_validation_request_processor.rb index c20301a..a390807 100644 --- a/lib/processors/step_validation_request_processor.rb +++ b/lib/processors/step_validation_request_processor.rb @@ -22,15 +22,14 @@ def process_step_validation_request(message) is_valid = true msg = '' err_type = nil - blocks = MethodCache.get_steps(step_validate_request.stepText) - if blocks.length > 1 - is_valid = false - msg = "Multiple step implementations found for => '#{step_validate_request.stepText}'" - err_type = Messages::StepValidateResponse::ErrorType::DUPLICATE_STEP_IMPLEMENTATION - elsif blocks.length == 0 + if !MethodCache.valid_step? step_validate_request.stepText is_valid = false msg = 'Step implementation not found' err_type = Messages::StepValidateResponse::ErrorType::STEP_IMPLEMENTATION_NOT_FOUND + elsif MethodCache.multiple_implementation?(step_validate_request.stepText) + is_valid = false + msg = "Multiple step implementations found for => '#{step_validate_request.stepText}'" + err_type = Messages::StepValidateResponse::ErrorType::DUPLICATE_STEP_IMPLEMENTATION end step_validate_response = Messages::StepValidateResponse.new(:isValid => is_valid, :errorMessage => msg, :errorType => err_type) Messages::Message.new(:messageType => Messages::Message::MessageType::StepValidateResponse, diff --git a/lib/static_loader.rb b/lib/static_loader.rb new file mode 100644 index 0000000..2de88c7 --- /dev/null +++ b/lib/static_loader.rb @@ -0,0 +1,59 @@ +# Copyright 2018 ThoughtWorks, Inc. + +# This file is part of Gauge-Ruby. + +# Gauge-Ruby is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# Gauge-Ruby is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with Gauge-Ruby. If not, see . + +require_relative 'gauge' +require_relative 'code_parser' +require_relative 'method_cache' + +module Gauge + # @api private + module StaticLoader + def self.load_files(dir) + Dir["#{dir}/**/*.rb"].each do |x| + ast = CodeParser.code_to_ast File.read x + load_steps(x, ast) if ast + end + end + + def self.load_steps(file, ast) + ast.children.each do |node| + process_node(file, node) if step_node? node + end + end + + def self.reload_steps(file, ast) + return unless ast + remove_steps file + load_steps(file, ast) + end + + def self.remove_steps(file) + Gauge::MethodCache.remove_steps file + end + + def self.step_node?(node) + node.type == :block && node.children[0].children.size > 2 && node.children[0].children[1] == :step + end + + def self.process_node(file, node) + step_text = node.children[0].children[2].children[0] + step_value = Gauge::Connector.step_value step_text + si = {location: {file: file, span: node.loc}, step_text: step_text} + Gauge::MethodCache.add_step(step_value, si) + end + end +end \ No newline at end of file diff --git a/spec/code_parser_spec.rb b/spec/code_parser_spec.rb index c80053e..be1da5b 100644 --- a/spec/code_parser_spec.rb +++ b/spec/code_parser_spec.rb @@ -1,4 +1,4 @@ -# Copyright 2015 ThoughtWorks, Inc. +# Copyright 2018 ThoughtWorks, Inc. # This file is part of Gauge-Ruby. @@ -34,6 +34,7 @@ describe 'self.refactor_args' do before(:each) do + Gauge::MethodCache.clear @parsed_step = 'say {} to {}' @parsed_step_no_args = 'say hello' diff --git a/spec/method_cache_spec.rb b/spec/method_cache_spec.rb index 8cfac48..11aebe0 100644 --- a/spec/method_cache_spec.rb +++ b/spec/method_cache_spec.rb @@ -1,4 +1,4 @@ -# Copyright 2015 ThoughtWorks, Inc. +# Copyright 2018 ThoughtWorks, Inc. # This file is part of Gauge-Ruby. @@ -207,9 +207,10 @@ end end context 'with :continue_on_failure => true' do + before { subject.clear } it 'should be marked as recoverable' do step 'step_text', continue_on_failure: true, &given_block - expect(subject.is_recoverable? 'parameterized_step_text').to eq true + expect(subject.recoverable? 'parameterized_step_text').to eq true end end end diff --git a/spec/refactor_processor_spec.rb b/spec/refactor_processor_spec.rb index d36857d..5a56325 100644 --- a/spec/refactor_processor_spec.rb +++ b/spec/refactor_processor_spec.rb @@ -1,4 +1,4 @@ -# Copyright 2015 ThoughtWorks, Inc. +# Copyright 2018 ThoughtWorks, Inc. # This file is part of Gauge-Ruby. @@ -19,15 +19,15 @@ let(:given_block) { -> { puts 'foo' } } context '.get_step' do describe 'should return step block' do - before { Gauge::MethodCache.add_step 'step_text', &given_block } + before { Gauge::MethodCache.add_step 'step_text', {block: given_block} } it 'should get registered ' do expect(subject.get_step('step_text')).to eq given_block end end describe 'should throw exception when duplicate step impl' do - before { Gauge::MethodCache.add_step 'step_text', &given_block } - before { Gauge::MethodCache.add_step 'step_text', &given_block } + before { Gauge::MethodCache.add_step 'step_text', {block: given_block} } + before { Gauge::MethodCache.add_step 'step_text', {block: given_block} } it { expect { subject.get_step('step_text') }.to raise_error("Multiple step implementations found for => 'step_text'") } diff --git a/spec/static_loader_spec.rb b/spec/static_loader_spec.rb new file mode 100644 index 0000000..4166a9a --- /dev/null +++ b/spec/static_loader_spec.rb @@ -0,0 +1,58 @@ +# Copyright 2015 ThoughtWorks, Inc. + +# This file is part of Gauge-Ruby. + +# Gauge-Ruby is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# Gauge-Ruby is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with Gauge-Ruby. If not, see . + +describe Gauge::StaticLoader do + + context 'load method cache from file content without executing' do + subject { Gauge::MethodCache } + before(:each){ + subject.clear + } + + it 'should pupuplate method cache' do + file = 'foo.rb' + content = "@vowels = nil +step 'foo ' do |v| + @vowels = v +end" + ast = Gauge::CodeParser.code_to_ast content + Gauge::StaticLoader.load_steps(file, ast) + expect(subject.valid_step? 'foo {}').to eq true + expect(subject.get_step_text 'foo {}').to eq 'foo ' + end + + it 'reload a given file' do + file = 'foo.rb' + content = "@vowels = nil +step 'foo ' do |v| + @vowels = v +end" + ast = Gauge::CodeParser.code_to_ast content + Gauge::StaticLoader.load_steps(file, ast) + expect(subject.valid_step? 'foo {}').to eq true + expect(subject.get_step_text 'foo {}').to eq 'foo ' + content = "@vowels = nil +step 'hello ' do |v| + @vowels = v +end" + ast = Gauge::CodeParser.code_to_ast content + Gauge::StaticLoader.reload_steps(file, ast) + expect(subject.valid_step? 'foo {}').to eq false + expect(subject.valid_step? 'hello {}').to eq true + end + end +end diff --git a/spec/validate_processor_spec.rb b/spec/validate_processor_spec.rb index a1a4f80..4ed64c9 100644 --- a/spec/validate_processor_spec.rb +++ b/spec/validate_processor_spec.rb @@ -1,4 +1,4 @@ -# Copyright 2015 ThoughtWorks, Inc. +# Copyright 2018 ThoughtWorks, Inc. # This file is part of Gauge-Ruby. @@ -21,7 +21,7 @@ context '.process_step_validation_request' do describe 'should return valid response' do before { - Gauge::MethodCache.add_step 'step_text1', &given_block + Gauge::MethodCache.add_step 'step_text1', {block: given_block} allow(message).to receive_message_chain(:stepValidateRequest, :stepText => 'step_text1') allow(message).to receive(:messageId) { 1 } } @@ -32,8 +32,8 @@ describe 'should return error response when duplicate step impl' do before { - Gauge::MethodCache.add_step 'step_text2', &given_block - Gauge::MethodCache.add_step 'step_text2', &given_block + Gauge::MethodCache.add_step 'step_text2', {block: given_block} + Gauge::MethodCache.add_step 'step_text2', {block: given_block} allow(message).to receive_message_chain(:stepValidateRequest, :stepText => 'step_text2') allow(message).to receive(:messageId) { 1 } }