tinyci 0.4 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/tinyci/runner.rb CHANGED
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tinyci/subprocesses'
2
4
  require 'tinyci/git_utils'
5
+ require 'tinyci/path_utils'
3
6
  require 'tinyci/logging'
4
7
  require 'tinyci/config'
5
8
 
@@ -14,178 +17,191 @@ require 'fileutils'
14
17
 
15
18
  module TinyCI
16
19
  # Responsible for managing the running of TinyCI against a single git object.
17
- #
18
- # @attr builder [TinyCI::Executor] Returns the Builder object. Used solely for testing at this time.
19
- # @attr tester [TinyCI::Executor] Returns the Tester object. Used solely for testing at this time.
20
+ #
21
+ # @attr builder [TinyCI::Executor] Returns the Builder object. Used solely for testing.
22
+ # @attr tester [TinyCI::Executor] Returns the Tester object. Used solely for testing.
20
23
  class Runner
21
24
  include Subprocesses
22
25
  include GitUtils
26
+ include PathUtils
23
27
  include Logging
24
-
28
+
29
+ CONFIG_FILENAME = '.tinyci.yml'
30
+
25
31
  attr_accessor :builder, :tester, :hooker
26
-
32
+
27
33
  # Constructor, allows injection of generic configuration params.
28
- #
34
+ #
29
35
  # @param working_dir [String] The working directory to execute against.
30
36
  # @param commit [String] SHA1 of git object to run against
31
37
  # @param logger [Logger] Logger object
32
- # @param time [Time] Override time of object creation. Used solely for testing at this time.
33
- # @param config [Hash] Override TinyCI config object, normally loaded from `.tinyci` file. Used solely for testing at this time.
38
+ # @param time [Time] Override time of object creation. Used solely for testing.
39
+ # @param config [Hash] Override TinyCI config object, normally loaded from `.tinyci` file.
40
+ # Used solely for testing.
34
41
  def initialize(working_dir: '.', commit:, time: nil, logger: nil, config: nil)
35
42
  @working_dir = working_dir
36
- @logger = logger
37
43
  @config = config
38
44
  @commit = commit
39
- @time = time || commit_time
45
+ @time = time
46
+ @logger = logger.is_a?(MultiLogger) ? MultiLogger.new(quiet: logger.quiet) : nil
47
+ ensure_path target_path
48
+ setup_log
40
49
  end
41
-
50
+
42
51
  # Runs the TinyCI system against the single git object referenced in `@commit`.
43
- #
52
+ #
44
53
  # @return [Boolean] `true` if the commit was built and tested successfully, `false` otherwise
54
+ # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity
45
55
  def run!
46
56
  begin
47
- ensure_path target_path
48
- setup_log
49
-
50
57
  log_info "Commit: #{@commit}"
51
-
52
- log_info "Cleaning..."
58
+
59
+ unless config_exists?
60
+ log_error "No config found for #{@commit}"
61
+
62
+ return false
63
+ end
64
+
65
+ log_info 'Cleaning...'
53
66
  clean
54
-
55
- log_info "Exporting..."
67
+
68
+ log_info 'Exporting...'
56
69
  ensure_path export_path
57
70
  export
58
-
71
+
59
72
  begin
60
- load_config
73
+ config
61
74
  rescue ConfigMissingError => e
62
75
  log_error e.message
63
76
  log_error 'Removing export...'
64
77
  clean
65
-
78
+
66
79
  return false
67
80
  end
68
81
  @builder ||= instantiate_builder
69
82
  @tester ||= instantiate_tester
70
83
  @hooker ||= instantiate_hooker
71
-
72
- log_info "Building..."
84
+
85
+ log_info 'Building...'
73
86
  run_hook! :before_build
74
87
  begin
75
88
  @builder.build
76
- rescue => e
89
+ rescue StandardError => e
77
90
  run_hook! :after_build_failure
78
-
91
+
79
92
  raise e if ENV['TINYCI_ENV'] == 'test'
80
-
93
+
81
94
  log_error e
82
95
  log_debug e.backtrace
83
-
96
+
84
97
  return false
85
98
  else
86
99
  run_hook! :after_build_success
87
100
  ensure
88
101
  run_hook! :after_build
