fixman 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.
- checksums.yaml +7 -0
- data/History.txt +6 -0
- data/Manifest.txt +23 -0
- data/README.md +176 -0
- data/Rakefile +19 -0
- data/bin/fixman +50 -0
- data/lib/fixman.rb +13 -0
- data/lib/fixman/command_line.rb +184 -0
- data/lib/fixman/configuration.rb +161 -0
- data/lib/fixman/controller.rb +121 -0
- data/lib/fixman/raw_task.rb +103 -0
- data/lib/fixman/repository.rb +99 -0
- data/lib/fixman/task.rb +19 -0
- data/lib/fixman/task_parse_error.rb +12 -0
- data/lib/fixman/tester.rb +69 -0
- data/lib/fixman/utilities.rb +8 -0
- data/test/test_command_line.rb +250 -0
- data/test/test_configuration.rb +58 -0
- data/test/test_controller.rb +16 -0
- data/test/test_fixman.rb +7 -0
- data/test/test_raw_task.rb +97 -0
- data/test/test_repository.rb +21 -0
- data/test/test_task.rb +28 -0
- data/test/test_tester.rb +89 -0
- metadata +163 -0
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'English'
|
2
|
+
require 'yaml'
|
3
|
+
require 'pathname'
|
4
|
+
require 'fixman/utilities'
|
5
|
+
require 'fixman/task_parse_error'
|
6
|
+
require 'classy_hash'
|
7
|
+
|
8
|
+
module Fixman
|
9
|
+
class Configuration
|
10
|
+
DEFAULT_LEDGER_FILE = '.fixman_ledger.yaml'
|
11
|
+
DEFAULT_CONF_FILE = '.fixman_conf.yaml'
|
12
|
+
|
13
|
+
CONDITION_OR_CLEANUP_SCHEMA = ->(h) {
|
14
|
+
return ':type is mising' unless h.has_key? :type
|
15
|
+
unless [:ruby, :shell].include? h[:type]
|
16
|
+
return ':type must be one of :ruby or :shell'
|
17
|
+
end
|
18
|
+
return ':action is mising' unless h.has_key? :action
|
19
|
+
return ':action should be a String' unless h[:action].is_a? String
|
20
|
+
|
21
|
+
if h[:type] == :ruby
|
22
|
+
begin
|
23
|
+
# Here a misformed Ruby source would also throw an ArgumentError
|
24
|
+
raise ArgumentError unless eval(h[:action]).is_a? Proc
|
25
|
+
rescue ArgumentError
|
26
|
+
return ':action should evaluate to a Proc object'
|
27
|
+
end
|
28
|
+
elsif h[:type] == :shell && h[:exit_status]
|
29
|
+
es = h[:exit_status]
|
30
|
+
unless es.is_a?(Integer) && (0..255).include?(es)
|
31
|
+
return ':exit_status should be an integer in 0..255 range'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
true
|
36
|
+
}
|
37
|
+
|
38
|
+
REPO_INFO_SCHEMA = {
|
39
|
+
symbol: Symbol,
|
40
|
+
prompt: String,
|
41
|
+
label: String,
|
42
|
+
type: Symbol,
|
43
|
+
choices: [ :optional, [[String]] ]
|
44
|
+
}
|
45
|
+
|
46
|
+
TASK_SCHEMA = {
|
47
|
+
name: String,
|
48
|
+
target_placeholder: [ :optional, String ],
|
49
|
+
command: {
|
50
|
+
action: String,
|
51
|
+
exit_status: [ :optional, 0..255 ]
|
52
|
+
},
|
53
|
+
condition: [ :optional, CONDITION_OR_CLEANUP_SCHEMA ],
|
54
|
+
cleanup: [ :optional, CONDITION_OR_CLEANUP_SCHEMA ],
|
55
|
+
groups: [ :optional, [[ String ]] ]
|
56
|
+
}
|
57
|
+
|
58
|
+
CONF_SCHEMA = {
|
59
|
+
fixtures_base: String,
|
60
|
+
fixtures_ledger: [ :optional, String ],
|
61
|
+
tasks: ->(tasks) {
|
62
|
+
if tasks.is_a?(Array) && tasks.size > 0
|
63
|
+
begin
|
64
|
+
tasks.all? do |task|
|
65
|
+
begin
|
66
|
+
CH.validate task, TASK_SCHEMA
|
67
|
+
rescue => e
|
68
|
+
index = tasks.find_index task
|
69
|
+
raise Fixman::TaskParseError.new(e, index)
|
70
|
+
end
|
71
|
+
true
|
72
|
+
end
|
73
|
+
rescue Fixman::TaskParseError => e
|
74
|
+
e.message
|
75
|
+
end
|
76
|
+
else
|
77
|
+
"a non-empty array"
|
78
|
+
end
|
79
|
+
},
|
80
|
+
groups: [ :optional, [[ String ]] ],
|
81
|
+
extra_repo_info: [ :optional, [[ REPO_INFO_SCHEMA ]] ]
|
82
|
+
}
|
83
|
+
|
84
|
+
include Fixman::Utilities
|
85
|
+
|
86
|
+
attr_reader :fixtures_base, :fixture_ledger, :raw_tasks, :extra_repo_info, :groups
|
87
|
+
|
88
|
+
def initialize(fixtures_base,
|
89
|
+
fixture_ledger,
|
90
|
+
raw_tasks,
|
91
|
+
groups,
|
92
|
+
extra_repo_info)
|
93
|
+
@fixtures_base = Pathname.new(fixtures_base)
|
94
|
+
@fixture_ledger = Pathname.new(fixture_ledger)
|
95
|
+
@raw_tasks = raw_tasks
|
96
|
+
@extra_repo_info = extra_repo_info
|
97
|
+
@groups = groups
|
98
|
+
end
|
99
|
+
|
100
|
+
class << self
|
101
|
+
def read(path_to_conf)
|
102
|
+
conf_yaml = YAML.load IO.read(path_to_conf)
|
103
|
+
|
104
|
+
ClassyHash.validate conf_yaml, CONF_SCHEMA
|
105
|
+
initialize_defaults conf_yaml
|
106
|
+
|
107
|
+
raw_tasks = conf_yaml[:tasks].map do |task|
|
108
|
+
RawTask.new task
|
109
|
+
end
|
110
|
+
|
111
|
+
Configuration.new(conf_yaml[:fixtures_base],
|
112
|
+
conf_yaml[:fixture_ledger],
|
113
|
+
raw_tasks,
|
114
|
+
conf_yaml[:groups],
|
115
|
+
conf_yaml[:extra_repo_info])
|
116
|
+
end
|
117
|
+
|
118
|
+
def initialize_defaults(conf_hash)
|
119
|
+
conf_hash[:tasks].each do |task|
|
120
|
+
command = task[:command]
|
121
|
+
command[:exit_status] = 0 unless command[:exit_status]
|
122
|
+
unless task[:target_placeholder]
|
123
|
+
task[:target_placeholder] = 'TARGET'
|
124
|
+
end
|
125
|
+
|
126
|
+
condition = task[:condition]
|
127
|
+
if !condition
|
128
|
+
task[:condition] = {
|
129
|
+
type: :ruby,
|
130
|
+
action: 'proc { true }'
|
131
|
+
}
|
132
|
+
elsif condition[:type] == :shell && !condition[:exit_status]
|
133
|
+
condition[:exit_status] = 0
|
134
|
+
end
|
135
|
+
|
136
|
+
cleanup = task[:cleanup]
|
137
|
+
if !cleanup
|
138
|
+
task[:cleanup] = {
|
139
|
+
type: :ruby,
|
140
|
+
action: 'proc { true }'
|
141
|
+
}
|
142
|
+
end
|
143
|
+
|
144
|
+
task[:variables] = [] unless task[:variables]
|
145
|
+
end
|
146
|
+
|
147
|
+
unless conf_hash[:fixture_ledger]
|
148
|
+
conf_hash[:fixture_ledger] = DEFAULT_LEDGER_FILE
|
149
|
+
end
|
150
|
+
|
151
|
+
[:groups, :extra_repo_info].each do |key|
|
152
|
+
conf_hash[key] = [] unless conf_hash[key]
|
153
|
+
end
|
154
|
+
|
155
|
+
conf_hash[:extra_repo_info].each do |repo_info|
|
156
|
+
repo_info[:prompt] << ' '
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'fixman/repository'
|
2
|
+
require 'git'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Fixman
|
6
|
+
class Controller
|
7
|
+
attr_reader :repos
|
8
|
+
|
9
|
+
def initialize(repos = [])
|
10
|
+
@repos = repos
|
11
|
+
end
|
12
|
+
|
13
|
+
# Add a new repository to the collection.
|
14
|
+
def add(input, fixtures_base, extra_repo_info)
|
15
|
+
repo = Repository.new input, fixtures_base, extra_repo_info
|
16
|
+
|
17
|
+
@repos << repo
|
18
|
+
end
|
19
|
+
|
20
|
+
# Remove repository from the fixtures list.
|
21
|
+
def delete(canonical_name)
|
22
|
+
repo = find canonical_name
|
23
|
+
|
24
|
+
if repo
|
25
|
+
@repos.delete repo
|
26
|
+
repo.destroy
|
27
|
+
else
|
28
|
+
fail ArgumentError
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Download repositories belonging to the at least one of the given groups.
|
33
|
+
def fetch(groups)
|
34
|
+
repos = find_by_groups(groups).reject { |repo| repo.fetched? }
|
35
|
+
repos.each do |repo|
|
36
|
+
begin
|
37
|
+
repo.fetch
|
38
|
+
rescue Git::GitExecuteError => error
|
39
|
+
STDERR.puts "Warning: Repository #{repo.canonical_name} could not be fetched."
|
40
|
+
STDERR.puts error.message
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def update(canonical_name, sha = nil)
|
46
|
+
repo = find canonical_name
|
47
|
+
fail ArgumentError unless repo
|
48
|
+
|
49
|
+
repo.sha = sha || repo.retrieve_head_sha
|
50
|
+
end
|
51
|
+
|
52
|
+
def upgrade(groups)
|
53
|
+
repos = find_by_groups(groups)
|
54
|
+
repos.each do |repo|
|
55
|
+
begin
|
56
|
+
repo.upgrade
|
57
|
+
rescue Git::GitExecuteError => error
|
58
|
+
STDERR.puts "Warning: Repisotiry #{repo.canonical_name} could not be \"
|
59
|
+
upgraded."
|
60
|
+
STDERR.puts error.message
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class << self
|
66
|
+
# Controller for the command line interface.
|
67
|
+
def open(conf)
|
68
|
+
original_ledger = ""
|
69
|
+
|
70
|
+
controller =
|
71
|
+
begin
|
72
|
+
original_ledger = YAML.load(IO.read conf.fixture_ledger)
|
73
|
+
original_ledger.map! { |entry| YAML.load entry }
|
74
|
+
|
75
|
+
repos = []
|
76
|
+
original_ledger.each do |repo_params|
|
77
|
+
repos << Repository.new(repo_params,
|
78
|
+
conf.fixtures_base,
|
79
|
+
conf.extra_repo_info)
|
80
|
+
end
|
81
|
+
|
82
|
+
Controller.new repos
|
83
|
+
rescue Errno::ENOENT
|
84
|
+
Controller.new
|
85
|
+
end
|
86
|
+
|
87
|
+
result = yield controller
|
88
|
+
|
89
|
+
write_ledger controller.repos, original_ledger, conf.fixture_ledger
|
90
|
+
|
91
|
+
result
|
92
|
+
rescue Git::GitExecuteError => error
|
93
|
+
puts error.message
|
94
|
+
exit 1
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def write_ledger(repos, original_ledger, fixture_ledger)
|
100
|
+
new_ledger = YAML.dump repos.map(&:to_yaml)
|
101
|
+
if new_ledger != original_ledger
|
102
|
+
IO.write fixture_ledger, new_ledger
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def find_by_groups(groups)
|
110
|
+
if groups.empty?
|
111
|
+
@repos
|
112
|
+
else
|
113
|
+
@repos.select { |repo| !(repo.groups & groups).empty? }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def find(canonical_name)
|
118
|
+
@repos.find { |repo| canonical_name == repo.canonical_name }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'fixman/utilities'
|
3
|
+
|
4
|
+
module Fixman
|
5
|
+
class RawTask
|
6
|
+
include Utilities
|
7
|
+
|
8
|
+
def initialize params
|
9
|
+
# Common to commands, cleanup, condition
|
10
|
+
@name = params[:name]
|
11
|
+
@target_placeholder = params[:target_placeholder]
|
12
|
+
|
13
|
+
# Command to execute and check exit status
|
14
|
+
@command = params[:command]
|
15
|
+
@variables = params[:variables] || []
|
16
|
+
|
17
|
+
# Condition to run the task
|
18
|
+
@condition = params[:condition]
|
19
|
+
|
20
|
+
# Clean up
|
21
|
+
@cleanup = params[:cleanup]
|
22
|
+
end
|
23
|
+
|
24
|
+
def refine
|
25
|
+
condition_proc = refine_condition
|
26
|
+
cleanup_proc = refine_cleanup
|
27
|
+
command_procs = refine_command
|
28
|
+
|
29
|
+
command_procs.map do |command_proc|
|
30
|
+
Task.new @name, condition_proc, command_proc, cleanup_proc
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Defines refine_cleanup and refine_condition methods.
|
37
|
+
# ivar is the associated instance variable i.e. @condition and @cleanup
|
38
|
+
%w(condition cleanup).each do |ivar_name|
|
39
|
+
define_method("refine_#{ivar_name}") do
|
40
|
+
ivar = instance_variable_get("@#{ivar_name}")
|
41
|
+
|
42
|
+
return proc { true } unless ivar
|
43
|
+
|
44
|
+
result =
|
45
|
+
case ivar[:type]
|
46
|
+
when :ruby
|
47
|
+
eval ivar[:action]
|
48
|
+
when :shell
|
49
|
+
shell_to_proc ivar[:action], ivar[:exit_status]
|
50
|
+
else
|
51
|
+
error "Unknown #{ivar_name} type"
|
52
|
+
end
|
53
|
+
|
54
|
+
error 'Invalid action' unless result.class == Proc
|
55
|
+
|
56
|
+
result
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def refine_command
|
61
|
+
actions = substitute_variables
|
62
|
+
|
63
|
+
actions.map do |action|
|
64
|
+
shell_to_proc action, @command[:exit_status]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def substitute_variables
|
69
|
+
actions = [@command[:action]]
|
70
|
+
|
71
|
+
@variables.each do |variable|
|
72
|
+
key_regexp = Regexp.new variable[:key]
|
73
|
+
actions.map! do |action|
|
74
|
+
if key_regexp =~ action
|
75
|
+
variable[:values].map { |value| action.gsub(variable[:key], value) }
|
76
|
+
else
|
77
|
+
action
|
78
|
+
end
|
79
|
+
end.flatten!
|
80
|
+
end
|
81
|
+
|
82
|
+
actions
|
83
|
+
end
|
84
|
+
|
85
|
+
def shell_to_proc(shell_command, exit_status)
|
86
|
+
proc do |target|
|
87
|
+
final_command_str =
|
88
|
+
if target
|
89
|
+
shell_command.gsub(@target_placeholder, target.to_s)
|
90
|
+
else
|
91
|
+
shell_command
|
92
|
+
end
|
93
|
+
|
94
|
+
final_command_str << ' &> /dev/null'
|
95
|
+
|
96
|
+
system(final_command_str)
|
97
|
+
if exit_status
|
98
|
+
$CHILD_STATUS.exitstatus == exit_status
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'English'
|
2
|
+
require 'git'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
module Fixman
|
7
|
+
class Repository
|
8
|
+
FIELD_LABEL_WIDTH = 20
|
9
|
+
|
10
|
+
GIT_URL_REGEXP = %r{
|
11
|
+
(?:github\.com)\/
|
12
|
+
(?<owner>(?:\w|-)+)
|
13
|
+
\/
|
14
|
+
(?<name>(?:\w|-)+)
|
15
|
+
(?:\.git)?
|
16
|
+
$
|
17
|
+
}xi
|
18
|
+
|
19
|
+
attr_accessor :url, :name, :owner, :sha, :groups, :other_fields
|
20
|
+
attr_reader :path
|
21
|
+
|
22
|
+
def initialize(repo_params, fixtures_base, extra_repo_info)
|
23
|
+
@url = repo_params[:url]
|
24
|
+
@name = repo_params[:name]
|
25
|
+
@owner = repo_params[:owner]
|
26
|
+
@sha = repo_params[:sha]
|
27
|
+
@groups = repo_params[:groups]
|
28
|
+
|
29
|
+
[:url, :name, :owner, :sha, :groups].each { |key| repo_params.delete key }
|
30
|
+
@other_fields = repo_params
|
31
|
+
|
32
|
+
@extra_repo_info = extra_repo_info
|
33
|
+
@path = fixtures_base + canonical_name
|
34
|
+
end
|
35
|
+
|
36
|
+
def fetched?
|
37
|
+
File.exist? @path
|
38
|
+
end
|
39
|
+
|
40
|
+
def fetch
|
41
|
+
git = Git.clone @url, @path
|
42
|
+
git.reset_hard @sha
|
43
|
+
end
|
44
|
+
|
45
|
+
def upgrade
|
46
|
+
git = Git.open @path
|
47
|
+
git.pull
|
48
|
+
git.reset_hard @sha
|
49
|
+
end
|
50
|
+
|
51
|
+
def destroy
|
52
|
+
FileUtils.rm_rf @path if File.exist? @path
|
53
|
+
end
|
54
|
+
|
55
|
+
def canonical_name
|
56
|
+
"#{@owner}/#{@name}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def summary
|
60
|
+
"#{canonical_name}\t#{@sha}"
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_s
|
64
|
+
str = StringIO.new
|
65
|
+
str.puts 'Repository'.ljust(FIELD_LABEL_WIDTH) + canonical_name
|
66
|
+
str.puts 'URL'.ljust(FIELD_LABEL_WIDTH) + @url
|
67
|
+
str.puts 'Commit SHA'.ljust(FIELD_LABEL_WIDTH) + @sha
|
68
|
+
str.puts 'Groups'.ljust(FIELD_LABEL_WIDTH) + @groups.join(', ')
|
69
|
+
@extra_repo_info.each do |info|
|
70
|
+
str.puts info[:label].ljust(FIELD_LABEL_WIDTH) +
|
71
|
+
@other_fields[info[:symbol]]
|
72
|
+
end
|
73
|
+
str.string
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_yaml
|
77
|
+
{
|
78
|
+
name: @name,
|
79
|
+
owner: @owner,
|
80
|
+
sha: @sha,
|
81
|
+
url: @url,
|
82
|
+
access_right: @access_right,
|
83
|
+
notes: @notes
|
84
|
+
}.merge(@other_fields).to_yaml
|
85
|
+
end
|
86
|
+
|
87
|
+
class << self
|
88
|
+
def extract_owner_and_name(url)
|
89
|
+
match_data = GIT_URL_REGEXP.match url
|
90
|
+
[match_data[:owner], match_data[:name]] if match_data
|
91
|
+
end
|
92
|
+
|
93
|
+
def retrieve_head_sha url
|
94
|
+
ref = Git.ls_remote url
|
95
|
+
ref['head'][:sha]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|