librarianp 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +10 -0
  5. data/CHANGELOG.md +255 -0
  6. data/Gemfile +8 -0
  7. data/Gemfile.lock +235 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +55 -0
  10. data/Rakefile +28 -0
  11. data/VERSION +1 -0
  12. data/lib/librarian/action/base.rb +24 -0
  13. data/lib/librarian/action/clean.rb +44 -0
  14. data/lib/librarian/action/ensure.rb +24 -0
  15. data/lib/librarian/action/install.rb +95 -0
  16. data/lib/librarian/action/persist_resolution_mixin.rb +51 -0
  17. data/lib/librarian/action/resolve.rb +46 -0
  18. data/lib/librarian/action/update.rb +44 -0
  19. data/lib/librarian/action.rb +5 -0
  20. data/lib/librarian/algorithms.rb +133 -0
  21. data/lib/librarian/cli/manifest_presenter.rb +89 -0
  22. data/lib/librarian/cli.rb +225 -0
  23. data/lib/librarian/config/database.rb +205 -0
  24. data/lib/librarian/config/file_source.rb +47 -0
  25. data/lib/librarian/config/hash_source.rb +33 -0
  26. data/lib/librarian/config/source.rb +149 -0
  27. data/lib/librarian/config.rb +7 -0
  28. data/lib/librarian/dependency.rb +153 -0
  29. data/lib/librarian/dsl/receiver.rb +42 -0
  30. data/lib/librarian/dsl/target.rb +171 -0
  31. data/lib/librarian/dsl.rb +102 -0
  32. data/lib/librarian/environment/runtime_cache.rb +101 -0
  33. data/lib/librarian/environment.rb +230 -0
  34. data/lib/librarian/error.rb +4 -0
  35. data/lib/librarian/helpers.rb +29 -0
  36. data/lib/librarian/linter/source_linter.rb +55 -0
  37. data/lib/librarian/lockfile/compiler.rb +66 -0
  38. data/lib/librarian/lockfile/parser.rb +123 -0
  39. data/lib/librarian/lockfile.rb +29 -0
  40. data/lib/librarian/logger.rb +46 -0
  41. data/lib/librarian/manifest.rb +146 -0
  42. data/lib/librarian/manifest_set.rb +150 -0
  43. data/lib/librarian/mock/cli.rb +19 -0
  44. data/lib/librarian/mock/dsl.rb +15 -0
  45. data/lib/librarian/mock/environment.rb +21 -0
  46. data/lib/librarian/mock/extension.rb +9 -0
  47. data/lib/librarian/mock/source/mock/registry.rb +83 -0
  48. data/lib/librarian/mock/source/mock.rb +80 -0
  49. data/lib/librarian/mock/source.rb +1 -0
  50. data/lib/librarian/mock/version.rb +5 -0
  51. data/lib/librarian/mock.rb +1 -0
  52. data/lib/librarian/posix.rb +129 -0
  53. data/lib/librarian/resolution.rb +46 -0
  54. data/lib/librarian/resolver/implementation.rb +238 -0
  55. data/lib/librarian/resolver.rb +94 -0
  56. data/lib/librarian/rspec/support/cli_macro.rb +120 -0
  57. data/lib/librarian/source/basic_api.rb +45 -0
  58. data/lib/librarian/source/git/repository.rb +193 -0
  59. data/lib/librarian/source/git.rb +172 -0
  60. data/lib/librarian/source/local.rb +54 -0
  61. data/lib/librarian/source/path.rb +56 -0
  62. data/lib/librarian/source.rb +2 -0
  63. data/lib/librarian/spec.rb +13 -0
  64. data/lib/librarian/spec_change_set.rb +173 -0
  65. data/lib/librarian/specfile.rb +19 -0
  66. data/lib/librarian/support/abstract_method.rb +21 -0
  67. data/lib/librarian/ui.rb +64 -0
  68. data/lib/librarian/version.rb +3 -0
  69. data/lib/librarian.rb +11 -0
  70. data/librarian.gemspec +47 -0
  71. data/spec/functional/cli_spec.rb +27 -0
  72. data/spec/functional/posix_spec.rb +32 -0
  73. data/spec/functional/source/git/repository_spec.rb +199 -0
  74. data/spec/functional/source/git_spec.rb +174 -0
  75. data/spec/support/fakefs.rb +37 -0
  76. data/spec/support/method_patch_macro.rb +30 -0
  77. data/spec/support/project_path_macro.rb +14 -0
  78. data/spec/support/with_env_macro.rb +22 -0
  79. data/spec/unit/action/base_spec.rb +18 -0
  80. data/spec/unit/action/clean_spec.rb +102 -0
  81. data/spec/unit/action/ensure_spec.rb +37 -0
  82. data/spec/unit/action/install_spec.rb +111 -0
  83. data/spec/unit/algorithms_spec.rb +131 -0
  84. data/spec/unit/config/database_spec.rb +320 -0
  85. data/spec/unit/dependency/requirement_spec.rb +12 -0
  86. data/spec/unit/dependency_spec.rb +212 -0
  87. data/spec/unit/dsl_spec.rb +173 -0
  88. data/spec/unit/environment/runtime_cache_spec.rb +73 -0
  89. data/spec/unit/environment_spec.rb +209 -0
  90. data/spec/unit/lockfile/parser_spec.rb +162 -0
  91. data/spec/unit/lockfile_spec.rb +65 -0
  92. data/spec/unit/manifest/version_spec.rb +11 -0
  93. data/spec/unit/manifest_set_spec.rb +202 -0
  94. data/spec/unit/manifest_spec.rb +36 -0
  95. data/spec/unit/mock/environment_spec.rb +25 -0
  96. data/spec/unit/mock/source/mock_spec.rb +22 -0
  97. data/spec/unit/resolver_spec.rb +299 -0
  98. data/spec/unit/source/git_spec.rb +29 -0
  99. data/spec/unit/spec_change_set_spec.rb +169 -0
  100. metadata +257 -0