89
102
  end
90
-
91
-
92
- log_info "Testing..."
103
+
104
+ log_info 'Testing...'
93
105
  run_hook! :before_test
94
106
  begin
95
107
  @tester.test
96
- rescue => e
108
+ rescue StandardError => e
97
109
  run_hook! :after_test_failure
98
-
110
+
99
111
  raise e if ENV['TINYCI_ENV'] == 'test'
100
-
112
+
101
113
  log_error e
102
114
  log_debug e.backtrace
103
-
115
+
104
116
  return false
105
117
  else
106
118
  run_hook! :after_test_success
107
119
  ensure
108
120
  run_hook! :after_test
109
121
  end
110
-
122
+
111
123
  log_info "Finished #{@commit}"
112
- rescue => e
124
+ rescue StandardError => e
113
125
  raise e if ENV['TINYCI_ENV'] == 'test'
114
-
126
+
115
127
  log_error e
116
128
  log_debug e.backtrace
117
129
  return false
118
130
  ensure
119
131
  run_hook! :after_all
120
132
  end
121
-
133
+
122
134
  true
123
135
  end
124
-
125
- # Build the absolute target path
126
- def target_path
127
- File.absolute_path("#{@working_dir}/builds/#{@time.to_i}_#{@commit}/")
128
- end
129
-
130
- # Build the export path
131
- def export_path
132
- File.join(target_path, 'export')
133
- end
134
-
136
+ # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/CyclomaticComplexity
137
+
135
138
  private
136
-
139
+
137
140
  def run_hook!(name)
138
141
  return unless @hooker
139
-
142
+
140
143
  @hooker.send("#{name}!")
141
144
  end
142
-
145
+
143
146
  # Creates log file if it doesnt exist
144
147
  def setup_log
145
148
  return unless @logger.is_a? MultiLogger
146
- FileUtils.touch logfile_path
147
- @logger.output_path = logfile_path
148
- end
149
-
150
- def logfile_path
151
- File.join(target_path, 'tinyci.log')
149
+
150
+ @logger.add_output_path logfile_path
151
+ @logger.add_output_path repo_logfile_path
152
152
  end
153
-
153
+
154
154
  # Instantiate the Builder object according to the class named in the config
155
155
  def instantiate_builder
156
156
  klass = TinyCI::Builders.const_get(@config[:builder][:class])
157
- klass.new(@config[:builder][:config].merge(target: target_path, export: export_path, commit: @commit), logger: @logger)
157
+ konfig = @config[:builder][:config].merge(
158
+ target: target_path,
159
+ export: export_path,
160
+ commit: @commit,
161
+ logger: @logger
162
+ )
163
+ klass.new(konfig)
158
164
  end
159
-
165
+
160
166
  # Instantiate the Tester object according to the class named in the config
161
167
  def instantiate_tester
162
168
  klass = TinyCI::Testers.const_get(@config[:tester][:class])
163
- klass.new(@config[:tester][:config].merge(target: target_path, export: export_path, commit: @commit), logger: @logger)
169
+ konfig = @config[:tester][:config].merge(
170
+ target: target_path,
171
+ export: export_path,
172
+ commit: @commit,
173
+ logger: @logger
174
+ )
175
+ klass.new(konfig)
164
176
  end
165
-
177
+
166
178
  # Instantiate the Hooker object according to the class named in the config
167
179
  def instantiate_hooker
168
180
  return nil unless @config[:hooker].is_a? Hash
169
-
181
+
170
182
  klass = TinyCI::Hookers.const_get(@config[:hooker][:class])
171
- klass.new(@config[:hooker][:config].merge(target: target_path, export: export_path, commit: @commit), logger: @logger)
183
+ konfig = @config[:hooker][:config].merge(
184
+ target: target_path,
185
+ export: export_path,
186
+ commit: @commit,
187
+ logger: @logger
188
+ )
189
+ klass.new(konfig)
172
190
  end
173
-
174
- # Instantiate the {Config} object from the `.tinyci.yml` file in the exported directory
175
- def load_config
176
- @config ||= Config.new(working_dir: export_path)
191
+
192
+ def config_path
193
+ File.expand_path(CONFIG_FILENAME, export_path)
177
194
  end
