puppetfile-resolver 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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