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