178
195
 
179
- # Parse the commit time from git
180
- def commit_time
181
- Time.at execute(git_cmd('show', '-s', '--format=%ct', @commit)).to_i
196
+ def config_exists?
197
+ file_exists_in_git? CONFIG_FILENAME
182
198
  end
183
199
 
184
- # Ensure a path exists
185
- def ensure_path(path)
186
- execute 'mkdir', '-p', path
200
+ # The {Config} object from the `.tinyci.yml` file in the exported directory
201
+ def config
202
+ @config ||= Config.new(config_path)
187
203
  end
188
-
204
+
189
205
  # Delete the export path
190
206
  def clean
191
207
  FileUtils.rm_rf export_path
@@ -196,7 +212,8 @@ module TinyCI
196
212
  # a `git export` subcommand.
197
213
  # see https://stackoverflow.com/a/163769
198
214
  def export
199
- execute_pipe git_cmd('archive', '--format=tar', @commit), ['tar', '-C', export_path, '-xf', '-']
215
+ execute_pipe git_cmd('archive', '--format=tar',
216
+ @commit), ['tar', '-C', export_path, '-xf', '-']
200
217
  end
201
218
  end
202
219
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tinyci/runner'
2
4
  require 'tinyci/subprocesses'
3
5
  require 'tinyci/git_utils'
@@ -7,82 +9,82 @@ module TinyCI
7
9
  # Manages the execution of test jobs. Responsible for deciding which
8
10
  # commits need to be built and tested. Also manages the pidfile. This
9
11
  # is the main entrypoint for TinyCI.
10
- #
12
+ #
11
13
  # @attr_reader [String] working_dir The working directory to execute against
12
14
  class Scheduler
13
15
  include Subprocesses
14
16
  include Logging
15
17
  include GitUtils
16
-
18
+
17
19
  attr_reader :working_dir
18
-
20
+
19
21
  # Constructor, allows injection of configuration and custom {Runner} class.
20
22
  # Config params are passed to {Runner} instances.
21
- #
23
+ #
22
24
  # @param working_dir [String] The working directory to execute against
23
25
  # @param logger [Logger] Logger object
24
26
  # @param commit [String] specific git object to run against
25
27
  # @param runner_class [TinyCI::Runner] Injection of {Runner} dependency
26
28
  def initialize(
27
- working_dir: nil,
28
- logger: nil,
29
- commit: nil,
30
- runner_class: Runner
31
- )
32
-
29
+ working_dir: nil,
30
+ logger: nil,
31
+ commit: nil,
32
+ runner_class: Runner
33
+ )
34
+
33
35
  @working_dir = working_dir || repo_root
34
36
  @logger = logger
35
37
  @runner_class = runner_class
36
38
  @commit = commit
37
39
  end
38
-
40
+
39
41
  # Runs the TinyCI system against the relevant commits. Also sets up the pidfile.
40
- #
42
+ #
41
43
  # @return [Boolean] `true` if all commits built and tested successfully, `false` otherwise
42
44
  def run!
43
45
  pid = PidFile.new(pidfile: 'tinyci.pid', piddir: @working_dir)
44
-
46
+
45
47
  result = if @commit
46
- run_commit get_commit @commit
47
- else
48
- run_all_commits
49
- end
50
-
48
+ run_commit get_commit @commit
49
+ else
50
+ run_all_commits
51
+ end
52
+
51
53
  pid.release
52
-
54
+
53
55
  result
54
56
  end
55
-
57
+
56
58
  private
57
-
59
+
58
60
  # Git objects to be executed against, all those without a tinyci tag
59
- #
61
+ #
60
62
  # @return [Array<String>] the sha1 hashes in reverse order of creation time
61
- def get_commits
63
+ def retrieve_commits
62
64
  log = execute(git_cmd('log', '--notes=tinyci*', '--format=%H %ct %N§§§', '--reverse'))
63
- lines = log.split("§§§")
64
-
65
- lines.map {|l| format_commit_data(l)}.select {|c| c[:result].nil?}
65
+ lines = log.split('§§§')
66
+
67
+ lines.map { |l| format_commit_data(l) }.select { |c| c[:result].nil? }
66
68
  end
