From eeaf39d19f2e4f2f13b40df90623b8ddcefcd456 Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Wed, 27 Feb 2019 20:29:04 +0800 Subject: [PATCH 1/7] (GH-121) Extract module name during creating module object Previously the monkey patch to inject the editor workspace as a module did not extract the module name correctly. While this was ok for older Puppet 3 API, the Puppet 4 API namespaces objects based on the module name, so this TODO item needed to be completed. Now we extract the module name correctly. --- .../puppet_modulepath_monkey_patches.rb | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/puppet-languageserver-sidecar/puppet_modulepath_monkey_patches.rb b/lib/puppet-languageserver-sidecar/puppet_modulepath_monkey_patches.rb index 56c121b6..c056e3f4 100644 --- a/lib/puppet-languageserver-sidecar/puppet_modulepath_monkey_patches.rb +++ b/lib/puppet-languageserver-sidecar/puppet_modulepath_monkey_patches.rb @@ -57,13 +57,23 @@ def create_workspace_module_object(path) begin metadata = workspace_load_json(File.read(md_file, :encoding => 'utf-8')) return nil if metadata['name'].nil? - # TODO : Need to rip out the module name + # Extract the actual module name + if Puppet::Module.is_module_directory_name?(metadata['name']) + module_name = metadata['name'] + elsif Puppet::Module.is_module_namespaced_name?(metadata['name']) + # Based on regex at https://github.com/puppetlabs/puppet/blob/f5ca8c05174c944f783cfd0b18582e2160b77d0e/lib/puppet/module.rb#L54 + result = /^[a-zA-Z0-9]+[-]([a-z][a-z0-9_]*)$/.match(metadata['name']) + module_name = result[1] + else + # TODO: This is an invalid puppet module name in the metadata.json. Should we log an error/warning? + return nil + end # The Puppet::Module initializer was changed in # https://github.com/puppetlabs/puppet/commit/935c0311dbaf1df03937822525c36b26de5390ef # We need to switch the creation based on whether the modules_strict_semver? method is available - return Puppet::Module.new(metadata['name'], path, self, modules_strict_semver?) if respond_to?('modules_strict_semver?') - return Puppet::Module.new(metadata['name'], path, self) + return Puppet::Module.new(module_name, path, self, modules_strict_semver?) if respond_to?('modules_strict_semver?') + return Puppet::Module.new(module_name, path, self) rescue StandardError return nil end From ab270a1a4ddf1815106ca5608e343d16e99278bd Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Sun, 12 May 2019 12:59:39 -0700 Subject: [PATCH 2/7] (GH-121) Add a 'puppetstrings' feature flag The Sidecar will be experimenting with extracting puppet metadata (classes, functions and types etc.) via the Puppet Strings gem. This is an experimental function and is to be hidden behind a feature flag so users can opt in to the feature if needed. This commit: * Adds a feature flag detection method so downstream code can query if certain flags are set * Verifies that the feature flag is valid. Puppet Strings, for now, is only available from the PDK ruby based environment, not the Agent ruby envuronment. * Duplicates the puppet_helper and puppet_monkey_patches files in prepartion of them being modified when the feature flag is set. This allows changes to code to be truly isolated depending on the flag status * Duplicates the integration tests so that behaviour can be verified that is has not changed when the flag is set * Updates the Gemfile to bring in Puppet Strings during development. Note that the Puppet Strings gem is NOT vendored, though that may change in the future --- Gemfile | 4 + .../puppet_helper_puppetstrings.rb | 359 ++++++++++++++++++ .../puppet_monkey_patches_puppetstrings.rb | 89 +++++ lib/puppet_languageserver_sidecar.rb | 40 +- .../featureflag_puppetstrings_spec.rb | 343 +++++++++++++++++ .../puppet-languageserver-sidecar_spec.rb | 62 +-- 6 files changed, 862 insertions(+), 35 deletions(-) create mode 100644 lib/puppet-languageserver-sidecar/puppet_helper_puppetstrings.rb create mode 100644 lib/puppet-languageserver-sidecar/puppet_monkey_patches_puppetstrings.rb create mode 100644 spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/featureflag_puppetstrings/featureflag_puppetstrings_spec.rb diff --git a/Gemfile b/Gemfile index 6b6ec8d6..65f9258e 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,10 @@ group :development do else gem 'puppet', :require => false end + # TODO: This should be vendored into Editor Services after it is no longer a feature flag + # The puppet-strings gem is not available in the Puppet Agent, but is in the PDK. We add it to the + # Gemfile here for testing and development. + gem "puppet-strings", "~> 2.0", :require => false case RUBY_PLATFORM when /darwin/ diff --git a/lib/puppet-languageserver-sidecar/puppet_helper_puppetstrings.rb b/lib/puppet-languageserver-sidecar/puppet_helper_puppetstrings.rb new file mode 100644 index 00000000..4c16f401 --- /dev/null +++ b/lib/puppet-languageserver-sidecar/puppet_helper_puppetstrings.rb @@ -0,0 +1,359 @@ +# frozen_string_literal: true + +require 'puppet/indirector/face' + +module PuppetLanguageServerSidecar + module PuppetHelper + SIDECAR_PUPPET_ENVIRONMENT = 'sidecarenvironment' + + def self.path_has_child?(path, child) + # Doesn't matter what the child is, if the path is nil it's true. + return true if path.nil? + return false if path.length >= child.length + + value = child.slice(0, path.length) + return true if value.casecmp(path).zero? + false + end + + # Resource Face + def self.get_puppet_resource(typename, title = nil) + result = PuppetLanguageServer::Sidecar::Protocol::PuppetClassList.new + if title.nil? + resources = Puppet::Face[:resource, '0.0.1'].search(typename) + else + resources = Puppet::Face[:resource, '0.0.1'].find("#{typename}/#{title}") + end + return result if resources.nil? + resources = [resources] unless resources.is_a?(Array) + prune_resource_parameters(resources).each do |item| + obj = PuppetLanguageServer::Sidecar::Protocol::Resource.new + obj.manifest = item.to_manifest + result << obj + end + result + end + + # Class and Defined Type loading + def self.retrieve_classes(cache, options = {}) + PuppetLanguageServerSidecar.log_message(:debug, '[PuppetHelper::retrieve_classes] Starting') + + # TODO: Can probably do this better, but this works. + current_env = current_environment + module_path_list = current_env + .modules + .select { |mod| Dir.exist?(File.join(mod.path, 'manifests')) } + .map { |mod| mod.path } + manifest_path_list = module_path_list.map { |mod_path| File.join(mod_path, 'manifests') } + PuppetLanguageServerSidecar.log_message(:debug, "[PuppetHelper::retrieve_classes] Loading classes from #{module_path_list}") + + # Find and parse all manifests in the manifest paths + classes = PuppetLanguageServer::Sidecar::Protocol::PuppetClassList.new + manifest_path_list.each do |manifest_path| + Dir.glob("#{manifest_path}/**/*.pp").each do |manifest_file| + begin + if path_has_child?(options[:root_path], manifest_file) # rubocop:disable Style/IfUnlessModifier Nicer to read like this + classes.concat(load_classes_from_manifest(cache, manifest_file)) + end + rescue StandardError => e + PuppetLanguageServerSidecar.log_message(:error, "[PuppetHelper::retrieve_classes] Error loading manifest #{manifest_file}: #{e} #{e.backtrace}") + end + end + end + + PuppetLanguageServerSidecar.log_message(:debug, "[PuppetHelper::retrieve_classes] Finished loading #{classes.count} classes") + classes + end + + # Function loading + def self.retrieve_functions(cache, options = {}) + PuppetLanguageServerSidecar.log_message(:debug, '[PuppetHelper::load_functions] Starting') + + autoloader = Puppet::Util::Autoload.new(self, 'puppet/parser/functions') + current_env = current_environment + funcs = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new + + # Functions that are already loaded (e.g. system default functions like alert) + # should already be populated so insert them into the function results + # + # Find the unique filename list + filenames = [] + Puppet::Parser::Functions.monkey_function_list.each_value do |data| + filenames << data[:source_location][:source] unless data[:source_location].nil? || data[:source_location][:source].nil? + end + # Now add the functions in each file to the cache + filenames.uniq.compact.each do |filename| + Puppet::Parser::Functions.monkey_function_list + .select { |_k, i| filename.casecmp(i[:source_location][:source].to_s).zero? } + .select { |_k, i| path_has_child?(options[:root_path], i[:source_location][:source]) } + .each do |name, item| + obj = PuppetLanguageServerSidecar::Protocol::PuppetFunction.from_puppet(name, item) + funcs << obj + end + end + + # Now we can load functions from the default locations + if autoloader.method(:files_to_load).arity.zero? + params = [] + else + params = [current_env] + end + autoloader.files_to_load(*params).each do |file| + name = file.gsub(autoloader.path + '/', '') + begin + expanded_name = autoloader.expand(name) + absolute_name = Puppet::Util::Autoload.get_file(expanded_name, current_env) + raise("Could not find absolute path of function #{name}") if absolute_name.nil? + if path_has_child?(options[:root_path], absolute_name) # rubocop:disable Style/IfUnlessModifier Nicer to read like this + funcs.concat(load_function_file(cache, name, absolute_name, autoloader, current_env)) + end + rescue StandardError => e + PuppetLanguageServerSidecar.log_message(:error, "[PuppetHelper::load_functions] Error loading function #{file}: #{e} #{e.backtrace}") + end + end + + PuppetLanguageServerSidecar.log_message(:debug, "[PuppetHelper::load_functions] Finished loading #{funcs.count} functions") + funcs + end + + def self.retrieve_types(cache, options = {}) + PuppetLanguageServerSidecar.log_message(:debug, '[PuppetHelper::retrieve_types] Starting') + + # From https://github.com/puppetlabs/puppet/blob/ebd96213cab43bb2a8071b7ac0206c3ed0be8e58/lib/puppet/metatype/manager.rb#L182-L189 + autoloader = Puppet::Util::Autoload.new(self, 'puppet/type') + current_env = current_environment + types = PuppetLanguageServer::Sidecar::Protocol::PuppetTypeList.new + + # This is an expensive call + if autoloader.method(:files_to_load).arity.zero? + params = [] + else + params = [current_env] + end + autoloader.files_to_load(*params).each do |file| + name = file.gsub(autoloader.path + '/', '') + begin + expanded_name = autoloader.expand(name) + absolute_name = Puppet::Util::Autoload.get_file(expanded_name, current_env) + raise("Could not find absolute path of type #{name}") if absolute_name.nil? + if path_has_child?(options[:root_path], absolute_name) # rubocop:disable Style/IfUnlessModifier Nicer to read like this + types.concat(load_type_file(cache, name, absolute_name, autoloader, current_env)) + end + rescue StandardError => e + PuppetLanguageServerSidecar.log_message(:error, "[PuppetHelper::retrieve_types] Error loading type #{file}: #{e} #{e.backtrace}") + end + end + + PuppetLanguageServerSidecar.log_message(:debug, "[PuppetHelper::retrieve_types] Finished loading #{types.count} type/s") + + types + end + + # Private functions + + def self.prune_resource_parameters(resources) + # From https://github.com/puppetlabs/puppet/blob/488661d84e54904124514ab9e4500e81b10f84d1/lib/puppet/application/resource.rb#L146-L148 + if resources.is_a?(Array) + resources.map(&:prune_parameters) + else + resources.prune_parameters + end + end + private_class_method :prune_resource_parameters + + def self.current_environment + begin + env = Puppet.lookup(:environments).get!(Puppet.settings[:environment]) + return env unless env.nil? + rescue Puppet::Environments::EnvironmentNotFound + PuppetLanguageServerSidecar.log_message(:warning, "[PuppetHelper::current_environment] Unable to load environment #{Puppet.settings[:environment]}") + rescue StandardError => e + PuppetLanguageServerSidecar.log_message(:warning, "[PuppetHelper::current_environment] Error loading environment #{Puppet.settings[:environment]}: #{e}") + end + Puppet.lookup(:current_environment) + end + private_class_method :current_environment + + def self.load_classes_from_manifest(cache, manifest_file) + class_info = PuppetLanguageServer::Sidecar::Protocol::PuppetClassList.new + + if cache.active? + cached_result = cache.load(manifest_file, PuppetLanguageServerSidecar::Cache::CLASSES_SECTION) + unless cached_result.nil? + begin + class_info.from_json!(cached_result) + return class_info + rescue StandardError => e + PuppetLanguageServerSidecar.log_message(:warn, "[PuppetHelper::load_classes_from_manifest] Error while deserializing #{manifest_file} from cache: #{e}") + class_info = PuppetLanguageServer::Sidecar::Protocol::PuppetClassList.new + end + end + end + + file_content = File.open(manifest_file, 'r:UTF-8') { |f| f.read } + + parser = Puppet::Pops::Parser::Parser.new + result = nil + begin + result = parser.parse_string(file_content, '') + rescue Puppet::ParseErrorWithIssue + # Any parsing errors means we can't inspect the document + return class_info + end + + # Enumerate the entire AST looking for classes and defined types + # TODO: Need to learn how to read the help/docs for hover support + if result.model.respond_to? :eAllContents + # Puppet 4 AST + result.model.eAllContents.select do |item| + puppet_class = {} + case item.class.to_s + when 'Puppet::Pops::Model::HostClassDefinition' + puppet_class['type'] = :class + when 'Puppet::Pops::Model::ResourceTypeDefinition' + puppet_class['type'] = :typedefinition + else + next + end + puppet_class['name'] = item.name + puppet_class['doc'] = nil + puppet_class['parameters'] = item.parameters + puppet_class['source'] = manifest_file + puppet_class['line'] = result.locator.line_for_offset(item.offset) - 1 + puppet_class['char'] = result.locator.offset_on_line(item.offset) + + obj = PuppetLanguageServerSidecar::Protocol::PuppetClass.from_puppet(item.name, puppet_class, result.locator) + class_info << obj + end + else + result.model._pcore_all_contents([]) do |item| + puppet_class = {} + case item.class.to_s + when 'Puppet::Pops::Model::HostClassDefinition' + puppet_class['type'] = :class + when 'Puppet::Pops::Model::ResourceTypeDefinition' + puppet_class['type'] = :typedefinition + else + next + end + puppet_class['name'] = item.name + puppet_class['doc'] = nil + puppet_class['parameters'] = item.parameters + puppet_class['source'] = manifest_file + puppet_class['line'] = item.line + puppet_class['char'] = item.pos + obj = PuppetLanguageServerSidecar::Protocol::PuppetClass.from_puppet(item.name, puppet_class, item.locator) + class_info << obj + end + end + cache.save(manifest_file, PuppetLanguageServerSidecar::Cache::CLASSES_SECTION, class_info.to_json) if cache.active? + + class_info + end + private_class_method :load_classes_from_manifest + + def self.load_function_file(cache, name, absolute_name, autoloader, env) + funcs = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new + + if cache.active? + cached_result = cache.load(absolute_name, PuppetLanguageServerSidecar::Cache::FUNCTIONS_SECTION) + unless cached_result.nil? + begin + funcs.from_json!(cached_result) + return funcs + rescue StandardError => e + PuppetLanguageServerSidecar.log_message(:warn, "[PuppetHelper::load_function_file] Error while deserializing #{absolute_name} from cache: #{e}") + funcs = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new + end + end + end + + unless autoloader.loaded?(name) + # This is an expensive call + unless autoloader.load(name, env) # rubocop:disable Style/IfUnlessModifier Nicer to read like this + PuppetLanguageServerSidecar.log_message(:error, "[PuppetHelper::load_function_file] function #{absolute_name} did not load") + end + end + + # Find the functions that were loaded based on source file name (case insensitive) + Puppet::Parser::Functions.monkey_function_list + .select { |_k, i| absolute_name.casecmp(i[:source_location][:source].to_s).zero? } + .each do |func_name, item| + obj = PuppetLanguageServerSidecar::Protocol::PuppetFunction.from_puppet(func_name, item) + obj.calling_source = absolute_name + funcs << obj + end + PuppetLanguageServerSidecar.log_message(:warn, "[PuppetHelper::load_function_file] file #{absolute_name} did load any functions") if funcs.count.zero? + cache.save(absolute_name, PuppetLanguageServerSidecar::Cache::FUNCTIONS_SECTION, funcs.to_json) if cache.active? + + funcs + end + private_class_method :load_function_file + + def self.load_type_file(cache, name, absolute_name, autoloader, env) + types = PuppetLanguageServer::Sidecar::Protocol::PuppetTypeList.new + if cache.active? + cached_result = cache.load(absolute_name, PuppetLanguageServerSidecar::Cache::TYPES_SECTION) + unless cached_result.nil? + begin + types.from_json!(cached_result) + return types + rescue StandardError => e + PuppetLanguageServerSidecar.log_message(:warn, "[PuppetHelper::load_type_file] Error while deserializing #{absolute_name} from cache: #{e}") + types = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new + end + end + end + + # Get the list of currently loaded types + loaded_types = [] + # Due to PUP-8301, if no types have been loaded yet then Puppet::Type.eachtype + # will throw instead of not yielding. + begin + Puppet::Type.eachtype { |item| loaded_types << item.name } + rescue NoMethodError => e + # Detect PUP-8301 + if e.respond_to?(:receiver) + raise unless e.name == :each && e.receiver.nil? + else + raise unless e.name == :each && e.message =~ /nil:NilClass/ + end + end + + unless autoloader.loaded?(name) + # This is an expensive call + unless autoloader.load(name, env) # rubocop:disable Style/IfUnlessModifier Nicer to read like this + PuppetLanguageServerSidecar.log_message(:error, "[PuppetHelper::load_type_file] type #{absolute_name} did not load") + end + end + + # Find the types that were loaded + # Due to PUP-8301, if no types have been loaded yet then Puppet::Type.eachtype + # will throw instead of not yielding. + begin + Puppet::Type.eachtype do |item| + next if loaded_types.include?(item.name) + # Ignore the internal only Puppet Types + next if item.name == :component || item.name == :whit + obj = PuppetLanguageServerSidecar::Protocol::PuppetType.from_puppet(item.name, item) + # TODO: Need to use calling_source in the cache backing store + # Perhaps I should be incrementally adding items to the cache instead of batch mode? + obj.calling_source = absolute_name + types << obj + end + rescue NoMethodError => e + # Detect PUP-8301 + if e.respond_to?(:receiver) + raise unless e.name == :each && e.receiver.nil? + else + raise unless e.name == :each && e.message =~ /nil:NilClass/ + end + end + PuppetLanguageServerSidecar.log_message(:warn, "[PuppetHelper::load_type_file] type #{absolute_name} did not load any types") if types.empty? + cache.save(absolute_name, PuppetLanguageServerSidecar::Cache::TYPES_SECTION, types.to_json) if cache.active? + + types + end + private_class_method :load_type_file + end +end diff --git a/lib/puppet-languageserver-sidecar/puppet_monkey_patches_puppetstrings.rb b/lib/puppet-languageserver-sidecar/puppet_monkey_patches_puppetstrings.rb new file mode 100644 index 00000000..f082ff78 --- /dev/null +++ b/lib/puppet-languageserver-sidecar/puppet_monkey_patches_puppetstrings.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +# Monkey Patch 3.x functions so where know where they were loaded from +require 'puppet/parser/functions' +module Puppet + module Parser + module Functions + class << self + alias_method :original_newfunction, :newfunction + def newfunction(name, options = {}, &block) + # See if we've hooked elsewhere. This can happen while in debuggers (pry). If we're not in the previous caller + # stack then just use the last caller + monkey_index = Kernel.caller_locations.find_index { |loc| loc.path.match(/puppet_monkey_patches\.rb/) } + monkey_index = -1 if monkey_index.nil? + caller = Kernel.caller_locations[monkey_index + 1] + # Call the original new function method + result = original_newfunction(name, options, &block) + # Append the caller information + result[:source_location] = { + :source => caller.absolute_path, + :line => caller.lineno - 1 # Convert to a zero based line number system + } + monkey_append_function_info(name, result) + + result + end + + def monkey_clear_function_info + @monkey_function_list = {} + end + + def monkey_append_function_info(name, value) + @monkey_function_list = {} if @monkey_function_list.nil? + @monkey_function_list[name] = { + :arity => value[:arity], + :name => value[:name], + :type => value[:type], + :doc => value[:doc], + :source_location => value[:source_location] + } + end + + def monkey_function_list + @monkey_function_list = {} if @monkey_function_list.nil? + @monkey_function_list.clone + end + end + end + end +end + +# Add an additional method on Puppet Types to store their source location +require 'puppet/type' +module Puppet + class Type + class << self + attr_accessor :_source_location + end + end +end + +# Monkey Patch type loading so we can inject the source location information +require 'puppet/metatype/manager' +module Puppet + module MetaType + module Manager + alias_method :original_newtype, :newtype + def newtype(name, options = {}, &block) + result = original_newtype(name, options, &block) + + if block_given? && !block.source_location.nil? + result._source_location = { + :source => block.source_location[0], + :line => block.source_location[1] - 1 # Convert to a zero based line number system + } + end + result + end + end + end +end + +# MUST BE LAST!!!!!! +# Suppress any warning messages to STDOUT. It can pollute stdout when running in STDIO mode +Puppet::Util::Log.newdesttype :null_logger do + def handle(msg) + PuppetLanguageServerSidecar.log_message(:debug, "[PUPPET LOG] [#{msg.level}] #{msg.message}") + end +end diff --git a/lib/puppet_languageserver_sidecar.rb b/lib/puppet_languageserver_sidecar.rb index 0564a365..d9992dbc 100644 --- a/lib/puppet_languageserver_sidecar.rb +++ b/lib/puppet_languageserver_sidecar.rb @@ -27,6 +27,15 @@ def self.version PuppetEditorServices.version end + def self.configure_featureflags(flags) + @flags = flags + end + + def self.featureflag?(flagname) + return false if @flags.nil? || @flags.empty? + @flags.include?(flagname) + end + def self.require_gems(options) original_verbose = $VERBOSE $VERBOSE = nil @@ -45,16 +54,39 @@ def self.require_gems(options) require 'puppet' - %w[ + # Validate the feature flags + unless options[:flags].nil? || options[:flags].empty? + flags = options[:flags] + log_message(:debug, "Detected feature flags [#{options[:flags].join(', ')}]") + + strings_gem = Gem::Specification.select { |item| item.name.casecmp('puppet-strings').zero? } + if flags.include?('puppetstrings') && strings_gem.count.zero? + # The puppetstrings flag is only valid when the puppet-strings gem is available + PuppetEditorServices.log_message(:error, "The feature flag 'puppetstrings' has been specified but it is not capable due to Puppet Strings being unavailable. Turning off the flag.") + flags -= ['puppetstrings'] + end + configure_featureflags(flags) + end + + require_list = %w[ cache/base cache/null cache/filesystem - puppet_helper puppet_parser_helper - puppet_monkey_patches sidecar_protocol_extensions workspace - ].each do |lib| + ] + + # Load files based on feature flags + if featureflag?('puppetstrings') + require_list << 'puppet_helper_puppetstrings' + require_list << 'puppet_monkey_patches_puppetstrings' + else + require_list << 'puppet_helper' + require_list << 'puppet_monkey_patches' + end + + require_list.each do |lib| begin require "puppet-languageserver-sidecar/#{lib}" rescue LoadError diff --git a/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/featureflag_puppetstrings/featureflag_puppetstrings_spec.rb b/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/featureflag_puppetstrings/featureflag_puppetstrings_spec.rb new file mode 100644 index 00000000..54407b9f --- /dev/null +++ b/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/featureflag_puppetstrings/featureflag_puppetstrings_spec.rb @@ -0,0 +1,343 @@ +require 'spec_helper' +require 'open3' +require 'tempfile' + +describe 'PuppetLanguageServerSidecar with Feature Flag puppetstrings' do + def run_sidecar(cmd_options) + cmd_options << '--no-cache' + + # Append the feature flag + cmd_options << '--feature-flag=puppetstrings' + + # Append the puppet test-fixtures + cmd_options << '--puppet-settings' + cmd_options << "--vardir,#{File.join($fixtures_dir, 'real_agent', 'cache')},--confdir,#{File.join($fixtures_dir, 'real_agent', 'confdir')}" + + cmd = ['ruby', 'puppet-languageserver-sidecar'].concat(cmd_options) + stdout, _stderr, status = Open3.capture3(*cmd) + + raise "Expected exit code of 0, but got #{status.exitstatus} #{_stderr}" unless status.exitstatus.zero? + return stdout.bytes.pack('U*') + end + + def child_with_key(array, key) + idx = array.index { |item| item.key == key } + return idx.nil? ? nil : array[idx] + end + + def with_temporary_file(content) + tempfile = Tempfile.new("langserver-sidecar") + tempfile.open + + tempfile.write(content) + + tempfile.close + + yield tempfile.path + ensure + tempfile.delete if tempfile + end + + RSpec::Matchers.define :contain_child_with_key do |key| + match do |actual| + !(actual.index { |item| item.key == key }).nil? + end + + failure_message do |actual| + "expected that #{actual.class.to_s} would contain a child with key #{key}" + end + end + + describe 'when running default_classes action' do + let (:cmd_options) { ['--action', 'default_classes'] } + + it 'should return a deserializable class list with default classes' do + result = run_sidecar(cmd_options) + deserial = PuppetLanguageServer::Sidecar::Protocol::PuppetClassList.new() + expect { deserial.from_json!(result) }.to_not raise_error + + expect(deserial.count).to be > 0 + + # There are no default classes in Puppet, so only check for ones in the environment + # These are defined in the fixtures/real_agent/environments/testfixtures/modules/defaultmodule + expect(deserial).to contain_child_with_key(:defaultdefinedtype) + expect(deserial).to contain_child_with_key(:defaultmodule) + end + end + + describe 'when running default_functions action' do + let (:cmd_options) { ['--action', 'default_functions'] } + + it 'should return a deserializable function list with default functions' do + result = run_sidecar(cmd_options) + deserial = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new() + expect { deserial.from_json!(result) }.to_not raise_error + + expect(deserial.count).to be > 0 + + # These functions always exist + expect(deserial).to contain_child_with_key(:notice) + expect(deserial).to contain_child_with_key(:alert) + + # These are defined in the fixtures/real_agent/cache/lib/puppet/parser/functions + expect(deserial).to contain_child_with_key(:default_cache_function) + end + end + + describe 'when running default_types action' do + let (:cmd_options) { ['--action', 'default_types'] } + + it 'should return a deserializable type list with default types' do + result = run_sidecar(cmd_options) + deserial = PuppetLanguageServer::Sidecar::Protocol::PuppetTypeList.new() + expect { deserial.from_json!(result) }.to_not raise_error + + expect(deserial.count).to be > 0 + + # These types always exist + expect(deserial).to contain_child_with_key(:user) + expect(deserial).to contain_child_with_key(:group) + expect(deserial).to contain_child_with_key(:package) + expect(deserial).to contain_child_with_key(:service) + + # These are defined in the fixtures/real_agent/cache/lib/puppet/type + expect(deserial).to contain_child_with_key(:default_type) + end + end + + context 'given a workspace containing a module' do + # Test fixtures used is fixtures/valid_module_workspace + let(:workspace) { File.join($fixtures_dir, 'valid_module_workspace') } + + describe 'when running node_graph action' do + let (:cmd_options) { ['--action', 'node_graph', '--local-workspace', workspace] } + + it 'should return a deserializable node graph' do + # The fixture type is only present in the local workspace + with_temporary_file("fixture { 'test':\n}") do |filepath| + action_params = PuppetLanguageServer::Sidecar::Protocol::ActionParams.new() + action_params['source'] = filepath + + result = run_sidecar(cmd_options.concat(['--action-parameters', action_params.to_json])) + + deserial = PuppetLanguageServer::Sidecar::Protocol::NodeGraph.new() + expect { deserial.from_json!(result) }.to_not raise_error + + expect(deserial.dot_content).to match(/Fixture\[test\]/) + expect(deserial.error_content.to_s).to eq('') + end + end + end + + describe 'when running workspace_classes action' do + let (:cmd_options) { ['--action', 'workspace_classes', '--local-workspace', workspace] } + + it 'should return a deserializable class list with the named fixtures' do + result = run_sidecar(cmd_options) + deserial = PuppetLanguageServer::Sidecar::Protocol::PuppetClassList.new() + expect { deserial.from_json!(result) }.to_not raise_error + + # Classes + expect(deserial).to contain_child_with_key(:valid) + expect(deserial).to contain_child_with_key(:"valid::nested::anotherclass") + # Defined Types + expect(deserial).to contain_child_with_key(:deftypeone) + end + end + + describe 'when running workspace_functions action' do + let (:cmd_options) { ['--action', 'workspace_functions', '--local-workspace', workspace] } + + it 'should return a deserializable function list with the named fixtures' do + result = run_sidecar(cmd_options) + deserial = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new() + expect { deserial.from_json!(result) }.to_not raise_error + + expect(deserial).to_not contain_child_with_key(:badfile) + expect(deserial).to contain_child_with_key(:bad_function) + expect(deserial).to contain_child_with_key(:fixture_function) + + # Make sure the function has the right properties + func = child_with_key(deserial, :fixture_function) + expect(func.doc).to eq('doc_fixture_function') + expect(func.source).to match(/valid_module_workspace/) + end + end + + describe 'when running workspace_types action' do + let (:cmd_options) { ['--action', 'workspace_types', '--local-workspace', workspace] } + + it 'should return a deserializable type list with the named fixtures' do + result = run_sidecar(cmd_options) + deserial = PuppetLanguageServer::Sidecar::Protocol::PuppetTypeList.new() + expect { deserial.from_json!(result) }.to_not raise_error + + expect(deserial).to contain_child_with_key(:fixture) + + # Make sure the type has the right properties + obj = child_with_key(deserial, :fixture) + expect(obj.doc).to eq('doc_type_fixture') + expect(obj.source).to match(/valid_module_workspace/) + # Make sure the type attributes are correct + expect(obj.attributes.key?(:name)).to be true + expect(obj.attributes.key?(:when)).to be true + expect(obj.attributes[:name][:type]).to eq(:param) + expect(obj.attributes[:name][:doc]).to eq("name_parameter\n\n") + expect(obj.attributes[:name][:required?]).to be true + expect(obj.attributes[:when][:type]).to eq(:property) + expect(obj.attributes[:when][:doc]).to eq("when_property\n\n") + expect(obj.attributes[:when][:required?]).to be_nil + end + end + end + + context 'given a workspace containing an environment.conf' do + # Test fixtures used is fixtures/valid_environment_workspace + let(:workspace) { File.join($fixtures_dir, 'valid_environment_workspace') } + + describe 'when running node_graph action' do + let (:cmd_options) { ['--action', 'node_graph', '--local-workspace', workspace] } + + it 'should return a deserializable node graph' do + # The envtype type is only present in the local workspace + with_temporary_file("envtype { 'test':\n}") do |filepath| + action_params = PuppetLanguageServer::Sidecar::Protocol::ActionParams.new() + action_params['source'] = filepath + + result = run_sidecar(cmd_options.concat(['--action-parameters', action_params.to_json])) + + deserial = PuppetLanguageServer::Sidecar::Protocol::NodeGraph.new() + expect { deserial.from_json!(result) }.to_not raise_error + + expect(deserial.dot_content).to match(/Envtype\[test\]/) + expect(deserial.error_content.to_s).to eq('') + end + end + end + + describe 'when running workspace_classes action' do + let (:cmd_options) { ['--action', 'workspace_classes', '--local-workspace', workspace] } + + it 'should return a deserializable class list with the named fixtures' do + result = run_sidecar(cmd_options) + deserial = PuppetLanguageServer::Sidecar::Protocol::PuppetClassList.new() + expect { deserial.from_json!(result) }.to_not raise_error + + # Classes + expect(deserial).to contain_child_with_key(:"role::base") + expect(deserial).to contain_child_with_key(:"profile::main") + # Defined Types + expect(deserial).to contain_child_with_key(:envdeftype) + + # Make sure the class has the right properties + obj = child_with_key(deserial, :envdeftype) + expect(obj.doc).to be_nil # We don't yet get documentation for classes or defined types + expect(obj.parameters['ensure']).to_not be_nil + expect(obj.parameters['ensure'][:type]).to eq('String') + expect(obj.source).to match(/valid_environment_workspace/) + end + end + + describe 'when running workspace_functions action' do + let (:cmd_options) { ['--action', 'workspace_functions', '--local-workspace', workspace] } + + it 'should return a deserializable function list with the named fixtures' do + result = run_sidecar(cmd_options) + deserial = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new() + expect { deserial.from_json!(result) }.to_not raise_error + + expect(deserial).to contain_child_with_key(:env_function) + + # Make sure the function has the right properties + func = child_with_key(deserial, :env_function) + expect(func.doc).to eq('doc_env_function') + expect(func.source).to match(/valid_environment_workspace/) + end + end + + describe 'when running workspace_types action' do + let (:cmd_options) { ['--action', 'workspace_types', '--local-workspace', workspace] } + + it 'should return a deserializable type list with the named fixtures' do + result = run_sidecar(cmd_options) + deserial = PuppetLanguageServer::Sidecar::Protocol::PuppetTypeList.new() + expect { deserial.from_json!(result) }.to_not raise_error + + expect(deserial).to contain_child_with_key(:envtype) + + # Make sure the type has the right properties + obj = child_with_key(deserial, :envtype) + expect(obj.doc).to eq('doc_type_fixture') + expect(obj.source).to match(/valid_environment_workspace/) + # Make sure the type attributes are correct + expect(obj.attributes.key?(:name)).to be true + expect(obj.attributes.key?(:when)).to be true + expect(obj.attributes[:name][:type]).to eq(:param) + expect(obj.attributes[:name][:doc]).to eq("name_env_parameter\n\n") + expect(obj.attributes[:name][:required?]).to be true + expect(obj.attributes[:when][:type]).to eq(:property) + expect(obj.attributes[:when][:doc]).to eq("when_env_property\n\n") + expect(obj.attributes[:when][:required?]).to be_nil + end + end + end + + describe 'when running node_graph action' do + let (:cmd_options) { ['--action', 'node_graph'] } + + it 'should return a deserializable node graph' do + with_temporary_file("user { 'test':\nensure => present\n}") do |filepath| + action_params = PuppetLanguageServer::Sidecar::Protocol::ActionParams.new() + action_params['source'] = filepath + + result = run_sidecar(cmd_options.concat(['--action-parameters', action_params.to_json])) + + deserial = PuppetLanguageServer::Sidecar::Protocol::NodeGraph.new() + expect { deserial.from_json!(result) }.to_not raise_error + + expect(deserial.dot_content).to_not eq('') + expect(deserial.error_content.to_s).to eq('') + end + end + end + + describe 'when running resource_list action' do + let (:cmd_options) { ['--action', 'resource_list'] } + + context 'for a resource with no title' do + let (:action_params) { + value = PuppetLanguageServer::Sidecar::Protocol::ActionParams.new() + value['typename'] = 'user' + value + } + + it 'should return a deserializable resource list' do + result = run_sidecar(cmd_options.concat(['--action-parameters', action_params.to_json])) + deserial = PuppetLanguageServer::Sidecar::Protocol::ResourceList.new() + expect { deserial.from_json!(result) }.to_not raise_error + + expect(deserial.count).to be > 0 + end + end + + context 'for a resource with a title' do + let (:action_params) { + # This may do odd things with non ASCII usernames on Windows + current_username = ENV['USER'] || ENV['USERNAME'] + + value = PuppetLanguageServer::Sidecar::Protocol::ActionParams.new() + value['typename'] = 'user' + value['title'] = current_username + value + } + + it 'should return a deserializable resource list with a single item' do + result = run_sidecar(cmd_options.concat(['--action-parameters', action_params.to_json])) + deserial = PuppetLanguageServer::Sidecar::Protocol::ResourceList.new() + expect { deserial.from_json!(result) }.to_not raise_error + + expect(deserial.count).to be 1 + end + end + end +end diff --git a/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/puppet-languageserver-sidecar_spec.rb b/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/puppet-languageserver-sidecar_spec.rb index 139111a0..e6874c42 100644 --- a/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/puppet-languageserver-sidecar_spec.rb +++ b/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/puppet-languageserver-sidecar_spec.rb @@ -2,49 +2,49 @@ require 'open3' require 'tempfile' -def run_sidecar(cmd_options) - cmd_options << '--no-cache' - - # Append the puppet test-fixtures - cmd_options << '--puppet-settings' - cmd_options << "--vardir,#{File.join($fixtures_dir, 'real_agent', 'cache')},--confdir,#{File.join($fixtures_dir, 'real_agent', 'confdir')}" +describe 'PuppetLanguageServerSidecar' do + def run_sidecar(cmd_options) + cmd_options << '--no-cache' - cmd = ['ruby', 'puppet-languageserver-sidecar'].concat(cmd_options) - stdout, _stderr, status = Open3.capture3(*cmd) + # Append the puppet test-fixtures + cmd_options << '--puppet-settings' + cmd_options << "--vardir,#{File.join($fixtures_dir, 'real_agent', 'cache')},--confdir,#{File.join($fixtures_dir, 'real_agent', 'confdir')}" - raise "Expected exit code of 0, but got #{status.exitstatus} #{_stderr}" unless status.exitstatus.zero? - return stdout.bytes.pack('U*') -end + cmd = ['ruby', 'puppet-languageserver-sidecar'].concat(cmd_options) + stdout, _stderr, status = Open3.capture3(*cmd) -def child_with_key(array, key) - idx = array.index { |item| item.key == key } - return idx.nil? ? nil : array[idx] -end + raise "Expected exit code of 0, but got #{status.exitstatus} #{_stderr}" unless status.exitstatus.zero? + return stdout.bytes.pack('U*') + end -def with_temporary_file(content) - tempfile = Tempfile.new("langserver-sidecar") - tempfile.open + def child_with_key(array, key) + idx = array.index { |item| item.key == key } + return idx.nil? ? nil : array[idx] + end - tempfile.write(content) + def with_temporary_file(content) + tempfile = Tempfile.new("langserver-sidecar") + tempfile.open - tempfile.close + tempfile.write(content) - yield tempfile.path -ensure - tempfile.delete if tempfile -end + tempfile.close -RSpec::Matchers.define :contain_child_with_key do |key| - match do |actual| - !(actual.index { |item| item.key == key }).nil? + yield tempfile.path + ensure + tempfile.delete if tempfile end - failure_message do |actual| - "expected that #{actual.class.to_s} would contain a child with key #{key}" + RSpec::Matchers.define :contain_child_with_key do |key| + match do |actual| + !(actual.index { |item| item.key == key }).nil? + end + + failure_message do |actual| + "expected that #{actual.class.to_s} would contain a child with key #{key}" + end end -end -describe 'PuppetLanguageServerSidecar' do describe 'when running default_classes action' do let (:cmd_options) { ['--action', 'default_classes'] } From 18899e6b1b2d08ca0052c10d8a63419004814e7d Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Thu, 16 May 2019 16:29:37 +0800 Subject: [PATCH 3/7] (GH-121) Add Puppet Strings helper methods The Puppet Strings gem uses YARD to parse the relevant files, however this is not useful for the Sidecar as we need access to the ruby objects, not a markdown or JSON file being created. This commit: * Adds a PuppetStringsHelper which can configure and execute YARD in the same way Puppet Strings does and then allow the Sidecar to extract the information it needs later. * Later commits will modify the helper to understand the various metadata the Sidecar needs. * Adds a caching layer to the results of running YARD. This means that if a file is queried more than once, YARD will only be executed once as running YARD is an expensive exercise. * Monkey patches YARD to suppress ALL output. By default the command line parameters still emit text of STDOUT, STDERR which breaks the STDIO transport for the Sidecar. --- .../puppet_strings_helper.rb | 95 +++++++++++++++++++ .../puppet_strings_monkey_patches.rb | 16 ++++ 2 files changed, 111 insertions(+) create mode 100644 lib/puppet-languageserver-sidecar/puppet_strings_helper.rb create mode 100644 lib/puppet-languageserver-sidecar/puppet_strings_monkey_patches.rb diff --git a/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb b/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb new file mode 100644 index 00000000..63226710 --- /dev/null +++ b/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module PuppetLanguageServerSidecar + module PuppetStringsHelper + # Returns a FileDocumentation object for a given path + # + # @param [String] path The absolute path to the file that will be documented + # @return [FileDocumentation, nil] Returns the documentation for the path, or nil if it cannot be extracted + def self.file_documentation(path) + return nil unless require_puppet_strings + @helper_cache = FileDocumentationCache.new if @helper_cache.nil? + return @helper_cache.document(path) if @helper_cache.path_exists?(path) + PuppetLanguageServerSidecar.log_message(:debug, "[PuppetStringsHelper::file_documentation] Fetching documentation for #{path}") + + setup_yard! + + # For now, assume a single file path + search_patterns = [path] + + # Format the arguments to YARD + args = ['doc'] + args << '--no-output' + args << '--quiet' + args << '--no-stats' + args << '--no-progress' + args << '--no-save' + args << '--api public' + args << '--api private' + args << '--no-api' + args += search_patterns + + # Run YARD + ::YARD::CLI::Yardoc.run(*args) + + # Populate the documentation cache from the YARD information + @helper_cache.populate_from_yard_registry! + + # Return the documentation details + @helper_cache.document(path) + end + + def self.require_puppet_strings + return @puppet_strings_loaded unless @puppet_strings_loaded.nil? + begin + require 'puppet-strings' + require 'puppet-strings/yard' + require 'puppet-strings/json' + @puppet_strings_loaded = true + rescue LoadError => e + PuppetLanguageServerSidecar.log_message(:error, "[PuppetStringsHelper::require_puppet_strings] Unable to load puppet-strings gem: #{e}") + @puppet_strings_loaded = false + end + @puppet_strings_loaded + end + private_class_method :require_puppet_strings + + def self.setup_yard! + unless @yard_setup # rubocop:disable Style/GuardClause + ::PuppetStrings::Yard.setup! + @yard_setup = true + end + end + private_class_method :setup_yard! + end + + class FileDocumentationCache + def initialize + # Hash of <[String] path, FileDocumentation> objects + @cache = {} + end + + def path_exists?(path) + @cache.key?(path) + end + + def document(path) + @cache[path] + end + + def populate_from_yard_registry! + # Extract all of the information + # Ref - https://github.com/puppetlabs/puppet-strings/blob/87a8e10f45bfeb7b6b8e766324bfb126de59f791/lib/puppet-strings/json.rb#L10-L16 + end + + private + + class FileDocumentation + # The path to file that has been documented + attr_reader :path + + def initialize(path) + @path = path + end + end +end diff --git a/lib/puppet-languageserver-sidecar/puppet_strings_monkey_patches.rb b/lib/puppet-languageserver-sidecar/puppet_strings_monkey_patches.rb new file mode 100644 index 00000000..297cf198 --- /dev/null +++ b/lib/puppet-languageserver-sidecar/puppet_strings_monkey_patches.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'yard/logging' +module YARD + class Logger < ::Logger + # Suppress ANY output + def self.instance(_pipe = STDOUT) + @logger ||= new(nil) + end + + # Suppress ANY progress indicators + def show_progress + false + end + end +end From 12cf6199ae5ac7924ad74f3d7669a2036162772b Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Thu, 16 May 2019 16:30:40 +0800 Subject: [PATCH 4/7] (GH-121) Load Puppet 3 and 4 API Functions via Puppet Strings Currently the Sidecar can only detect and load Puppet 3 API functions. The newer Puppet 4 API functions use a different loader and schema, and importantly have additional properties e.g. Puppet 4 API functions have one or more signatures, whereas Puppet 3 API functions use arity. In order to add the Puppet Strings loader, this commit will load Puppet 4 API functions but present them to the Language Server as if they were Puppet 3. A later commit will then change this behaviour so that all the metadata for Puppet 4 API functions will be known by the Language Server, and Puppet 3 API function metadata will be munged into the 4 API equivalent. This commit: * Extends the Sidecar protocol to add a function_version property to the Puppet Function schema. This can be used later by the Language Server to determine how to handle the metadata. * Adds a new method called retrieve_via_puppet_strings to the puppet_helper. This method queries the Puppet Loaders (vai Puppet-As-A-Library PAL) for all the files for particular puppet objects (functions in this case) and then gets the documentation about these files via the Puppet Strings helper * The PAL files are only available on Puppet Gem 6 and above, so the feature flag is modified to only be active on Puppet version 6+ * Adds in a new method called 'discover_paths' on all PAL loaders. The loaders themselves are normally used to load _something_ by name, however the Sidecar wants to load EVERYTHING. Generally this information is private to each loader. By adding this additional method, we can extract all of the discoverable paths, without needing to write our own loaders * Removes the old function loading and monkey patches --- .../puppet_helper_puppetstrings.rb | 137 ++++++------------ .../puppet_monkey_patches_puppetstrings.rb | 126 +++++++++------- .../puppet_strings_helper.rb | 51 ++++++- lib/puppet-languageserver/sidecar_protocol.rb | 10 +- lib/puppet_languageserver_sidecar.rb | 26 +++- spec/languageserver/spec_helper.rb | 1 + .../sidecar_protocol_spec.rb | 2 +- 7 files changed, 206 insertions(+), 147 deletions(-) diff --git a/lib/puppet-languageserver-sidecar/puppet_helper_puppetstrings.rb b/lib/puppet-languageserver-sidecar/puppet_helper_puppetstrings.rb index 4c16f401..d3c6e55b 100644 --- a/lib/puppet-languageserver-sidecar/puppet_helper_puppetstrings.rb +++ b/lib/puppet-languageserver-sidecar/puppet_helper_puppetstrings.rb @@ -34,6 +34,54 @@ def self.get_puppet_resource(typename, title = nil) result end + # Puppet Strings loading + def self.available_documentation_types + [:function] + end + + # Retrieve objects via the Puppet 4 API loaders + def self.retrieve_via_puppet_strings(_cache, options = {}) + PuppetLanguageServerSidecar.log_message(:debug, '[PuppetHelper::retrieve_via_puppet_strings] Starting') + + object_types = options[:object_types].nil? ? available_documentation_types : options[:object_types] + object_types.select! { |i| available_documentation_types.include?(i) } + + result = {} + return result if object_types.empty? + + result[:functions] = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new if object_types.include?(:function) + + current_env = current_environment + for_agent = options[:for_agent].nil? ? true : options[:for_agent] + loaders = Puppet::Pops::Loaders.new(current_env, for_agent) + + paths = [] + paths.concat(discover_type_paths(:function, loaders)) if object_types.include?(:function) + + paths.each do |path| + next unless path_has_child?(options[:root_path], path) + file_doc = PuppetLanguageServerSidecar::PuppetStringsHelper.file_documentation(path) + next if file_doc.nil? + + if object_types.include?(:function) # rubocop:disable Style/IfUnlessModifier This reads better + file_doc.functions.each { |item| result[:functions] << item } + end + end + + # Remove Puppet3 functions which have a Puppet4 function already loaded + if object_types.include?(:function) + pup4_functions = result[:functions].select { |i| i.function_version == 4 }.map { |i| i.key } + result[:functions].reject! { |i| i.function_version == 3 && pup4_functions.include?(i.key) } + end + + result.each { |key, item| PuppetLanguageServerSidecar.log_message(:debug, "[PuppetHelper::retrieve_via_puppet_strings] Finished loading #{item.count} #{key}") } + result + end + + def self.discover_type_paths(type, loaders) + loaders.private_environment_loader.discover_paths(type) + end + # Class and Defined Type loading def self.retrieve_classes(cache, options = {}) PuppetLanguageServerSidecar.log_message(:debug, '[PuppetHelper::retrieve_classes] Starting') @@ -65,57 +113,6 @@ def self.retrieve_classes(cache, options = {}) classes end - # Function loading - def self.retrieve_functions(cache, options = {}) - PuppetLanguageServerSidecar.log_message(:debug, '[PuppetHelper::load_functions] Starting') - - autoloader = Puppet::Util::Autoload.new(self, 'puppet/parser/functions') - current_env = current_environment - funcs = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new - - # Functions that are already loaded (e.g. system default functions like alert) - # should already be populated so insert them into the function results - # - # Find the unique filename list - filenames = [] - Puppet::Parser::Functions.monkey_function_list.each_value do |data| - filenames << data[:source_location][:source] unless data[:source_location].nil? || data[:source_location][:source].nil? - end - # Now add the functions in each file to the cache - filenames.uniq.compact.each do |filename| - Puppet::Parser::Functions.monkey_function_list - .select { |_k, i| filename.casecmp(i[:source_location][:source].to_s).zero? } - .select { |_k, i| path_has_child?(options[:root_path], i[:source_location][:source]) } - .each do |name, item| - obj = PuppetLanguageServerSidecar::Protocol::PuppetFunction.from_puppet(name, item) - funcs << obj - end - end - - # Now we can load functions from the default locations - if autoloader.method(:files_to_load).arity.zero? - params = [] - else - params = [current_env] - end - autoloader.files_to_load(*params).each do |file| - name = file.gsub(autoloader.path + '/', '') - begin - expanded_name = autoloader.expand(name) - absolute_name = Puppet::Util::Autoload.get_file(expanded_name, current_env) - raise("Could not find absolute path of function #{name}") if absolute_name.nil? - if path_has_child?(options[:root_path], absolute_name) # rubocop:disable Style/IfUnlessModifier Nicer to read like this - funcs.concat(load_function_file(cache, name, absolute_name, autoloader, current_env)) - end - rescue StandardError => e - PuppetLanguageServerSidecar.log_message(:error, "[PuppetHelper::load_functions] Error loading function #{file}: #{e} #{e.backtrace}") - end - end - - PuppetLanguageServerSidecar.log_message(:debug, "[PuppetHelper::load_functions] Finished loading #{funcs.count} functions") - funcs - end - def self.retrieve_types(cache, options = {}) PuppetLanguageServerSidecar.log_message(:debug, '[PuppetHelper::retrieve_types] Starting') @@ -252,44 +249,6 @@ def self.load_classes_from_manifest(cache, manifest_file) end private_class_method :load_classes_from_manifest - def self.load_function_file(cache, name, absolute_name, autoloader, env) - funcs = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new - - if cache.active? - cached_result = cache.load(absolute_name, PuppetLanguageServerSidecar::Cache::FUNCTIONS_SECTION) - unless cached_result.nil? - begin - funcs.from_json!(cached_result) - return funcs - rescue StandardError => e - PuppetLanguageServerSidecar.log_message(:warn, "[PuppetHelper::load_function_file] Error while deserializing #{absolute_name} from cache: #{e}") - funcs = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new - end - end - end - - unless autoloader.loaded?(name) - # This is an expensive call - unless autoloader.load(name, env) # rubocop:disable Style/IfUnlessModifier Nicer to read like this - PuppetLanguageServerSidecar.log_message(:error, "[PuppetHelper::load_function_file] function #{absolute_name} did not load") - end - end - - # Find the functions that were loaded based on source file name (case insensitive) - Puppet::Parser::Functions.monkey_function_list - .select { |_k, i| absolute_name.casecmp(i[:source_location][:source].to_s).zero? } - .each do |func_name, item| - obj = PuppetLanguageServerSidecar::Protocol::PuppetFunction.from_puppet(func_name, item) - obj.calling_source = absolute_name - funcs << obj - end - PuppetLanguageServerSidecar.log_message(:warn, "[PuppetHelper::load_function_file] file #{absolute_name} did load any functions") if funcs.count.zero? - cache.save(absolute_name, PuppetLanguageServerSidecar::Cache::FUNCTIONS_SECTION, funcs.to_json) if cache.active? - - funcs - end - private_class_method :load_function_file - def self.load_type_file(cache, name, absolute_name, autoloader, env) types = PuppetLanguageServer::Sidecar::Protocol::PuppetTypeList.new if cache.active? diff --git a/lib/puppet-languageserver-sidecar/puppet_monkey_patches_puppetstrings.rb b/lib/puppet-languageserver-sidecar/puppet_monkey_patches_puppetstrings.rb index f082ff78..4b092a17 100644 --- a/lib/puppet-languageserver-sidecar/puppet_monkey_patches_puppetstrings.rb +++ b/lib/puppet-languageserver-sidecar/puppet_monkey_patches_puppetstrings.rb @@ -1,54 +1,5 @@ # frozen_string_literal: true -# Monkey Patch 3.x functions so where know where they were loaded from -require 'puppet/parser/functions' -module Puppet - module Parser - module Functions - class << self - alias_method :original_newfunction, :newfunction - def newfunction(name, options = {}, &block) - # See if we've hooked elsewhere. This can happen while in debuggers (pry). If we're not in the previous caller - # stack then just use the last caller - monkey_index = Kernel.caller_locations.find_index { |loc| loc.path.match(/puppet_monkey_patches\.rb/) } - monkey_index = -1 if monkey_index.nil? - caller = Kernel.caller_locations[monkey_index + 1] - # Call the original new function method - result = original_newfunction(name, options, &block) - # Append the caller information - result[:source_location] = { - :source => caller.absolute_path, - :line => caller.lineno - 1 # Convert to a zero based line number system - } - monkey_append_function_info(name, result) - - result - end - - def monkey_clear_function_info - @monkey_function_list = {} - end - - def monkey_append_function_info(name, value) - @monkey_function_list = {} if @monkey_function_list.nil? - @monkey_function_list[name] = { - :arity => value[:arity], - :name => value[:name], - :type => value[:type], - :doc => value[:doc], - :source_location => value[:source_location] - } - end - - def monkey_function_list - @monkey_function_list = {} if @monkey_function_list.nil? - @monkey_function_list.clone - end - end - end - end -end - # Add an additional method on Puppet Types to store their source location require 'puppet/type' module Puppet @@ -80,6 +31,83 @@ def newtype(name, options = {}, &block) end end +if Gem::Version.new(Puppet.version) >= Gem::Version.new('6.0.0') + # Due to PUP-9509, need to monkey patch the cache loader + # This need to be guarded on Puppet 6.0.0+ + require 'puppet/pops/loader/module_loaders' + module Puppet + module Pops + module Loader + module ModuleLoaders + def self.cached_loader_from(parent_loader, loaders) + LibRootedFileBased.new(parent_loader, + loaders, + NAMESPACE_WILDCARD, + Puppet[:libdir], + 'cached_puppet_lib', + %i[func_4x func_3x datatype]) + end + end + end + end + end +end + +module Puppet + module Pops + module Loader + class Loader + def discover_paths(type, name_authority = Pcore::RUNTIME_NAME_AUTHORITY) + if parent.nil? + [] + else + parent.discover_paths(type, name_authority) + end + end + end + end + end +end + +module Puppet + module Pops + module Loader + class DependencyLoader + def discover_paths(type, name_authority = Pcore::RUNTIME_NAME_AUTHORITY) + result = [] + + @dependency_loaders.each { |loader| result.concat(loader.discover_paths(type, name_authority)) } + result.concat(super) + result.uniq + end + end + end + end +end + +module Puppet + module Pops + module Loader + module ModuleLoaders + class AbstractPathBasedModuleLoader + def discover_paths(type, name_authority = Pcore::RUNTIME_NAME_AUTHORITY) + result = [] + if name_authority == Pcore::RUNTIME_NAME_AUTHORITY + smart_paths.effective_paths(type).each do |sp| + relative_paths(sp).each do |rp| + result << File.join(sp.generic_path, rp) + end + end + end + result.concat(super) + result.uniq + end + end + end + end + end +end + # MUST BE LAST!!!!!! # Suppress any warning messages to STDOUT. It can pollute stdout when running in STDIO mode Puppet::Util::Log.newdesttype :null_logger do diff --git a/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb b/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb index 63226710..8d5067fa 100644 --- a/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb +++ b/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb @@ -10,6 +10,7 @@ def self.file_documentation(path) return nil unless require_puppet_strings @helper_cache = FileDocumentationCache.new if @helper_cache.nil? return @helper_cache.document(path) if @helper_cache.path_exists?(path) + PuppetLanguageServerSidecar.log_message(:debug, "[PuppetStringsHelper::file_documentation] Fetching documentation for #{path}") setup_yard! @@ -80,16 +81,62 @@ def document(path) def populate_from_yard_registry! # Extract all of the information # Ref - https://github.com/puppetlabs/puppet-strings/blob/87a8e10f45bfeb7b6b8e766324bfb126de59f791/lib/puppet-strings/json.rb#L10-L16 + populate_functions_from_yard_registry! end private + def populate_functions_from_yard_registry! + ::YARD::Registry.all(:puppet_function).map(&:to_hash).each do |item| + source_path = item[:file] + func_name = item[:name].to_s + @cache[source_path] = FileDocumentation.new(source_path) if @cache[source_path].nil? + + obj = PuppetLanguageServer::Sidecar::Protocol::PuppetFunction.new + obj.key = func_name + obj.source = item[:file] + obj.calling_source = obj.source + obj.line = item[:line] + obj.doc = item[:docstring][:text] + obj.arity = -1 # We don't care about arity + obj.function_version = item[:type] == 'ruby4x' ? 4 : 3 + + # Try and determine the function call site from the source file + char = item[:source].index(":#{func_name}") + unless char.nil? + obj.char = char + obj.length = func_name.length + 1 + end + + case item[:type] + when 'ruby3x' + obj.function_version = 3 + # This is a bit hacky but it works (probably). Puppet-Strings doesn't rip this information out, but you do have the + # the source to query + obj.type = item[:source].match(/:rvalue/) ? :rvalue : :statement + when 'ruby4x' + obj.function_version = 4 + # All ruby functions are statements + obj.type = :statement + else + PuppetLanguageServerSidecar.log_message(:error, "[#{self.class}] Unknown function type #{item[:type]}") + end + + @cache[source_path].functions << obj + end + end + end + class FileDocumentation # The path to file that has been documented - attr_reader :path + attr_accessor :path + + # PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList object holding all functions + attr_accessor :functions - def initialize(path) + def initialize(path = nil) @path = path + @functions = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new end end end diff --git a/lib/puppet-languageserver/sidecar_protocol.rb b/lib/puppet-languageserver/sidecar_protocol.rb index 6e680370..7e448429 100644 --- a/lib/puppet-languageserver/sidecar_protocol.rb +++ b/lib/puppet-languageserver/sidecar_protocol.rb @@ -154,12 +154,15 @@ class PuppetFunction < BasePuppetObject attr_accessor :doc attr_accessor :arity attr_accessor :type + # The version of this function, typically 3 or 4. + attr_accessor :function_version def to_h super.to_h.merge( - 'doc' => doc, - 'arity' => arity, - 'type' => type + 'doc' => doc, + 'arity' => arity, + 'type' => type, + 'function_version' => function_version ) end @@ -169,6 +172,7 @@ def from_h!(value) self.doc = value['doc'] self.arity = value['arity'] self.type = value['type'].intern + self.function_version = value['function_version'] self end end diff --git a/lib/puppet_languageserver_sidecar.rb b/lib/puppet_languageserver_sidecar.rb index d9992dbc..b31339f1 100644 --- a/lib/puppet_languageserver_sidecar.rb +++ b/lib/puppet_languageserver_sidecar.rb @@ -65,6 +65,11 @@ def self.require_gems(options) PuppetEditorServices.log_message(:error, "The feature flag 'puppetstrings' has been specified but it is not capable due to Puppet Strings being unavailable. Turning off the flag.") flags -= ['puppetstrings'] end + if flags.include?('puppetstrings') && Gem::Version.new(Puppet.version) < Gem::Version.new('6.0.0') + # The puppetstrings flag is only valid on Puppet 6.0.0+ + PuppetEditorServices.log_message(:error, "The feature flag 'puppetstrings' has been specified but it is not capable due to low Puppet version (< 6). Turning off the flag.") + flags -= ['puppetstrings'] + end configure_featureflags(flags) end @@ -81,6 +86,7 @@ def self.require_gems(options) if featureflag?('puppetstrings') require_list << 'puppet_helper_puppetstrings' require_list << 'puppet_monkey_patches_puppetstrings' + require_list << 'puppet_strings_helper' else require_list << 'puppet_helper' require_list << 'puppet_monkey_patches' @@ -248,6 +254,8 @@ def self.inject_workspace_as_environment end def self.execute(options) + use_puppet_strings = featureflag?('puppetstrings') + case options[:action].downcase when 'noop' [] @@ -258,7 +266,11 @@ def self.execute(options) when 'default_functions' cache = options[:disable_cache] ? PuppetLanguageServerSidecar::Cache::Null.new : PuppetLanguageServerSidecar::Cache::FileSystem.new - PuppetLanguageServerSidecar::PuppetHelper.retrieve_functions(cache) + if use_puppet_strings + PuppetLanguageServerSidecar::PuppetHelper.retrieve_via_puppet_strings(cache, :object_types => [:function])[:functions] + else + PuppetLanguageServerSidecar::PuppetHelper.retrieve_functions(cache) + end when 'default_types' cache = options[:disable_cache] ? PuppetLanguageServerSidecar::Cache::Null.new : PuppetLanguageServerSidecar::Cache::FileSystem.new @@ -298,8 +310,16 @@ def self.execute(options) when 'workspace_functions' null_cache = PuppetLanguageServerSidecar::Cache::Null.new return nil unless inject_workspace_as_module || inject_workspace_as_environment - PuppetLanguageServerSidecar::PuppetHelper.retrieve_functions(null_cache, - :root_path => PuppetLanguageServerSidecar::Workspace.root_path) + + if use_puppet_strings + cache = options[:disable_cache] ? PuppetLanguageServerSidecar::Cache::Null.new : PuppetLanguageServerSidecar::Cache::FileSystem.new + PuppetLanguageServerSidecar::PuppetHelper.retrieve_via_puppet_strings(cache, + :object_types => [:function], + :root_path => PuppetLanguageServerSidecar::Workspace.root_path)[:functions] + else + PuppetLanguageServerSidecar::PuppetHelper.retrieve_functions(null_cache, + :root_path => PuppetLanguageServerSidecar::Workspace.root_path) + end when 'workspace_types' null_cache = PuppetLanguageServerSidecar::Cache::Null.new diff --git a/spec/languageserver/spec_helper.rb b/spec/languageserver/spec_helper.rb index e3e938a0..dfe4795b 100644 --- a/spec/languageserver/spec_helper.rb +++ b/spec/languageserver/spec_helper.rb @@ -70,6 +70,7 @@ def random_sidecar_puppet_function result.doc = 'doc' + rand(1000).to_s result.arity = rand(1000) result.type = ('type' + rand(1000).to_s).intern + result.function_version = rand(1) + 3 result end diff --git a/spec/languageserver/unit/puppet-languageserver/sidecar_protocol_spec.rb b/spec/languageserver/unit/puppet-languageserver/sidecar_protocol_spec.rb index 041dfdab..8aaea921 100644 --- a/spec/languageserver/unit/puppet-languageserver/sidecar_protocol_spec.rb +++ b/spec/languageserver/unit/puppet-languageserver/sidecar_protocol_spec.rb @@ -55,7 +55,7 @@ basepuppetobject_properties = [:key, :calling_source, :source, :line, :char, :length] nodegraph_properties = [:dot_content, :error_content] puppetclass_properties = [:doc, :parameters] - puppetfunction_properties = [:doc, :arity, :type] + puppetfunction_properties = [:doc, :arity, :type, :function_version] puppettype_properties = [:doc, :attributes] resource_properties = [:manifest] From c2822d794c0d4372186501223df246d5c9983e4f Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Thu, 16 May 2019 16:31:07 +0800 Subject: [PATCH 5/7] (GH-121) Add tests for Puppet API 3 and 4 loading This commit adds test fixtures for Puppet 4 API style functions and modifies the integration tests to expect these new fixtures. --- .../puppet/functions/default_pup4_function.rb | 8 +++ .../environment/default_env_pup4_function.rb | 8 +++ .../modname/default_mod_pup4_function.rb | 8 +++ .../puppet/functions/badname/pup4_function.rb | 8 +++ .../profile/pup4_envprofile_function.rb | 8 +++ .../lib/puppet/functions/pup4_env_badfile.rb | 10 ++++ .../puppet/functions/pup4_env_badfunction.rb | 9 ++++ .../lib/puppet/functions/pup4_env_function.rb | 8 +++ .../badname/fixture_pup4_env_function.rb | 8 +++ .../puppet/functions/fixture_pup4_badfile.rb | 10 ++++ .../functions/fixture_pup4_badfunction.rb | 9 ++++ .../puppet/functions/fixture_pup4_function.rb | 8 +++ .../valid/fixture_pup4_mod_function.rb | 8 +++ .../featureflag_puppetstrings_spec.rb | 54 ++++++++++++++++++- 14 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 spec/languageserver-sidecar/fixtures/real_agent/cache/lib/puppet/functions/default_pup4_function.rb create mode 100644 spec/languageserver-sidecar/fixtures/real_agent/cache/lib/puppet/functions/environment/default_env_pup4_function.rb create mode 100644 spec/languageserver-sidecar/fixtures/real_agent/cache/lib/puppet/functions/modname/default_mod_pup4_function.rb create mode 100644 spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/badname/pup4_function.rb create mode 100644 spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/profile/pup4_envprofile_function.rb create mode 100644 spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/pup4_env_badfile.rb create mode 100644 spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/pup4_env_badfunction.rb create mode 100644 spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/pup4_env_function.rb create mode 100644 spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/badname/fixture_pup4_env_function.rb create mode 100644 spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/fixture_pup4_badfile.rb create mode 100644 spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/fixture_pup4_badfunction.rb create mode 100644 spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/fixture_pup4_function.rb create mode 100644 spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/valid/fixture_pup4_mod_function.rb diff --git a/spec/languageserver-sidecar/fixtures/real_agent/cache/lib/puppet/functions/default_pup4_function.rb b/spec/languageserver-sidecar/fixtures/real_agent/cache/lib/puppet/functions/default_pup4_function.rb new file mode 100644 index 00000000..cd958a9d --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/real_agent/cache/lib/puppet/functions/default_pup4_function.rb @@ -0,0 +1,8 @@ +# Example function using the Puppet 4 API +# This should be loaded +Puppet::Functions.create_function(:default_pup4_function) do + # @return [Array] + def default_pup4_function + 'default_pup4_function result' + end +end diff --git a/spec/languageserver-sidecar/fixtures/real_agent/cache/lib/puppet/functions/environment/default_env_pup4_function.rb b/spec/languageserver-sidecar/fixtures/real_agent/cache/lib/puppet/functions/environment/default_env_pup4_function.rb new file mode 100644 index 00000000..32accc47 --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/real_agent/cache/lib/puppet/functions/environment/default_env_pup4_function.rb @@ -0,0 +1,8 @@ +# Example function using the Puppet 4 API +# This should be loaded in the environment namespace +Puppet::Functions.create_function(:'environment::default_env_pup4_function') do + # @return [Array] + def default_env_pup4_function + 'default_env_pup4_function result' + end +end diff --git a/spec/languageserver-sidecar/fixtures/real_agent/cache/lib/puppet/functions/modname/default_mod_pup4_function.rb b/spec/languageserver-sidecar/fixtures/real_agent/cache/lib/puppet/functions/modname/default_mod_pup4_function.rb new file mode 100644 index 00000000..422ea373 --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/real_agent/cache/lib/puppet/functions/modname/default_mod_pup4_function.rb @@ -0,0 +1,8 @@ +# Example function using the Puppet 4 API +# This should be loaded in the module called 'modname' namespace +Puppet::Functions.create_function(:'modname::default_mod_pup4_function') do + # @return [Array] + def default_mod_pup4_function + 'default_env_pup4_function result' + end +end diff --git a/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/badname/pup4_function.rb b/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/badname/pup4_function.rb new file mode 100644 index 00000000..453e94ec --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/badname/pup4_function.rb @@ -0,0 +1,8 @@ +# Example function using the Puppet 4 API in a module +# This should not be loaded in the environment namespace +Puppet::Functions.create_function(:'badname::pup4_function') do + # @return [Array] + def pup4_function + 'pup4_function result' + end +end diff --git a/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/profile/pup4_envprofile_function.rb b/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/profile/pup4_envprofile_function.rb new file mode 100644 index 00000000..250d4fb1 --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/profile/pup4_envprofile_function.rb @@ -0,0 +1,8 @@ +# Example function using the Puppet 4 API in a module +# This should be loaded in the module namespace +Puppet::Functions.create_function(:'profile::pup4_envprofile_function') do + # @return [Array] + def pup4_envprofile_function + 'pup4_envprofile_function result' + end +end diff --git a/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/pup4_env_badfile.rb b/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/pup4_env_badfile.rb new file mode 100644 index 00000000..3fcc02a3 --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/pup4_env_badfile.rb @@ -0,0 +1,10 @@ +require 'a_bad_gem_that_does_not_exist' + +# Example function using the Puppet 4 API in a module +# This should not be loaded +Puppet::Functions.create_function(:pup4_env_badfile) do + # @return [Array] + def pup4_env_badfile + 'pup4_env_badfile result' + end +end diff --git a/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/pup4_env_badfunction.rb b/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/pup4_env_badfunction.rb new file mode 100644 index 00000000..ad31fdec --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/pup4_env_badfunction.rb @@ -0,0 +1,9 @@ +# Example function using the Puppet 4 API in a module +# This should be loaded but never actually successfully invoke +Puppet::Functions.create_function(:pup4_env_badfunction) do + # @return [Array] + def pup4_env_badfunction + require 'a_bad_gem_that_does_not_exist' + 'pup4_env_badfunction result' + end +end diff --git a/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/pup4_env_function.rb b/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/pup4_env_function.rb new file mode 100644 index 00000000..88435708 --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/valid_environment_workspace/site/profile/lib/puppet/functions/pup4_env_function.rb @@ -0,0 +1,8 @@ +# Example function using the Puppet 4 API in a module +# ??? This should be loaded as global namespace function +Puppet::Functions.create_function(:pup4_env_function) do + # @return [Array] + def pup4_env_function + 'pup4_env_function result' + end +end diff --git a/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/badname/fixture_pup4_env_function.rb b/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/badname/fixture_pup4_env_function.rb new file mode 100644 index 00000000..af2e8753 --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/badname/fixture_pup4_env_function.rb @@ -0,0 +1,8 @@ +# Example function using the Puppet 4 API in a module +# This should not be loaded in the module namespace +Puppet::Functions.create_function(:'badname::fixture_pup4_badname_function') do + # @return [Array] + def fixture_pup4_badname_function + 'fixture_pup4_badname_function result' + end +end diff --git a/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/fixture_pup4_badfile.rb b/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/fixture_pup4_badfile.rb new file mode 100644 index 00000000..1f429c7f --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/fixture_pup4_badfile.rb @@ -0,0 +1,10 @@ +require 'a_bad_gem_that_does_not_exist' + +# Example function using the Puppet 4 API in a module +# This should not be loaded +Puppet::Functions.create_function(:fixture_pup4_badfile) do + # @return [Array] + def fixture_pup4_badfile + 'fixture_pup4_badfile result' + end +end diff --git a/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/fixture_pup4_badfunction.rb b/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/fixture_pup4_badfunction.rb new file mode 100644 index 00000000..14f04645 --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/fixture_pup4_badfunction.rb @@ -0,0 +1,9 @@ +# Example function using the Puppet 4 API in a module +# This should be loaded but never actually successfully invoke +Puppet::Functions.create_function(:fixture_pup4_badfunction) do + # @return [Array] + def fixture_pup4_badfunction + require 'a_bad_gem_that_does_not_exist' + 'fixture_pup4_badfunction result' + end +end diff --git a/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/fixture_pup4_function.rb b/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/fixture_pup4_function.rb new file mode 100644 index 00000000..9999aafe --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/fixture_pup4_function.rb @@ -0,0 +1,8 @@ +# Example function using the Puppet 4 API in a module +# This should be loaded as global namespace function +Puppet::Functions.create_function(:fixture_pup4_function) do + # @return [Array] + def fixture_pup4_function + 'fixture_pup4_function result' + end +end diff --git a/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/valid/fixture_pup4_mod_function.rb b/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/valid/fixture_pup4_mod_function.rb new file mode 100644 index 00000000..8f60b77d --- /dev/null +++ b/spec/languageserver-sidecar/fixtures/valid_module_workspace/lib/puppet/functions/valid/fixture_pup4_mod_function.rb @@ -0,0 +1,8 @@ +# Example function using the Puppet 4 API in a module +# This should be loaded in the module namespace +Puppet::Functions.create_function(:'valid::fixture_pup4_mod_function') do + # @return [Array] + def fixture_pup4_mod_function + 'fixture_pup4_mod_function result' + end +end diff --git a/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/featureflag_puppetstrings/featureflag_puppetstrings_spec.rb b/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/featureflag_puppetstrings/featureflag_puppetstrings_spec.rb index 54407b9f..6fb4c6de 100644 --- a/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/featureflag_puppetstrings/featureflag_puppetstrings_spec.rb +++ b/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/featureflag_puppetstrings/featureflag_puppetstrings_spec.rb @@ -2,7 +2,7 @@ require 'open3' require 'tempfile' -describe 'PuppetLanguageServerSidecar with Feature Flag puppetstrings' do +describe 'PuppetLanguageServerSidecar with Feature Flag puppetstrings', :if => Gem::Version.new(Puppet.version) >= Gem::Version.new('6.0.0') do def run_sidecar(cmd_options) cmd_options << '--no-cache' @@ -48,6 +48,17 @@ def with_temporary_file(content) end end + def should_not_contain_default_functions(deserial) + # These functions should not appear in the deserialised list as they're part + # of the default function set, not the workspace. + # + # They are defined in the fixtures/real_agent/cache/lib/... + expect(deserial).to_not contain_child_with_key(:default_cache_function) + expect(deserial).to_not contain_child_with_key(:default_pup4_function) + expect(deserial).to_not contain_child_with_key(:'environment::default_env_pup4_function') + expect(deserial).to_not contain_child_with_key(:'modname::default_mod_pup4_function') + end + describe 'when running default_classes action' do let (:cmd_options) { ['--action', 'default_classes'] } @@ -81,6 +92,12 @@ def with_temporary_file(content) # These are defined in the fixtures/real_agent/cache/lib/puppet/parser/functions expect(deserial).to contain_child_with_key(:default_cache_function) + # These are defined in the fixtures/real_agent/cache/lib/puppet/functions + expect(deserial).to contain_child_with_key(:default_pup4_function) + # These are defined in the fixtures/real_agent/cache/lib/puppet/functions/environment (Special environent namespace) + expect(deserial).to contain_child_with_key(:'environment::default_env_pup4_function') + # These are defined in the fixtures/real_agent/cache/lib/puppet/functions/modname (module namespaced function) + expect(deserial).to contain_child_with_key(:'modname::default_mod_pup4_function') end end @@ -153,14 +170,33 @@ def with_temporary_file(content) deserial = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new() expect { deserial.from_json!(result) }.to_not raise_error + should_not_contain_default_functions(deserial) + + # Puppet 3 API Functions expect(deserial).to_not contain_child_with_key(:badfile) expect(deserial).to contain_child_with_key(:bad_function) expect(deserial).to contain_child_with_key(:fixture_function) + # Puppet 4 API Functions + # The strings based parser will still see 'fixture_pup4_badfile' because it's never _actually_ loaded + # in Puppet therefore it will never error + expect(deserial).to contain_child_with_key(:fixture_pup4_badfile) + # The strings based parser will still see 'badname::fixture_pup4_badname_function' because it's never _actually_ loaded + # in Puppet therefore it will never error + expect(deserial).to contain_child_with_key(:'badname::fixture_pup4_badname_function') + expect(deserial).to contain_child_with_key(:fixture_pup4_function) + expect(deserial).to contain_child_with_key(:'valid::fixture_pup4_mod_function') + expect(deserial).to contain_child_with_key(:fixture_pup4_badfunction) + # Make sure the function has the right properties func = child_with_key(deserial, :fixture_function) expect(func.doc).to eq('doc_fixture_function') expect(func.source).to match(/valid_module_workspace/) + + # Make sure the function has the right properties + func = child_with_key(deserial, :fixture_pup4_function) + expect(func.doc).to match(/Example function using the Puppet 4 API in a module/) + expect(func.source).to match(/valid_module_workspace/) end end @@ -246,12 +282,28 @@ def with_temporary_file(content) deserial = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new() expect { deserial.from_json!(result) }.to_not raise_error + should_not_contain_default_functions(deserial) + + # The strings based parser will still see 'pup4_env_badfile' because it's never _actually_ loaded + # in Puppet therefore it will never error + expect(deserial).to contain_child_with_key(:pup4_env_badfile) + # The strings based parser will still see 'badname::pup4_function' because it's never _actually_ loaded + # in Puppet therefore it will never error + expect(deserial).to contain_child_with_key(:'badname::pup4_function') expect(deserial).to contain_child_with_key(:env_function) + expect(deserial).to contain_child_with_key(:pup4_env_function) + expect(deserial).to contain_child_with_key(:pup4_env_badfunction) + expect(deserial).to contain_child_with_key(:'profile::pup4_envprofile_function') # Make sure the function has the right properties func = child_with_key(deserial, :env_function) expect(func.doc).to eq('doc_env_function') expect(func.source).to match(/valid_environment_workspace/) + + # Make sure the function has the right properties + func = child_with_key(deserial, :pup4_env_function) + expect(func.doc).to match(/Example function using the Puppet 4 API in a module/) + expect(func.source).to match(/valid_environment_workspace/) end end From 8912ed968663f80db60f4bf3f4de3965b2e476bd Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Wed, 22 May 2019 14:22:18 +0800 Subject: [PATCH 6/7] (maint) Remove redundant basemodulepath change This setting has no effect. This commit removes the redundant code. --- lib/puppet_languageserver_sidecar.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/puppet_languageserver_sidecar.rb b/lib/puppet_languageserver_sidecar.rb index b31339f1..5bcbfdd8 100644 --- a/lib/puppet_languageserver_sidecar.rb +++ b/lib/puppet_languageserver_sidecar.rb @@ -222,8 +222,6 @@ def self.init_puppet_sidecar(options) def self.inject_workspace_as_module return false unless PuppetLanguageServerSidecar::Workspace.has_module_metadata? - Puppet.settings[:basemodulepath] = Puppet.settings[:basemodulepath] + ';' + PuppetLanguageServerSidecar::Workspace.root_path - %w[puppet_modulepath_monkey_patches].each do |lib| begin require "puppet-languageserver-sidecar/#{lib}" From 3e13a8ac08d5c823668c341998d70dba8fdb94ee Mon Sep 17 00:00:00 2001 From: Glenn Sarti Date: Wed, 22 May 2019 16:25:43 +0800 Subject: [PATCH 7/7] (GH-121) Add on disk caching to Puppet Strings results Previously the results of extracting puppet metadata needed to be calculated whenever the Sidecar was run. This commit modifies the Puppet Strings helper to also use a caching mechanism to store the results and speed up metadata gathering. * Adds a clear! method to the cache, mainly for testing purposes. * Adds integration tests to ensure that objects read from cache mirror the original results * Adds serialisation and deserialisation methods to the FileDocumentation object which allows it to be cached and read back. --- .../cache/base.rb | 7 +++ .../cache/filesystem.rb | 17 ++++-- .../cache/null.rb | 4 ++ .../puppet_helper_puppetstrings.rb | 4 +- .../puppet_strings_helper.rb | 61 ++++++++++++++++++- .../featureflag_puppetstrings_spec.rb | 47 +++++++++++++- 6 files changed, 129 insertions(+), 11 deletions(-) diff --git a/lib/puppet-languageserver-sidecar/cache/base.rb b/lib/puppet-languageserver-sidecar/cache/base.rb index 65246cd6..e14e4440 100644 --- a/lib/puppet-languageserver-sidecar/cache/base.rb +++ b/lib/puppet-languageserver-sidecar/cache/base.rb @@ -5,6 +5,7 @@ module Cache CLASSES_SECTION = 'classes' FUNCTIONS_SECTION = 'functions' TYPES_SECTION = 'types' + PUPPETSTRINGS_SECTION = 'puppetstrings' class Base attr_reader :cache_options @@ -24,6 +25,12 @@ def load(_absolute_path, _section) def save(_absolute_path, _section, _content_string) raise NotImplementedError end + + # WARNING - This method is only intended for testing the cache + # and should not be used for normal operations + def clear! + raise NotImplementedError + end end end end diff --git a/lib/puppet-languageserver-sidecar/cache/filesystem.rb b/lib/puppet-languageserver-sidecar/cache/filesystem.rb index 128141b0..676e75f1 100644 --- a/lib/puppet-languageserver-sidecar/cache/filesystem.rb +++ b/lib/puppet-languageserver-sidecar/cache/filesystem.rb @@ -15,7 +15,7 @@ def initialize(_options = {}) begin Dir.mkdir(@cache_dir) unless Dir.exist?(@cache_dir) rescue Errno::ENOENT => e - PuppetLanguageServerSidecar.log_message(:error, "[PuppetLanguageServerSidecar::Cache::File] An error occured while creating file cache. Disabling cache: #{e}") + PuppetLanguageServerSidecar.log_message(:error, "[PuppetLanguageServerSidecar::Cache::FileSystem] An error occured while creating file cache. Disabling cache: #{e}") @cache_dir = nil end end @@ -37,20 +37,20 @@ def load(absolute_path, section) # Check that this is from the same language server version unless json_obj['sidecar_version'] == PuppetLanguageServerSidecar.version - PuppetLanguageServerSidecar.log_message(:debug, "[PuppetLanguageServerSidecar::load] Error loading #{absolute_path}: Expected sidecar_version version #{PuppetLanguageServerSidecar.version} but found #{json_obj['sidecar_version']}") + PuppetLanguageServerSidecar.log_message(:debug, "[PuppetLanguageServerSidecar::Cache::FileSystem.load] Error loading #{absolute_path}: Expected sidecar_version version #{PuppetLanguageServerSidecar.version} but found #{json_obj['sidecar_version']}") return nil end # Check that the source file hash matches content_hash = calculate_hash(absolute_path) if json_obj['file_hash'] != content_hash - PuppetLanguageServerSidecar.log_message(:debug, "[PuppetLanguageServerSidecar::load] Error loading #{absolute_path}: Expected file_hash of #{content_hash} but found #{json_obj['file_hash']}") + PuppetLanguageServerSidecar.log_message(:debug, "[PuppetLanguageServerSidecar::Cache::FileSystem.load] Error loading #{absolute_path}: Expected file_hash of #{content_hash} but found #{json_obj['file_hash']}") return nil end - PuppetLanguageServerSidecar.log_message(:debug, "[PuppetLanguageServerSidecar::load] Loading #{absolute_path} from cache") + PuppetLanguageServerSidecar.log_message(:debug, "[PuppetLanguageServerSidecar::Cache::FileSystem.load] Loading #{absolute_path} from cache") json_obj['data'] rescue RuntimeError => e - PuppetLanguageServerSidecar.log_message(:debug, "[PuppetLanguageServerSidecar::load] Error loading #{absolute_path}: #{e}") + PuppetLanguageServerSidecar.log_message(:debug, "[PuppetLanguageServerSidecar::Cache::FileSystem.load] Error loading #{absolute_path}: #{e}") raise end @@ -67,9 +67,16 @@ def save(absolute_path, section, content_string) content['path'] = absolute_path content['section'] = section + PuppetLanguageServerSidecar.log_message(:debug, "[PuppetLanguageServerSidecar::Cache::FileSystem.save] Saving #{absolute_path} to cache") save_file(cache_file, content.to_json) end + def clear! + return unless active? + PuppetLanguageServerSidecar.log_message(:warn, '[PuppetLanguageServerSidecar::Cache::FileSystem.clear] Filesystem based cache is being cleared') + FileUtils.rm(Dir.glob(File.join(cache_dir, '*')), :force => true) + end + private def file_key(filepath, section) diff --git a/lib/puppet-languageserver-sidecar/cache/null.rb b/lib/puppet-languageserver-sidecar/cache/null.rb index e4a0cec6..2b743125 100644 --- a/lib/puppet-languageserver-sidecar/cache/null.rb +++ b/lib/puppet-languageserver-sidecar/cache/null.rb @@ -18,6 +18,10 @@ def load(*) def save(*) true end + + def clear! + nil + end end end end diff --git a/lib/puppet-languageserver-sidecar/puppet_helper_puppetstrings.rb b/lib/puppet-languageserver-sidecar/puppet_helper_puppetstrings.rb index d3c6e55b..a54ab690 100644 --- a/lib/puppet-languageserver-sidecar/puppet_helper_puppetstrings.rb +++ b/lib/puppet-languageserver-sidecar/puppet_helper_puppetstrings.rb @@ -40,7 +40,7 @@ def self.available_documentation_types end # Retrieve objects via the Puppet 4 API loaders - def self.retrieve_via_puppet_strings(_cache, options = {}) + def self.retrieve_via_puppet_strings(cache, options = {}) PuppetLanguageServerSidecar.log_message(:debug, '[PuppetHelper::retrieve_via_puppet_strings] Starting') object_types = options[:object_types].nil? ? available_documentation_types : options[:object_types] @@ -60,7 +60,7 @@ def self.retrieve_via_puppet_strings(_cache, options = {}) paths.each do |path| next unless path_has_child?(options[:root_path], path) - file_doc = PuppetLanguageServerSidecar::PuppetStringsHelper.file_documentation(path) + file_doc = PuppetLanguageServerSidecar::PuppetStringsHelper.file_documentation(path, cache) next if file_doc.nil? if object_types.include?(:function) # rubocop:disable Style/IfUnlessModifier This reads better diff --git a/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb b/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb index 8d5067fa..79b699dd 100644 --- a/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb +++ b/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb @@ -5,12 +5,17 @@ module PuppetStringsHelper # Returns a FileDocumentation object for a given path # # @param [String] path The absolute path to the file that will be documented + # @param [PuppetLanguageServerSidecar::Cache] cache A Sidecar cache which stores already parsed documents as serialised FileDocumentation objects # @return [FileDocumentation, nil] Returns the documentation for the path, or nil if it cannot be extracted - def self.file_documentation(path) + def self.file_documentation(path, cache = nil) return nil unless require_puppet_strings @helper_cache = FileDocumentationCache.new if @helper_cache.nil? return @helper_cache.document(path) if @helper_cache.path_exists?(path) + # Load from the permanent cache + @helper_cache.populate_from_sidecar_cache!(path, cache) unless cache.nil? || !cache.active? + return @helper_cache.document(path) if @helper_cache.path_exists?(path) + PuppetLanguageServerSidecar.log_message(:debug, "[PuppetStringsHelper::file_documentation] Fetching documentation for #{path}") setup_yard! @@ -36,6 +41,9 @@ def self.file_documentation(path) # Populate the documentation cache from the YARD information @helper_cache.populate_from_yard_registry! + # Save to the permanent cache + @helper_cache.save_to_sidecar_cache(path, cache) unless cache.nil? || !cache.active? + # Return the documentation details @helper_cache.document(path) end @@ -84,6 +92,22 @@ def populate_from_yard_registry! populate_functions_from_yard_registry! end + def populate_from_sidecar_cache!(path, cache) + cached_result = cache.load(path, PuppetLanguageServerSidecar::Cache::PUPPETSTRINGS_SECTION) + unless cached_result.nil? # rubocop:disable Style/GuardClause Reads better this way + begin + obj = FileDocumentation.new.from_json!(cached_result) + @cache[path] = obj + rescue StandardError => e + PuppetLanguageServerSidecar.log_message(:warn, "[FileDocumentationCache::populate_from_sidecar_cache!] Error while deserializing #{path} from cache: #{e}") + end + end + end + + def save_to_sidecar_cache(path, cache) + cache.save(path, PuppetLanguageServerSidecar::Cache::PUPPETSTRINGS_SECTION, document(path).to_json) if cache.active? + end + private def populate_functions_from_yard_registry! @@ -138,5 +162,40 @@ def initialize(path = nil) @path = path @functions = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new end + + # Serialisation + def to_h + { + 'path' => path, + 'functions' => functions + } + end + + def to_json(*options) + JSON.generate(to_h, options) + end + + # Deserialisation + def from_json!(json_string) + obj = JSON.parse(json_string) + + obj.keys.each do |key| + case key + when 'path' + # Simple deserialised object types + self.instance_variable_set("@#{key}", obj[key]) # rubocop:disable Style/RedundantSelf Reads better this way + else + # Sidecar protocol list object types + prop = self.instance_variable_get("@#{key}") # rubocop:disable Style/RedundantSelf Reads better this way + + obj[key].each do |child_hash| + child = prop.child_type.new + # Let the sidecar deserialise for us + prop << child.from_h!(child_hash) + end + end + end + self + end end end diff --git a/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/featureflag_puppetstrings/featureflag_puppetstrings_spec.rb b/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/featureflag_puppetstrings/featureflag_puppetstrings_spec.rb index 6fb4c6de..a5cecbaa 100644 --- a/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/featureflag_puppetstrings/featureflag_puppetstrings_spec.rb +++ b/spec/languageserver-sidecar/integration/puppet-languageserver-sidecar/featureflag_puppetstrings/featureflag_puppetstrings_spec.rb @@ -4,8 +4,6 @@ describe 'PuppetLanguageServerSidecar with Feature Flag puppetstrings', :if => Gem::Version.new(Puppet.version) >= Gem::Version.new('6.0.0') do def run_sidecar(cmd_options) - cmd_options << '--no-cache' - # Append the feature flag cmd_options << '--feature-flag=puppetstrings' @@ -59,6 +57,37 @@ def should_not_contain_default_functions(deserial) expect(deserial).to_not contain_child_with_key(:'modname::default_mod_pup4_function') end + before(:each) do + # Purge the File Cache + cache = PuppetLanguageServerSidecar::Cache::FileSystem.new + cache.clear! + end + + after(:all) do + # Purge the File Cache + cache = PuppetLanguageServerSidecar::Cache::FileSystem.new + cache.clear! + end + + def expect_empty_cache + cache = PuppetLanguageServerSidecar::Cache::FileSystem.new + expect(Dir.exists?(cache.cache_dir)).to eq(true), "Expected the cache directory #{cache.cache_dir} to exist" + expect(Dir.glob(File.join(cache.cache_dir,'*')).count).to eq(0), "Expected the cache directory #{cache.cache_dir} to be empty" + end + + def expect_populated_cache + cache = PuppetLanguageServerSidecar::Cache::FileSystem.new + expect(Dir.glob(File.join(cache.cache_dir,'*')).count).to be > 0, "Expected the cache directory #{cache.cache_dir} to be populated" + end + + def expect_same_array_content(a, b) + expect(a.count).to eq(b.count), "Expected array with #{b.count} items to have #{a.count} items" + + a.each_with_index do |item, index| + expect(item.to_json).to eq(b[index].to_json), "Expected item at index #{index} to have content #{item.to_json} but got #{b[index].to_json}" + end + end + describe 'when running default_classes action' do let (:cmd_options) { ['--action', 'default_classes'] } @@ -79,7 +108,9 @@ def should_not_contain_default_functions(deserial) describe 'when running default_functions action' do let (:cmd_options) { ['--action', 'default_functions'] } - it 'should return a deserializable function list with default functions' do + it 'should return a cachable deserializable function list with default functions' do + expect_empty_cache + result = run_sidecar(cmd_options) deserial = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new() expect { deserial.from_json!(result) }.to_not raise_error @@ -98,6 +129,16 @@ def should_not_contain_default_functions(deserial) expect(deserial).to contain_child_with_key(:'environment::default_env_pup4_function') # These are defined in the fixtures/real_agent/cache/lib/puppet/functions/modname (module namespaced function) expect(deserial).to contain_child_with_key(:'modname::default_mod_pup4_function') + + # Now run using cached information + expect_populated_cache + + result2 = run_sidecar(cmd_options) + deserial2 = PuppetLanguageServer::Sidecar::Protocol::PuppetFunctionList.new() + expect { deserial2.from_json!(result2) }.to_not raise_error + + # The second result should be the same as the first + expect_same_array_content(deserial, deserial2) end end