@@ -0,0 +1,46 @@
1
+ module Librarian
2
+ #
3
+ # Represents the output of the resolution process. Captures the declared
4
+ # dependencies plus the full set of resolved manifests. The sources are
5
+ # already known by the dependencies and by the resolved manifests, so they do
6
+ # not need to be captured explicitly.
7
+ #
8
+ # This representation may be produced by the resolver, may be serialized into
9
+ # a lockfile, and may be deserialized from a lockfile. It is expected that the
10
+ # lockfile is a direct representation in text of this representation, so that
11
+ # the serialization-deserialization process is just the identity function.
12
+ #
13
+ class Resolution
14
+ attr_accessor :dependencies, :manifests, :manifests_index
15
+ private :dependencies=, :manifests=, :manifests_index=
16
+
17
+ def initialize(dependencies, manifests)
18
+ self.dependencies = dependencies
19
+ self.manifests = manifests
20
+ self.manifests_index = build_manifests_index(manifests)
21
+ end
22
+
23
+ def correct?
24
+ manifests && manifests_consistent_with_dependencies? && manifests_internally_consistent?
25
+ end
26
+
27
+ def sources
28
+ manifests.map(&:source).uniq
29
+ end
30
+
31
+ private
32
+
33
+ def build_manifests_index(manifests)
34
+ Hash[manifests.map{|m| [m.name, m]}] if manifests
35
+ end
36
+
37
+ def manifests_consistent_with_dependencies?
38
+ ManifestSet.new(manifests).in_compliance_with?(dependencies)
39
+ end
40
+
41
+ def manifests_internally_consistent?
42
+ ManifestSet.new(manifests).consistent?
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,238 @@
1
+ require 'set'
2
+
3
+ require 'librarian/algorithms'
4
+ require 'librarian/dependency'
5
+
6
+ module Librarian
7
+ class Resolver
8
+ class Implementation
9
+
10
+ class MultiSource
11
+ attr_accessor :sources
12
+ def initialize(sources)
13
+ self.sources = sources
14
+ end
15
+ def manifests(name)
16
+ sources.reverse.map{|source| source.manifests(name)}.flatten(1).compact
17
+ end
18
+ def to_s
19
+ "(no source specified)"
20
+ end
21
+ end
22
+
23
+ class State
24
+ attr_accessor :manifests, :dependencies, :queue
25
+ private :manifests=, :dependencies=, :queue=
26
+ def initialize(manifests, dependencies, queue)
27
+ self.manifests = manifests
28
+ self.dependencies = dependencies # resolved
29
+ self.queue = queue # scheduled
30
+ end
31
+ end
32
+
33
+ attr_accessor :resolver, :spec, :cyclic
34
+ private :resolver=, :spec=, :cyclic=
35
+
36
+ def initialize(resolver, spec, options = { })
37
+ unrecognized_options = options.keys - [:cyclic]
38
+ unrecognized_options.empty? or raise Error,
39
+ "unrecognized options: #{unrecognized_options.join(", ")}"
40
+ self.resolver = resolver
41
+ self.spec = spec
42
+ self.cyclic = !!options[:cyclic]
43
+ @level = 0
44
+ end
45
+
46
+ def resolve(manifests)
47
+ manifests = index_by(manifests, &:name) if manifests.kind_of?(Array)
48
+ queue = spec.dependencies + sourced_dependencies_for_manifests(manifests)
49
+ state = State.new(manifests.dup, [], queue)
50
+ recursive_resolve(state)
51
+ end
52
+
53
+ private
54
+
55
+ def recursive_resolve(state)
56
+ shift_resolved_enqueued_dependencies(state) or return
57
+ state.queue.empty? and return state.manifests
58
+
59
+ state.dependencies << state.queue.shift
60
+ dependency = state.dependencies.last
61
+
62
+ resolving_dependency_map_find_manifests(dependency) do |manifest|
63
+ check_manifest(state, manifest) or next
64
+ check_manifest_for_cycles(state, manifest) or next unless cyclic
65
+
66
+ m = state.manifests.merge(dependency.name => manifest)
67
+ a = sourced_dependencies_for_manifest(manifest)
68
+ s = State.new(m, state.dependencies.dup, state.queue + a)
69
+
70
+ recursive_resolve(s)
71
+ end
72
+ end
73
+
74
+ def find_inconsistency(state, dependency)
75
+ m = state.manifests[dependency.name]
76
+ dependency.satisfied_by?(m) or return m if m
77
+ violation = lambda{|d| !dependency.consistent_with?(d)}
78
+ state.dependencies.find(&violation) || state.queue.find(&violation)
79
+ end
80
+
81
+ # When using this method, you are required to check the return value.
82
+ # Returns +true+ if the resolved enqueued dependencies at the front of the
83
+ # queue could all be moved to the resolved dependencies list.
84
+ # Returns +false+ if there was an inconsistency when trying to move one or
85
+ # more of them.
86
+ # This modifies +queue+ and +dependencies+.
87
+ def shift_resolved_enqueued_dependencies(state)
88
+ while (d = state.queue.first) && state.manifests[d.name]
89
+ if q = find_inconsistency(state, d)
90
+ debug_conflict d, q
91
+ return false
92
+ end
93
+ state.dependencies << state.queue.shift
94
+ end
95
+ true
96
+ end
97
+
98
+ # When using this method, you are required to check the return value.
99
+ # Returns +true+ if the manifest satisfies all of the dependencies.
100
+ # Returns +false+ if there was a dependency that the manifest does not
101
+ # satisfy.
102
+ def check_manifest(state, manifest)
103
+ violation = lambda{|d| d.name == manifest.name && !d.satisfied_by?(manifest)}
104
+ if q = state.dependencies.find(&violation) || state.queue.find(&violation)
105
+ debug_conflict manifest, q
106
+ return false
107
+ end
108
+ true
109
+ end
110
+
111
+ # When using this method, you are required to check the return value.
112
+ # Returns +true+ if the manifest does not introduce a cycle.
113
+ # Returns +false+ if the manifest introduces a cycle.
114
+ def check_manifest_for_cycles(state, manifest)
115
+ manifests = state.manifests.merge(manifest.name => manifest)
116
+ known = manifests.keys
117
+ graph = Hash[manifests.map{|n, m| [n, m.dependencies.map(&:name) & known]}]
118
+ if Algorithms::AdjacencyListDirectedGraph.cyclic?(graph)
119
+ debug_cycle manifest
120
+ return false
121
+ end
122
+ true
123
+ end
124
+
125
+ def default_source
126
+ @default_source ||= MultiSource.new(spec.sources)
127
+ end
128
+
129
+ def dependency_source_map
130
+ @dependency_source_map ||=
131
+ Hash[spec.dependencies.map{|d| [d.name, d.source]}]
132
+ end
133
+
134
+ def sourced_dependency_for(dependency)
135
+ return dependency if dependency.source
136
+
137
+ source = dependency_source_map[dependency.name] || default_source
138
+ Dependency.new(dependency.name, dependency.requirement, source)
139
+ end
140
+
141
+ def sourced_dependencies_for_manifest(manifest)
142
+ manifest.dependencies.map{|d| sourced_dependency_for(d)}
143
+ end
144
+
145
+ def sourced_dependencies_for_manifests(manifests)
146
+ manifests = manifests.values if manifests.kind_of?(Hash)
147
+ manifests.map{|m| sourced_dependencies_for_manifest(m)}.flatten(1)
148
+ end
149
+
150
+ def resolving_dependency_map_find_manifests(dependency)
151
+ scope_resolving_dependency dependency do
152
+ map_find(dependency.manifests) do |manifest|
153
+ scope_checking_manifest dependency, manifest do
154
+ yield manifest
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ def scope_resolving_dependency(dependency)
161
+ debug { "Resolving #{dependency}" }
162
+ resolution = nil
163
+ scope do
164
+ scope_checking_manifests do
165
+ resolution = yield
166
+ end
167
+ if resolution
168
+ debug { "Resolved #{dependency}" }
169
+ else
170
+ debug { "Failed to resolve #{dependency}" }
171
+ end
172
+ end
173
+ resolution
174
+ end
175
+
176
+ def scope_checking_manifests
177
+ debug { "Checking manifests" }
178
+ scope do
179
+ yield
180
+ end
181
+ end
182
+
183
+ def scope_checking_manifest(dependency, manifest)
184
+ debug { "Checking #{manifest}" }
185
+ resolution = nil
186
+ scope do
187
+ resolution = yield
188
+ if resolution
189
+ debug { "Resolved #{dependency} at #{manifest}" }
190
+ else
191
+ debug { "Backtracking from #{manifest}" }
192
+ end
193
+ end
194
+ resolution
195
+ end
196
+
197
+ def debug_schedule(dependency)
198
+ debug { "Scheduling #{dependency}" }
199
+ end
200
+
201
+ def debug_conflict(dependency, conflict)
202
+ debug { "Conflict between #{dependency} and #{conflict}" }
203
+ end
204
+
205
+ def debug_cycle(manifest)
206
+ debug { "Cycle with #{manifest}" }
207
+ end
208
+
209
+ def map_find(enum)
210
+ enum.each do |obj|
211
+ res = yield(obj)
212
+ res.nil? or return res
213
+ end
214
+ nil
215
+ end
216
+
217
+ def index_by(enum)
218
+ Hash[enum.map{|obj| [yield(obj), obj]}]
219
+ end
220
+
221
+ def scope
222
+ @level += 1
223
+ yield
224
+ ensure
225
+ @level -= 1
226
+ end
227
+
228
+ def debug
229
+ environment.logger.debug { ' ' * @level + yield }
230
+ end
231
+
232
+ def environment
233
+ resolver.environment
234
+ end
235
+
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,94 @@
1
+ require 'librarian/error'
2
+ require 'librarian/resolver/implementation'
3
+ require 'librarian/manifest_set'
4
+ require 'librarian/resolution'
5
+
6
+ module Librarian
7
+ class Resolver
8
+
9
+ attr_accessor :environment, :cyclic
10
+ private :environment=, :cyclic=
11
+
12
+ # Options:
13
+ # cyclic: truthy if the resolver should permit cyclic resolutions
14
+ def initialize(environment, options = { })
15
+ unrecognized_options = options.keys - [:cyclic]
16
+ unrecognized_options.empty? or raise Error,
17
+ "unrecognized options: #{unrecognized_options.join(", ")}"
18
+ self.environment = environment
19
+ self.cyclic = !!options[:cyclic]
20
+ end
21
+
22
+ def resolve(spec, partial_manifests = [])
23
+ manifests = implementation(spec).resolve(partial_manifests)
24
+ manifests or return
25
+ enforce_consistency!(spec.dependencies, manifests)
26
+ enforce_acyclicity!(manifests) unless cyclic
27
+ manifests = sort(manifests)
28
+ Resolution.new(spec.dependencies, manifests)
29
+ end
30
+
31
+ private
32
+
33
+ def implementation(spec)
34
+ Implementation.new(self, spec, :cyclic => cyclic)
35
+ end
36
+
37
+ def enforce_consistency!(dependencies, manifests)
38
+ manifest_set = ManifestSet.new(manifests)
39
+ return if manifest_set.in_compliance_with?(dependencies)
40
+ return if manifest_set.consistent?
41
+
42
+ debug { "Resolver Malfunctioned!" }
43
+ errors = []
44
+ dependencies.sort_by(&:name).each do |d|
45
+ m = manifests[d.name]
46
+ if !m
47
+ errors << ["Depends on #{d}", "Missing!"]
48
+ elsif !d.satisfied_by?(m)
49
+ errors << ["Depends on #{d}", "Found: #{m}"]
50
+ end
51
+ end
52
+ unless errors.empty?
53
+ errors.each do |a, b|
54
+ debug { " #{a}" }
55
+ debug { " #{b}" }
56
+ end
57
+ end
58
+ manifests.values.sort_by(&:name).each do |manifest|
59
+ errors = []
60
+ manifest.dependencies.sort_by(&:name).each do |d|
61
+ m = manifests[d.name]
62
+ if !m
63
+ errors << ["Depends on: #{d}", "Missing!"]
64
+ elsif !d.satisfied_by?(m)
65
+ errors << ["Depends on: #{d}", "Found: #{m}"]
66
+ end
67
+ end
68
+ unless errors.empty?
69
+ debug { " #{manifest}" }
70
+ errors.each do |a, b|
71
+ debug { " #{a}" }
72
+ debug { " #{b}" }
73
+ end
74
+ end
75
+ end
76
+ raise Error, "Resolver Malfunctioned!"
77
+ end
78
+
79
+ def enforce_acyclicity!(manifests)
80
+ ManifestSet.cyclic?(manifests) or return
81
+ debug { "Resolver Malfunctioned!" }
82
+ raise Error, "Resolver Malfunctioned!"
83
+ end
84
+
85
+ def sort(manifests)
86
+ ManifestSet.sort(manifests)
87
+ end
88
+
89
+ def debug(*args, &block)
90
+ environment.logger.debug(*args, &block)
91
+ end
92
+
93
+ end
94
+ end
@@ -0,0 +1,120 @@
1
+ require "json"
2
+ require "pathname"
3
+ require "securerandom"
4
+ require "stringio"
5
+ require "thor"
6
+
7
+ require "librarian/helpers"
8
+
9
+ module Librarian
10
+ module RSpec
11
+ module Support
12
+ module CliMacro
13
+
14
+ class FakeShell < Thor::Shell::Basic
15
+ def stdout
16
+ @stdout ||= StringIO.new
17
+ end
18
+ def stderr
19
+ @stderr ||= StringIO.new
20
+ end
21
+ def stdin
22
+ raise "unsupported"
23
+ end
24
+ end
25
+
26
+ class FileMatcher
27
+ attr_accessor :rel_path, :content, :type, :base_path
28
+ def initialize(rel_path, content, options = { })
29
+ self.rel_path = rel_path
30
+ self.content = content
31
+ self.type = options[:type]
32
+ end
33
+ def full_path
34
+ @full_path ||= base_path + rel_path
35
+ end
36
+ def actual_content
37
+ @actual_content ||= begin
38
+ s = full_path.read
39
+ s = JSON.parse(s) if type == :json
40
+ s
41
+ end
42
+ end
43
+ def matches?(base_path)
44
+ base_path = Pathname(base_path) unless Pathname === base_path
45
+ self.base_path = base_path
46
+
47
+ full_path.file? && (!content || actual_content == content)
48
+ end
49
+ end
50
+
51
+ def self.included(base)
52
+ base.instance_exec do
53
+ let(:project_path) do
54
+ project_path = Pathname.new(__FILE__).expand_path
55
+ project_path = project_path.dirname until project_path.join("Rakefile").exist?
56
+ project_path
57
+ end
58
+ let(:tmp) { project_path.join("tmp/spec/cli") }
59
+ let(:pwd) { tmp + SecureRandom.hex(8) }
60
+
61
+ before { tmp.mkpath }
62
+ before { pwd.mkpath }
63
+
64
+ after { tmp.rmtree }
65
+ end
66
+ end
67
+
68
+ def cli!(*args)
69
+ @shell = FakeShell.new
70
+ @exit_status = Dir.chdir(pwd) do
71
+ described_class.with_environment do
72
+ described_class.returning_status do
73
+ described_class.start args, :shell => @shell
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ def write_file!(path, content)
80
+ path = pwd.join(path)
81
+ path.dirname.mkpath
82
+ path.open("wb"){|f| f.write(content)}
83
+ end
84
+
85
+ def write_json_file!(path, content)
86
+ write_file! path, JSON.dump(content)
87
+ end
88
+
89
+ def strip_heredoc(text)
90
+ Librarian::Helpers.strip_heredoc(text)
91
+ end
92
+
93
+ def shell
94
+ @shell
95
+ end
96
+
97
+ def stdout
98
+ shell.stdout.string
99
+ end
100
+
101
+ def stderr
102
+ shell.stderr.string
103
+ end
104
+
105
+ def exit_status
106
+ @exit_status
107
+ end
108
+
109
+ def have_file(rel_path, content = nil)
110
+ FileMatcher.new(rel_path, content)
111
+ end
112
+
113
+ def have_json_file(rel_path, content)
114
+ FileMatcher.new(rel_path, content, :type => :json)
115
+ end
116
+
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,45 @@
1
+ module Librarian
2
+ module Source
3
+ module BasicApi
4
+
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ class << base
8
+ def lock_name(name)
9
+ def_sclass_prop(:lock_name, name)
10
+ end
11
+
12
+ def spec_options(keys)
13
+ def_sclass_prop(:spec_options, keys)
14
+ end
15
+
16
+ private
17
+
18
+ def def_sclass_prop(name, arg)
19
+ sclass = class << self ; self ; end
20
+ sclass.module_exec do
21
+ remove_method(name)
22
+ define_method(name) { arg }
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ module ClassMethods
29
+ def from_lock_options(environment, options)
30
+ new(environment, options[:remote], options.reject{|k, v| k == :remote})
31
+ end
32
+
33
+ def from_spec_args(environment, param, options)
34
+ recognized_options = spec_options
35
+ unrecognized_options = options.keys - recognized_options
36
+ unrecognized_options.empty? or raise Error,
37
+ "unrecognized options: #{unrecognized_options.join(", ")}"
38
+
39
+ new(environment, param, options)
40
+ end
41
+ end
42
+
43
+ end
44
+ end
45
+ end