67
-
69
+
68
70
  def get_commit(sha)
69
- data = execute(git_cmd('show', '--quiet', '--notes=tinyci*', "--format=%H %ct", sha))
70
-
71
+ data = execute(git_cmd('show', '--quiet', '--notes=tinyci*', '--format=%H %ct', sha))
72
+
71
73
  format_commit_data(data)
72
74
  end
73
-
75
+
74
76
  # Instantiates {Runner} for a given git object, runs it, and stores the result
75
77
  def run_commit(commit)
76
78
  result = @runner_class.new(
77
- working_dir: @working_dir,
79
+ working_dir: @working_dir,
78
80
  commit: commit[:sha],
79
81
  time: commit[:time],
80
82
  logger: @logger
81
83
  ).run!
82
-
84
+
83
85
  set_result(commit, result)
84
86
  end
85
-
87
+
86
88
  def format_commit_data(data)
87
89
  parts = data.split(' ')
88
90
  {
@@ -91,22 +93,22 @@ module TinyCI
91
93
  result: parts[2]
92
94
  }
93
95
  end
94
-
96
+
95
97
  # Repeatedly gets the list of eligable commits and runs TinyCI against them until there are no more remaining
96
98
  def run_all_commits
97
- commits = get_commits
98
-
99
- until commits.empty? do
100
- commits.each {|c| run_commit(c)}
101
-
102
- commits = get_commits
99
+ commits = retrieve_commits
100
+
101
+ until commits.empty?
102
+ commits.each { |c| run_commit(c) }
103
+
104
+ commits = retrieve_commits
103
105
  end
104
106
  end
105
-
107
+
106
108
  # Stores the result in a git note
107
109
  def set_result(commit, result)
108
110
  result_message = result ? 'success' : 'failure'
109
-
111
+
110
112
  execute git_cmd('notes', '--ref', 'tinyci-result', 'add', '-m', result_message, commit[:sha])
111
113
  end
112
114
  end
@@ -1,44 +1,48 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'open3'
4
+ require 'English'
2
5
  require 'tinyci/logging'
3
6
 
4
7
  module TinyCI
5
8
  # Methods for executing subprocesses in various ways and collecting the results.
6
9
  module Subprocesses
7
-
8
10
  # Synchronously execute a command as a subprocess and return the output.
9
- #
11
+ #
10
12
  # @param [Array<String>] command The command line
11
13
  # @param [String] label A label for debug and logging purposes
12
- #
14
+ #
13
15
  # @return [String] The output of the command
14
16
  # @raise [SubprocessError] if the subprocess returns status > 0
15
17
  def execute(*command, label: nil)
16
- output, status = Open3.capture2(*command.flatten)
17
-
18
+ stdout, stderr, status = Open3.capture3(*command.flatten)
19
+
18
20
  log_debug caller[0]
19
21
  log_debug "CMD: #{command.join(' ')}"
20
- log_debug "OUT: #{output}"
21
-
22
+ log_debug "OUT: #{stdout}"
23
+ log_debug "ERR: #{stderr}"
24
+
22
25
  unless status.success?
23
- log_error output
26
+ log_error stdout
27
+ log_error stderr
24
28
  raise SubprocessError.new(label, command.join(' '), status)
25
29
  end
26
-
27
- output.chomp
30
+
31
+ stdout.chomp
28
32
  end
29
-
33
+
30
34
  # Synchronously execute a chain multiple commands piped into each other as a
31
35
  # subprocess and return the output.
32
- #
36
+ #
33
37
  # @param [Array<Array<String>>] commands The command lines
34
38
  # @param [String] label A label for debug and logging purposes
35
- #
39
+ #
36
40
  # @return [String] The output of the command
37
41
  # @raise [SubprocessError] if the subprocess returns status > 0
38
42
  def execute_pipe(*commands, label: nil)
39
43
  stdout, waiters = Open3.pipeline_r(*commands)
40
44
  output = stdout.read
41
-
45
+
42
46
  waiters.each_with_index do |waiter, i|
43
47
  status = waiter.value
44
48
  unless status.success?
@@ -46,67 +50,72 @@ module TinyCI
46
50
  raise SubprocessError.new(label, commands[i].join(' '), status)
47
51
  end
