puppetfile-resolver 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/README.md +169 -0
  4. data/lib/puppetfile-resolver.rb +7 -0
  5. data/lib/puppetfile-resolver/cache/base.rb +28 -0
  6. data/lib/puppetfile-resolver/cache/persistent.rb +50 -0
  7. data/lib/puppetfile-resolver/data/ruby_ca_certs.pem +3432 -0
  8. data/lib/puppetfile-resolver/models.rb +8 -0
  9. data/lib/puppetfile-resolver/models/missing_module_specification.rb +27 -0
  10. data/lib/puppetfile-resolver/models/module_dependency.rb +55 -0
  11. data/lib/puppetfile-resolver/models/module_specification.rb +114 -0
  12. data/lib/puppetfile-resolver/models/puppet_dependency.rb +34 -0
  13. data/lib/puppetfile-resolver/models/puppet_specification.rb +25 -0
  14. data/lib/puppetfile-resolver/models/puppetfile_dependency.rb +14 -0
  15. data/lib/puppetfile-resolver/puppetfile.rb +22 -0
  16. data/lib/puppetfile-resolver/puppetfile/base_module.rb +62 -0
  17. data/lib/puppetfile-resolver/puppetfile/document.rb +125 -0
  18. data/lib/puppetfile-resolver/puppetfile/forge_module.rb +14 -0
  19. data/lib/puppetfile-resolver/puppetfile/git_module.rb +19 -0
  20. data/lib/puppetfile-resolver/puppetfile/invalid_module.rb +16 -0
  21. data/lib/puppetfile-resolver/puppetfile/local_module.rb +14 -0
  22. data/lib/puppetfile-resolver/puppetfile/parser/errors.rb +19 -0
  23. data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval.rb +133 -0
  24. data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/dsl.rb +51 -0
  25. data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/module/forge.rb +50 -0
  26. data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/module/git.rb +32 -0
  27. data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/module/invalid.rb +27 -0
  28. data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/module/local.rb +26 -0
  29. data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/module/svn.rb +30 -0
  30. data/lib/puppetfile-resolver/puppetfile/parser/r10k_eval/puppet_module.rb +36 -0
  31. data/lib/puppetfile-resolver/puppetfile/svn_module.rb +16 -0
  32. data/lib/puppetfile-resolver/puppetfile/validation_errors.rb +106 -0
  33. data/lib/puppetfile-resolver/resolution_provider.rb +182 -0
  34. data/lib/puppetfile-resolver/resolution_result.rb +30 -0
  35. data/lib/puppetfile-resolver/resolver.rb +77 -0
  36. data/lib/puppetfile-resolver/spec_searchers/common.rb +15 -0
  37. data/lib/puppetfile-resolver/spec_searchers/forge.rb +75 -0
  38. data/lib/puppetfile-resolver/spec_searchers/git.rb +64 -0
  39. data/lib/puppetfile-resolver/spec_searchers/local.rb +45 -0
  40. data/lib/puppetfile-resolver/ui/debug_ui.rb +15 -0
  41. data/lib/puppetfile-resolver/ui/null_ui.rb +20 -0
  42. data/lib/puppetfile-resolver/util.rb +23 -0
  43. data/lib/puppetfile-resolver/version.rb +5 -0
  44. data/puppetfile-cli.rb +101 -0
  45. data/spec/spec_helper.rb +48 -0
  46. data/spec/unit/puppetfile-resolver/puppetfile/document_spec.rb +316 -0
  47. data/spec/unit/puppetfile-resolver/puppetfile/parser/r10k_eval_spec.rb +460 -0
  48. data/spec/unit/puppetfile-resolver/resolver_spec.rb +421 -0
  49. metadata +124 -0
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetfileResolver
4
+ module SpecSearchers
5
+ module Common
6
+ def self.dependency_cache_id(caller, dependency)
7
+ "#{caller}-#{dependency.owner}-#{dependency.name}"
8
+ end
9
+
10
+ def self.specification_cache_id(caller, specification)
11
+ "#{caller}-#{specification.owner}-#{specification.name}-#{specification.version}"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppetfile-resolver/spec_searchers/common'
4
+ require 'uri'
5
+ require 'net/http'
6
+ require 'openssl'
7
+ require 'json'
8
+
9
+ module PuppetfileResolver
10
+ module SpecSearchers
11
+ module Forge
12
+ DEFAULT_FORGE_URI ||= 'https://forgeapi.puppet.com'
13
+
14
+ def self.find_all(dependency, cache, resolver_ui)
15
+ dep_id = ::PuppetfileResolver::SpecSearchers::Common.dependency_cache_id(self, dependency)
16
+
17
+ # Has the information been cached?
18
+ return cache.load(dep_id) if cache.exist?(dep_id)
19
+
20
+ result = []
21
+ # Query the forge
22
+ fetch_all_module_releases(dependency.owner, dependency.name, resolver_ui) do |partial_releases|
23
+ partial_releases.each do |release|
24
+ result << Models::ModuleSpecification.new(
25
+ name: release['module']['name'],
26
+ owner: release['module']['owner']['slug'],
27
+ origin: :forge,
28
+ version: release['version'],
29
+ metadata: ::PuppetfileResolver::Util.symbolise_object(release['metadata'])
30
+ )
31
+ end
32
+ end
33
+
34
+ cache.save(dep_id, result)
35
+ result
36
+ end
37
+
38
+ def self.fetch_all_module_releases(owner, name, forge_api_url = DEFAULT_FORGE_URI, resolver_ui, &block)
39
+ raise 'Requires a block to yield' unless block
40
+ uri = ::URI.parse("#{forge_api_url}/v3/releases")
41
+ params = { :module => "#{owner}-#{name}", :exclude_fields => 'readme changelog license reference tasks', :limit => 50 }
42
+ uri.query = ::URI.encode_www_form(params)
43
+
44
+ loops = 0
45
+ loop do
46
+ resolver_ui.debug { "Querying the forge for a module with #{uri}" }
47
+
48
+ http_options = { :use_ssl => uri.class == URI::HTTPS }
49
+ # Because on Windows Ruby doesn't use the Windows certificate store which has up-to date
50
+ # CA certs, we can't depend on someone setting the environment variable correctly. So use our
51
+ # static CA PEM file if SSL_CERT_FILE is not set.
52
+ http_options[:ca_file] = PuppetfileResolver::Util.static_ca_cert_file if ENV['SSL_CERT_FILE'].nil?
53
+
54
+ response = nil
55
+ Net::HTTP.start(uri.host, uri.port, http_options) do |http|
56
+ request = Net::HTTP::Get.new uri
57
+ response = http.request request
58
+ end
59
+ raise "Expected HTTP Code 200, but received #{response.code} for URI #{uri}: #{response.inspect}" unless response.code == '200'
60
+
61
+ reply = ::JSON.parse(response.body)
62
+ yield reply['results']
63
+
64
+ break if reply['pagination'].nil? || reply['pagination']['next'].nil?
65
+ uri = ::URI.parse("#{forge_api_url}#{reply['pagination']['next']}")
66
+
67
+ # Circuit breaker in case the worst happens (max 1000 module releases)
68
+ loops += 1
69
+ raise "Too many Forge API requests #{loops * 50}" if loops > 20
70
+ end
71
+ end
72
+ private_class_method :fetch_all_module_releases
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppetfile-resolver/spec_searchers/common'
4
+
5
+ module PuppetfileResolver
6
+ module SpecSearchers
7
+ module Git
8
+ def self.find_all(puppetfile_module, dependency, cache, resolver_ui)
9
+ dep_id = ::PuppetfileResolver::SpecSearchers::Common.dependency_cache_id(self, dependency)
10
+ # Has the information been cached?
11
+ return cache.load(dep_id) if cache.exist?(dep_id)
12
+
13
+ # We _could_ git clone this, but it'll take too long. So for now, just
14
+ # try and resolve github based repositories by crafting a URL
15
+
16
+ repo_url = nil
17
+ if puppetfile_module.remote.start_with?('git@github.com:')
18
+ repo_url = puppetfile_module.remote.slice(15..-1)
19
+ repo_url = repo_url.slice(0..-5) if repo_url.end_with?('.git')
20
+ end
21
+ if puppetfile_module.remote.start_with?('https://github.com/')
22
+ repo_url = puppetfile_module.remote.slice(19..-1)
23
+ repo_url = repo_url.slice(0..-5) if repo_url.end_with?('.git')
24
+ end
25
+
26
+ return [] if repo_url.nil?
27
+
28
+ metadata_url = 'https://raw.githubusercontent.com/' + repo_url + '/'
29
+ if puppetfile_module.ref
30
+ metadata_url += puppetfile_module.ref + '/'
31
+ elsif puppetfile_module.tag
32
+ metadata_url += puppetfile_module.tag + '/'
33
+ else
34
+ # Default to master. Should it raise?
35
+ metadata_url += 'master/'
36
+ end
37
+ metadata_url += 'metadata.json'
38
+
39
+ require 'net/http'
40
+ require 'uri'
41
+
42
+ resolver_ui.debug { "Querying the Github with #{metadata_url}" }
43
+ response = Net::HTTP.get_response(URI.parse(metadata_url))
44
+ if response.code != '200'
45
+ cache.save(dep_id, [])
46
+ return []
47
+ end
48
+
49
+ # TODO: symbolise_object should be in a Util namespace
50
+ metadata = ::PuppetfileResolver::Util.symbolise_object(::JSON.parse(response.body))
51
+ result = [Models::ModuleSpecification.new(
52
+ name: metadata[:name],
53
+ origin: :git,
54
+ version: metadata[:version],
55
+ metadata: metadata
56
+ )]
57
+
58
+ cache.save(dep_id, result)
59
+
60
+ result
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppetfile-resolver/spec_searchers/common'
4
+
5
+ module PuppetfileResolver
6
+ module SpecSearchers
7
+ module Local
8
+ def self.find_all(_puppetfile_module, puppet_module_paths, dependency, cache, resolver_ui)
9
+ dep_id = ::PuppetfileResolver::SpecSearchers::Common.dependency_cache_id(self, dependency)
10
+ # Has the information been cached?
11
+ return cache.load(dep_id) if cache.exist?(dep_id)
12
+
13
+ result = []
14
+ # Find the module in the modulepaths
15
+ puppet_module_paths.each do |module_path|
16
+ next unless Dir.exist?(module_path)
17
+ module_dir = File.expand_path(File.join(module_path, dependency.name))
18
+ next unless Dir.exist?(module_dir)
19
+ metadata_file = File.join(module_dir, 'metadata.json')
20
+ next unless File.exist?(metadata_file)
21
+
22
+ metadata = nil
23
+ begin
24
+ metadata = ::PuppetfileResolver::Util.symbolise_object(
25
+ ::JSON.parse(File.open(metadata_file, 'rb:utf-8') { |f| f.read })
26
+ )
27
+ rescue StandardError => _e # rubocop:disable Lint/SuppressedException Todo
28
+ # TODO: Should really do something?
29
+ end
30
+ resolver_ui.debug { "Found local module at #{metadata_file}" }
31
+
32
+ result << Models::ModuleSpecification.new(
33
+ name: metadata[:name],
34
+ origin: :local,
35
+ version: metadata[:version],
36
+ metadata: metadata
37
+ )
38
+ end
39
+ cache.save(dep_id, result)
40
+
41
+ result
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'molinillo'
4
+
5
+ module PuppetfileResolver
6
+ module UI
7
+ class DebugUI
8
+ include Molinillo::UI
9
+
10
+ def debug?
11
+ true
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'molinillo'
4
+
5
+ module PuppetfileResolver
6
+ module UI
7
+ class NullUI
8
+ include Molinillo::UI
9
+
10
+ # Suppress all output
11
+ def output
12
+ @output ||= File.open(File::NULL, 'w')
13
+ end
14
+
15
+ def debug?
16
+ false
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetfileResolver
4
+ module Util
5
+ def self.symbolise_object(object)
6
+ case # rubocop:disable Style/EmptyCaseCondition Ignore
7
+ when object.is_a?(Hash)
8
+ object.inject({}) do |memo, (k, v)| # rubocop:disable Style/EachWithObject Ignore
9
+ memo[k.to_sym] = symbolise_object(v)
10
+ memo
11
+ end
12
+ when object.is_a?(Array)
13
+ object.map { |i| symbolise_object(i) }
14
+ else
15
+ object
16
+ end
17
+ end
18
+
19
+ def self.static_ca_cert_file
20
+ @static_ca_cert_file ||= File.expand_path(File.join(__dir__, 'data', 'ruby_ca_certs.pem'))
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetfileResolver
4
+ VERSION ||= '0.1.0'
5
+ end
data/puppetfile-cli.rb ADDED
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is not supposed to be a fully fledged CLI for the resolver, as this gem is designed to be used
4
+ # as a library. The CLI is offered as an example as to how to use the library.
5
+
6
+ # Add the resolver into the load path
7
+ lib_root = File.expand_path(File.join(__dir__, 'lib'))
8
+ $LOAD_PATH.unshift(lib_root)
9
+
10
+ require 'puppetfile-resolver'
11
+ require 'optparse'
12
+
13
+ class CommandLineParser
14
+ def self.parse(options)
15
+ # Set defaults here
16
+ args = {
17
+ debug: false,
18
+ cache_dir: nil,
19
+ module_paths: [],
20
+ path: nil,
21
+ puppet_version: nil,
22
+ strict: false
23
+ }
24
+
25
+ opt_parser = OptionParser.new do |opts|
26
+ opts.banner = 'Usage: puppetfile-cli.rb [options]'
27
+
28
+ opts.on('-pPATH', '--path=PATH', 'Puppetfile to parse') do |path|
29
+ args[:path] = path
30
+ end
31
+
32
+ opts.on('-vVERSION', '--puppet_version=VERSION', 'Restrict the resolver to only modules which support the specified Puppet version') do |version|
33
+ args[:puppet_version] = version
34
+ end
35
+
36
+ opts.on('-cDIR', '--cache_directory=DIR', 'Directory to the persistent on disk cache. Optional') do |cache_dir|
37
+ args[:cache_dir] = cache_dir
38
+ end
39
+
40
+ opts.on('--debug', 'Output debug information. Default is no debug output') do
41
+ args[:debug] = true
42
+ end
43
+
44
+ opts.on('-s', '--strict', 'Do not allow missing dependencies. Default false which marks dependencies as missing and does not raise an error.') do
45
+ args[:strict] = true
46
+ end
47
+
48
+ opts.on('-mTEXT', '--module_paths=TEXT', Array, 'Comma delimited list of modules paths to search') do |text|
49
+ args[:module_paths] = text
50
+ end
51
+ end
52
+
53
+ opt_parser.parse!(options.dup)
54
+ args
55
+ end
56
+ end
57
+
58
+ options = CommandLineParser.parse(ARGV)
59
+ raise 'Missing --path' if options[:path].nil?
60
+
61
+ # Configure the cache
62
+ if options[:cache_dir].nil?
63
+ cache = nil
64
+ else
65
+ require 'puppetfile-resolver/cache/persistent'
66
+ cache = PuppetfileResolver::Cache::Persistent.new(options[:cache_dir])
67
+ end
68
+
69
+ # Parse the Puppetfile into an object model
70
+ content = File.open(options[:path], 'rb') { |f| f.read }
71
+ require 'puppetfile-resolver/puppetfile/parser/r10k_eval'
72
+ puppetfile = ::PuppetfileResolver::Puppetfile::Parser::R10KEval.parse(content)
73
+
74
+ # Make sure the Puppetfile is valid
75
+ unless puppetfile.valid?
76
+ puts 'Puppetfile is not valid'
77
+ puppetfile.validation_errors.each { |err| puts err }
78
+ exit 1
79
+ end
80
+
81
+ # Create the resolver
82
+ resolver = PuppetfileResolver::Resolver.new(puppetfile, options[:puppet_version])
83
+
84
+ # Configure the resolver
85
+ if options[:debug]
86
+ require 'puppetfile-resolver/ui/debug_ui'
87
+ ui = PuppetfileResolver::UI::DebugUI.new
88
+ else
89
+ ui = nil
90
+ end
91
+ opts = { cache: cache, ui: ui, module_paths: options[:module_paths], allow_missing_modules: !options[:strict] }
92
+
93
+ # Resolve
94
+ result = resolver.resolve(opts)
95
+
96
+ # Output errors
97
+ result.validation_errors.each { |err| puts "Resolution Validation Error: #{err}\n" }
98
+
99
+ # Output the Graph in a DOT format
100
+ puts "\n--- Dependency Graph"
101
+ puts result.to_dot
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ root = File.join(__dir__,'..',)
4
+ # Add the language server into the load path
5
+ $LOAD_PATH.unshift(File.join(root,'lib'))
6
+
7
+ # A cache which we can preload for the purposes of testing, mimicing
8
+ # Local modules
9
+ require 'puppetfile-resolver/cache/base'
10
+ class MockLocalModuleCache < PuppetfileResolver::Cache::Base
11
+ def initialize
12
+ super(nil)
13
+
14
+ @mock_modules = {}
15
+ end
16
+
17
+ def exist?(name)
18
+ result = super
19
+ return result if result
20
+ @mock_modules.key?(name)
21
+ end
22
+
23
+ def load(name)
24
+ result = super
25
+ return result unless result.nil?
26
+
27
+ @mock_modules[name]
28
+ end
29
+
30
+ def add_local_module_spec(name, dependencies = [], puppet_requirement = nil, version = '1.0.0')
31
+ requirements = []
32
+ requirements << { name: 'puppet', version_requirement: puppet_requirement } unless puppet_requirement.nil?
33
+ spec = PuppetfileResolver::Models::ModuleSpecification.new(
34
+ name: name,
35
+ origin: :local,
36
+ version: version,
37
+ metadata: {
38
+ dependencies: dependencies,
39
+ requirements: requirements
40
+ }
41
+ )
42
+ # Note - This is quite implementation dependant so could be fragile.
43
+ # Yes, I know it's expecting a Dependency object, but the spec is fine.
44
+ id = PuppetfileResolver::SpecSearchers::Common.dependency_cache_id(PuppetfileResolver::SpecSearchers::Local, spec)
45
+ @mock_modules[id] = [] if @mock_modules[id].nil?
46
+ @mock_modules[id] << spec
47
+ end
48
+ end
@@ -0,0 +1,316 @@
1
+ require 'spec_helper'
2
+
3
+ require 'puppetfile-resolver/models'
4
+ require 'puppetfile-resolver/resolution_result'
5
+ require 'puppetfile-resolver/puppetfile'
6
+
7
+ describe 'PuppetfileResolver::Puppetfile::Document' do
8
+ let(:puppetfile_content) { 'puppetfile' }
9
+ let(:subject) { PuppetfileResolver::Puppetfile::Document.new(puppetfile_content) }
10
+
11
+ describe "Document Validation" do
12
+ context 'Given an empty document' do
13
+ before(:each) do
14
+ subject.clear_modules
15
+ end
16
+
17
+ it 'should be valid' do
18
+ expect(subject.valid?).to eq(true)
19
+ end
20
+
21
+ it 'should have no validation errors' do
22
+ expect(subject.validation_errors).to eq([])
23
+ end
24
+ end
25
+
26
+ context 'Given a document with valid modules' do
27
+ let(:module1) { PuppetfileResolver::Puppetfile::LocalModule.new('foo') }
28
+ let(:module2) { PuppetfileResolver::Puppetfile::LocalModule.new('bar') }
29
+ let(:module3) { PuppetfileResolver::Puppetfile::LocalModule.new('baz') }
30
+
31
+ before(:each) do
32
+ subject.clear_modules
33
+ subject.add_module(module1)
34
+ subject.add_module(module2)
35
+ subject.add_module(module3)
36
+ end
37
+
38
+ it 'should be valid' do
39
+ expect(subject.valid?).to eq(true)
40
+ end
41
+
42
+ it 'should have no validation errors' do
43
+ expect(subject.validation_errors).to eq([])
44
+ end
45
+ end
46
+
47
+ context 'Given a document with an invalid module' do
48
+ let(:invalid_module) do
49
+ PuppetfileResolver::Puppetfile::InvalidModule.new('foo-invalid').tap { |m| m.reason = 'MockReason' }
50
+ end
51
+
52
+ before(:each) do
53
+ subject.clear_modules
54
+ subject.add_module(invalid_module)
55
+ end
56
+
57
+ it 'should not be valid' do
58
+ expect(subject.valid?).to eq(false)
59
+ end
60
+
61
+ it 'should have a validation error' do
62
+ expect(subject.validation_errors.count).to eq (1)
63
+ expect(subject.validation_errors[0]).to be_a(PuppetfileResolver::Puppetfile::DocumentInvalidModuleError)
64
+ expect(subject.validation_errors[0]).to have_attributes(
65
+ :message => 'MockReason',
66
+ :puppet_module => invalid_module
67
+ )
68
+ end
69
+ end
70
+
71
+ context 'Given a document with duplicate modules' do
72
+ let(:module1) { PuppetfileResolver::Puppetfile::LocalModule.new('foo') }
73
+ let(:module2) { PuppetfileResolver::Puppetfile::LocalModule.new('foo') }
74
+ let(:module3) { PuppetfileResolver::Puppetfile::LocalModule.new('foo') }
75
+ let(:module4) { PuppetfileResolver::Puppetfile::LocalModule.new('bar') }
76
+ let(:module5) { PuppetfileResolver::Puppetfile::LocalModule.new('bar') }
77
+ let(:module6) { PuppetfileResolver::Puppetfile::LocalModule.new('baz') }
78
+
79
+ before(:each) do
80
+ subject.clear_modules
81
+ subject.add_module(module1)
82
+ subject.add_module(module2)
83
+ subject.add_module(module3)
84
+ subject.add_module(module4)
85
+ subject.add_module(module5)
86
+ subject.add_module(module6)
87
+ end
88
+
89
+ it 'should not be valid' do
90
+ expect(subject.valid?).to eq(false)
91
+ end
92
+
93
+ it 'should have validation errors' do
94
+ expect(subject.validation_errors.count).to eq (2)
95
+ # Duplication foo module
96
+ err = subject.validation_errors[0]
97
+ expect(err).to be_a(PuppetfileResolver::Puppetfile::DocumentDuplicateModuleError)
98
+ expect(err).to have_attributes(
99
+ :message => /foo/,
100
+ :puppet_module => module1,
101
+ :duplicates => [module2, module3]
102
+ )
103
+ # Duplicate bar module
104
+ err = subject.validation_errors[1]
105
+ expect(err).to be_a(PuppetfileResolver::Puppetfile::DocumentDuplicateModuleError)
106
+ expect(err).to have_attributes(
107
+ :message => /bar/,
108
+ :puppet_module => module4,
109
+ :duplicates => [module5]
110
+ )
111
+ end
112
+ end
113
+ end
114
+
115
+ describe ".resolution_validation_errors" do
116
+ # Array of modules to add to the document
117
+ let(:document_fixtures) { [] }
118
+ let(:resolution_graph) { Molinillo::DependencyGraph.new }
119
+ let(:resolution_result) { PuppetfileResolver::ResolutionResult.new(resolution_graph, subject) }
120
+ let(:resolution_specifications) { [] }
121
+ let(:resolution_dependencies) { [] }
122
+
123
+ before(:each) do
124
+ # Generate the Puppetfile document
125
+ subject.clear_modules
126
+ @document_modules = document_fixtures.map do |doc_mod|
127
+ mod = doc_mod[:class].new(doc_mod[:name])
128
+ mod.version = doc_mod[:version]
129
+ subject.add_module(mod)
130
+ mod
131
+ end
132
+
133
+ # Generate the Dependency Graph
134
+ @module_specifications = {}
135
+ # Create all of the specifications
136
+ resolution_specifications.each do |res_spec|
137
+ res_spec[:class] = PuppetfileResolver::Models::ModuleSpecification if res_spec[:class].nil?
138
+ @module_specifications[res_spec[:name]] = res_spec[:class].new(name: res_spec[:name], origin: res_spec[:origin], metadata: {}, version: res_spec[:version])
139
+ end
140
+ # Create all of the vertexes in the graph
141
+ @module_specifications.each { |name, spec| resolution_graph.add_vertex(name, spec, false) }
142
+ # Create all of the edges in the graph
143
+ resolution_dependencies.each do |dep|
144
+ dep[:class] = PuppetfileResolver::Models::ModuleDependency if dep[:class].nil?
145
+ mod_dep = dep[:class].new(name: dep[:destination], version_requirement: dep[:version_requirement])
146
+ resolution_graph.add_edge(
147
+ resolution_graph.vertex_named(dep[:origin]),
148
+ resolution_graph.vertex_named(dep[:destination]),
149
+ mod_dep
150
+ )
151
+ end
152
+ end
153
+
154
+ context 'Given an invalid document' do
155
+ before(:each) do
156
+ allow(subject).to receive(:valid?).and_return(false)
157
+ end
158
+
159
+ it 'should raise' do
160
+ expect{ subject.resolution_validation_errors(resolution_result) }.to raise_error(/invalid document/)
161
+ end
162
+ end
163
+
164
+ context 'Given a valid resolution' do
165
+ let(:document_fixtures) do
166
+ [
167
+ { class: PuppetfileResolver::Puppetfile::LocalModule, name: 'foo' },
168
+ { class: PuppetfileResolver::Puppetfile::LocalModule, name: 'bar' }
169
+ ]
170
+ end
171
+ let(:resolution_specifications) do
172
+ [
173
+ { name: 'foo', origin: :local, version: '1.0.0' },
174
+ { name: 'bar', origin: :local, version: '1.0.0' }
175
+ ]
176
+ end
177
+ let(:resolution_dependencies) do
178
+ [
179
+ { origin: 'foo', destination: 'bar', version_requirement: '>= 0' },
180
+ ]
181
+ end
182
+ it 'should return no errors' do
183
+ expect(subject.resolution_validation_errors(resolution_result)).to be_empty
184
+ end
185
+ end
186
+
187
+ context 'Given a document with :latest' do
188
+ let(:document_fixtures) do
189
+ [
190
+ { class: PuppetfileResolver::Puppetfile::ForgeModule, name: 'foo', version: :latest }
191
+ ]
192
+ end
193
+ let(:resolution_specifications) do
194
+ [
195
+ { name: 'foo', origin: :forge, version: '1.0.0' }
196
+ ]
197
+ end
198
+
199
+ it 'should return a DocumentLatestVersionError' do
200
+ errors = subject.resolution_validation_errors(resolution_result)
201
+ expect(errors.count).to eq(1)
202
+ error = errors[0]
203
+ expect(error).to be_a(PuppetfileResolver::Puppetfile::DocumentLatestVersionError)
204
+ expect(error).to have_attributes(
205
+ message: /foo/,
206
+ module_specification: @module_specifications['foo'],
207
+ puppet_module: @document_modules[0]
208
+ )
209
+ end
210
+ end
211
+
212
+ context 'Given a document with a missing module' do
213
+ let(:document_fixtures) do
214
+ [
215
+ { class: PuppetfileResolver::Puppetfile::LocalModule, name: 'foo', version: :latest }
216
+ ]
217
+ end
218
+ let(:resolution_specifications) do
219
+ [
220
+ { class: PuppetfileResolver::Models::MissingModuleSpecification, name: 'foo' }
221
+ ]
222
+ end
223
+
224
+ it 'should return a DocumentMissingModuleError' do
225
+ errors = subject.resolution_validation_errors(resolution_result)
226
+ expect(errors.count).to eq(1)
227
+ error = errors[0]
228
+ expect(error).to be_a(PuppetfileResolver::Puppetfile::DocumentMissingModuleError)
229
+ expect(error).to have_attributes(
230
+ message: /foo/,
231
+ module_specification: @module_specifications['foo'],
232
+ puppet_module: @document_modules[0]
233
+ )
234
+ end
235
+ end
236
+
237
+ context 'Given a document with missing module dependencies' do
238
+ # We setup a Puppetfile that looks like
239
+ # ```
240
+ # mod 'foo', :local => true
241
+ # mod 'baz', :local => true
242
+ # mod 'waldo', :local => true
243
+ # ```
244
+ let(:document_fixtures) do
245
+ [
246
+ { class: PuppetfileResolver::Puppetfile::LocalModule, name: 'foo', version: '1.0.0' },
247
+ { class: PuppetfileResolver::Puppetfile::LocalModule, name: 'baz', version: '1.0.0' },
248
+ { class: PuppetfileResolver::Puppetfile::LocalModule, name: 'waldo', version: '1.0.0' },
249
+ ]
250
+ end
251
+ # We then setup a resolution graph that looks like
252
+ #
253
+ # foo --> bar --> baz --> qux
254
+ # waldo --^
255
+ #
256
+ # Where foo depends on bar, which depends on baz, which depends on qux. And
257
+ # waldo depends on baz, which depends on qux
258
+ #
259
+ # Due to the puppetfile and dependency graph:
260
+ # - foo will be missing bar and qux
261
+ # - baz will be missing qux
262
+ # - waldo will be missing qux
263
+ #
264
+ # This is to test for transitive dependencies, not just an entire dependency tree
265
+ let(:resolution_specifications) do
266
+ [
267
+ { name: 'foo', origin: :forge, version: '1.0.0' },
268
+ { name: 'bar', origin: :forge, version: '1.0.0' },
269
+ { name: 'baz', origin: :forge, version: '1.0.0' },
270
+ { name: 'qux', origin: :forge, version: '1.0.0' },
271
+ { name: 'waldo', origin: :forge, version: '1.0.0' },
272
+ ]
273
+ end
274
+ let(:resolution_dependencies) do
275
+ [
276
+ { origin: 'foo', destination: 'bar', version_requirement: '>= 0' },
277
+ { origin: 'bar', destination: 'baz', version_requirement: '>= 0' },
278
+ { origin: 'baz', destination: 'qux', version_requirement: '>= 0' },
279
+ { origin: 'waldo', destination: 'baz', version_requirement: '>= 0' },
280
+ ]
281
+ end
282
+
283
+ it 'should return transient DocumentMissingModuleError' do
284
+ errors = subject.resolution_validation_errors(resolution_result)
285
+ expect(errors.count).to eq(3)
286
+
287
+ error = errors[0]
288
+ expect(error).to be_a(PuppetfileResolver::Puppetfile::DocumentMissingDependenciesError)
289
+ expect(error).to have_attributes(
290
+ message: /foo/,
291
+ module_specification: @module_specifications['foo'],
292
+ puppet_module: @document_modules[0],
293
+ missing_specifications: [@module_specifications['bar'], @module_specifications['qux']]
294
+ )
295
+
296
+ error = errors[1]
297
+ expect(error).to be_a(PuppetfileResolver::Puppetfile::DocumentMissingDependenciesError)
298
+ expect(error).to have_attributes(
299
+ message: /baz/,
300
+ module_specification: @module_specifications['baz'],
301
+ puppet_module: @document_modules[1],
302
+ missing_specifications: [@module_specifications['qux']]
303
+ )
304
+
305
+ error = errors[2]
306
+ expect(error).to be_a(PuppetfileResolver::Puppetfile::DocumentMissingDependenciesError)
307
+ expect(error).to have_attributes(
308
+ message: /waldo/,
309
+ module_specification: @module_specifications['waldo'],
310
+ puppet_module: @document_modules[2],
311
+ missing_specifications: [@module_specifications['qux']]
312
+ )
313
+ end
314
+ end
315
+ end
316
+ end