librarianp 0.1.2

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 (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