48
52
  end
49
-
53
+
50
54
  output.chomp
51
55
  end
52
-
56
+
53
57
  # Synchronously execute a command as a subprocess and and stream the output
54
58
  # to `STDOUT`
55
- #
59
+ #
56
60
  # @param [Array<String>] command The command line
57
61
  # @param [String] label A label for debug and logging purposes
58
62
  # @param [String] pwd Optionally specify a different working directory in which to execute the command
59
- #
63
+ #
60
64
  # @return [TrueClass] `true` if the command executed successfully
61
65
  # @raise [SubprocessError] if the subprocess returns status > 0
62
66
  def execute_stream(*command, label: nil, pwd: nil)
63
67
  opts = {}
64
68
  opts[:chdir] = pwd unless pwd.nil?
65
-
69
+
66
70
  log_debug "CMD: #{command.join(' ')}"
67
-
71
+
68
72
  Open3.popen2e(command.join(' '), opts) do |stdin, stdout_and_stderr, wait_thr|
69
73
  stdin.close
70
-
74
+
71
75
  until stdout_and_stderr.closed? || stdout_and_stderr.eof?
72
76
  line = stdout_and_stderr.gets
73
77
  log_info line.chomp
74
78
  $stdout.flush
75
79
  end
76
-
77
- unless wait_thr.value.success?
80
+
81
+ unless wait_thr.value.success?
78
82
  raise SubprocessError.new(label, command.join(' '), wait_thr.value)
79
83
  end
84
+
85
+ ensure
80
86
  stdout_and_stderr.close
81
87
  end
82
-
88
+
83
89
  true
84
90
  end
85
-
91
+
92
+ def execute_and_return_status(command)
93
+ system(*command, out: File::NULL, err: File::NULL)
94
+
95
+ $CHILD_STATUS
96
+ end
97
+
86
98
  # An error raised when any of the {Subprocesses} methods fail
87
- #
99
+ #
88
100
  # @attr_reader [Integer] status The return code of the process
89
101
  # @attr_reader [String] command The command used to spawn the process
90
102
  class SubprocessError < RuntimeError
91
103
  attr_reader :status
92
104
  attr_reader :command
93
-
105
+
94
106
  def initialize(label, command, status, message = "#{label}: `#{command}` failed with status #{status.exitstatus}")
95
107
  @status = status
96
108
  @command = command
97
109
  super(message)
98
110
  end
99
111
  end
100
-
101
- private
102
-
112
+
103
113
  def self.included(base)
104
114
  base.include TinyCI::Logging
105
115
  end
106
-
116
+
107
117
  def self.extended(base)
108
118
  base.extend TinyCI::Logging
109
119
  end
110
-
111
120
  end
112
121
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module TinyCI
2
4
  module Symbolize
3
5
  # recursively make all keys of `hash` into symbols
@@ -7,7 +9,7 @@ module TinyCI
7
9
  hash.each { |key, value| h[key.to_sym] = map_value(value) }
8
10
  end
9
11
  end
10
-
12
+
11
13
  def map_value(thing)
12
14
  case thing
13
15
  when Hash
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tinyci/executor'
2
4
 
3
5
  module TinyCI
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'tinyci/executor'
2
4
 
3
5
  module TinyCI
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module TinyCI
2
- VERSION = "0.4"
4
+ VERSION = '0.5.1'
3
5
  end
data/lib/yard_plugin.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'redcarpet'
2
4
 
3
5
  # This file fixes the mismatch between how github handles links to other
@@ -27,7 +29,13 @@ class CompatMarkdown
27
29
 
28
30
  def initialize(text)
29
31
  renderer = YARD::Templates::Helpers::HtmlHelper::MDLinkRenderer.new
30
- markdown = Redcarpet::Markdown.new(renderer, no_intra_emphasis: true, gh_blockcode: true, fenced_code_blocks: true, autolink: true)
32
+ opts = {
33
+ no_intra_emphasis: true,
34
+ gh_blockcode: true,
35
+ fenced_code_blocks: true,
36
+ autolink: true
37
+ }
38
+ markdown = Redcarpet::Markdown.new(renderer, opts)
31
39
  @to_html = markdown.render(text)
32
40
  end
33
41
  end