arduino_ci 1.5.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/exe/arduino_ci.rb CHANGED
@@ -3,25 +3,34 @@ require 'arduino_ci'
3
3
  require 'set'
4
4
  require 'pathname'
5
5
  require 'optparse'
6
- require 'io/console'
7
6
 
8
- # be flexible between 80 and 132 cols of output
9
- WIDTH = begin
10
- [132, [80, IO::console.winsize[1] - 2].max].min
11
- rescue NoMethodError
12
- 80
13
- end
14
7
  VAR_CUSTOM_INIT_SCRIPT = "CUSTOM_INIT_SCRIPT".freeze
15
8
  VAR_USE_SUBDIR = "USE_SUBDIR".freeze
16
9
  VAR_EXPECT_EXAMPLES = "EXPECT_EXAMPLES".freeze
17
10
  VAR_EXPECT_UNITTESTS = "EXPECT_UNITTESTS".freeze
18
11
 
19
- @failure_count = 0
20
- @passfail = proc { |result| result ? "" : "✗" }
21
- @backend = nil
12
+ CLI_SKIP_EXAMPLES_COMPILATION = "--skip-examples-compilation".freeze
13
+ CLI_SKIP_UNITTESTS = "--skip-unittests".freeze
14
+
15
+ # script-level variables we'll use
16
+ @log = nil
17
+ @backend = nil
18
+ @cli_options = nil
22
19
 
23
20
  # Use some basic parsing to allow command-line overrides of config
24
21
  class Parser
22
+
23
+ def self.show_help(opts)
24
+ puts opts
25
+ puts
26
+ puts "Additionally, the following environment variables control the script:"
27
+ puts " - #{VAR_CUSTOM_INIT_SCRIPT} - if set, this script will be run from the Arduino/libraries directory"
28
+ puts " prior to any automated library installation or testing (e.g. to install unofficial libraries)"
29
+ puts " - #{VAR_USE_SUBDIR} - if set, the script will install the library from this subdirectory of the cwd"
30
+ puts " - #{VAR_EXPECT_EXAMPLES} - if set, testing will fail if no example sketches are present"
31
+ puts " - #{VAR_EXPECT_UNITTESTS} - if set, testing will fail if no unit tests are present"
32
+ end
33
+
25
34
  def self.parse(options)
26
35
  unit_config = {}
27
36
  output_options = {
@@ -36,11 +45,11 @@ class Parser
36
45
  opt_parser = OptionParser.new do |opts|
37
46
  opts.banner = "Usage: #{File.basename(__FILE__)} [options]"
38
47
 
39
- opts.on("--skip-unittests", "Don't run unit tests") do |p|
48
+ opts.on(CLI_SKIP_UNITTESTS, "Don't run unit tests") do |p|
40
49
  output_options[:skip_unittests] = p
41
50
  end
42
51
 
43
- opts.on("--skip-examples-compilation", "Don't compile example sketches") do |p|
52
+ opts.on(CLI_SKIP_EXAMPLES_COMPILATION, "Don't compile example sketches") do |p|
44
53
  output_options[:skip_compilation] = p
45
54
  end
46
55
 
@@ -61,140 +70,60 @@ class Parser
61
70
  end
62
71
 
63
72
  opts.on("-h", "--help", "Prints this help") do
64
- puts opts
65
- puts
66
- puts "Additionally, the following environment variables control the script:"
67
- puts " - #{VAR_CUSTOM_INIT_SCRIPT} - if set, this script will be run from the Arduino/libraries directory"
68
- puts " prior to any automated library installation or testing (e.g. to install unofficial libraries)"
69
- puts " - #{VAR_USE_SUBDIR} - if set, the script will install the library from this subdirectory of the cwd"
70
- puts " - #{VAR_EXPECT_EXAMPLES} - if set, testing will fail if no example sketches are present"
71
- puts " - #{VAR_EXPECT_UNITTESTS} - if set, testing will fail if no unit tests are present"
73
+ show_help(opts)
72
74
  exit
73
75
  end
74
76
  end
75
77
 
76
- opt_parser.parse!(options)
78
+ begin
79
+ opt_parser.parse!(options)
80
+ rescue OptionParser::InvalidOption => e
81
+ puts e
82
+ puts
83
+ show_help(opt_parser)
84
+ exit 1
85
+ end
77
86
  output_options
78
87
  end
79
88
  end
80
89
 
81
- # Read in command line options and make them read-only
82
- @cli_options = (Parser.parse ARGV).freeze
83
-
90
+ # print debugging information from the backend, to be used when things don't go as expected
84
91
  def print_backend_logs
85
- puts "========== Last backend command (if relevant):"
86
- puts @backend.last_msg.to_s
87
- puts "========== Backend Stdout:"
88
- puts @backend.last_out
89
- puts "========== Backend Stderr:"
90
- puts @backend.last_err
92
+ @log.iputs "========== Last backend command (if relevant):"
93
+ @log.iputs @backend.last_msg.to_s
94
+ @log.iputs "========== Backend Stdout:"
95
+ @log.iputs @backend.last_out
96
+ @log.iputs "========== Backend Stderr:"
97
+ @log.iputs @backend.last_err
98
+ end
99
+
100
+ # describe the last command, to help troubleshoot a failure
101
+ #
102
+ # @param cpp_library [CppLibrary]
103
+ def describe_last_command(cpp_library)
104
+ @log.iputs "Last command: #{cpp_library.last_cmd}"
105
+ @log.iputs cpp_library.last_out
106
+ @log.iputs cpp_library.last_err
91
107
  end
92
108
 
93
109
  # terminate after printing any debug info. TODO: capture debug info
94
110
  def terminate(final = nil)
95
- puts "Failures: #{@failure_count}"
96
- print_backend_logs unless @failure_count.zero? || final || @backend.nil?
97
- retcode = @failure_count.zero? ? 0 : 1
111
+ puts "Failures: #{@log.failure_count}"
112
+ print_backend_logs unless @log.failure_count.zero? || final || @backend.nil?
113
+ retcode = @log.failure_count.zero? ? 0 : 1
98
114
  exit(retcode)
99
115
  end
100
116
 
101
- # make a nice status line for an action and react to the action
102
- # TODO / note to self: inform_multiline is tougher to write
103
- # without altering the signature because it only leaves space
104
- # for the checkmark _after_ the multiline, it doesn't know how
105
- # to make that conditionally the body
106
- # @param message String the text of the progress indicator
107
- # @param multiline boolean whether multiline output is expected
108
- # @param mark_fn block (string) -> string that says how to describe the result
109
- # @param on_fail_msg String custom message for failure
110
- # @param tally_on_fail boolean whether to increment @failure_count
111
- # @param abort_on_fail boolean whether to abort immediately on failure (i.e. if this is a fatal error)
112
- def perform_action(message, multiline, mark_fn, on_fail_msg, tally_on_fail, abort_on_fail)
113
- line = "#{message}... "
114
- endline = "...#{message} "
115
- if multiline
116
- puts line
117
- else
118
- print line
119
- end
120
- $stdout.flush
121
- result = yield
122
- mark = mark_fn.nil? ? "" : mark_fn.call(result)
123
- # if multiline, put checkmark at full width
124
- print endline if multiline
125
- puts mark.to_s.rjust(WIDTH - line.length, " ")
126
- unless result
127
- puts on_fail_msg unless on_fail_msg.nil?
128
- @failure_count += 1 if tally_on_fail
129
- # print out error messaging here if we've captured it
130
- terminate if abort_on_fail
131
- end
132
- result
133
- end
134
-
135
- # Make a nice status for something that defers any failure code until script exit
136
- def attempt(message, &block)
137
- perform_action(message, false, @passfail, nil, true, false, &block)
138
- end
139
-
140
- # Make a nice status for something that defers any failure code until script exit
141
- def attempt_multiline(message, &block)
142
- perform_action(message, true, @passfail, nil, true, false, &block)
143
- end
144
-
145
- # Make a nice status for something that kills the script immediately on failure
146
- FAILED_ASSURANCE_MESSAGE = "This may indicate a problem with your configuration; halting here".freeze
147
- def assure(message, &block)
148
- perform_action(message, false, @passfail, FAILED_ASSURANCE_MESSAGE, true, true, &block)
149
- end
150
-
151
- def assure_multiline(message, &block)
152
- perform_action(message, true, @passfail, FAILED_ASSURANCE_MESSAGE, true, true, &block)
153
- end
154
-
155
- def inform(message, &block)
156
- perform_action(message, false, proc { |x| x }, nil, false, false, &block)
157
- end
158
-
159
- def inform_multiline(message, &block)
160
- perform_action(message, true, nil, nil, false, false, &block)
161
- end
162
-
163
- def rule(char)
164
- puts char[0] * WIDTH
165
- end
166
-
167
- def warn(message)
168
- inform("WARNING") { message }
169
- end
170
-
171
- def phase(name)
172
- puts
173
- rule("=")
174
- inform("Beginning the next phase of testing") { name }
175
- end
176
-
177
- def banner
178
- art = [
179
- " . __ ___",
180
- " _, ,_ _| , . * ._ _ / ` | ",
181
- "(_| [ `(_] (_| | [ ) (_) \\__. _|_ v#{ArduinoCI::VERSION}",
182
- ]
183
-
184
- pad = " " * ((WIDTH - art[2].length) / 2)
185
- art.each { |l| puts "#{pad}#{l}" }
186
- puts
187
- end
188
-
189
117
  # Assure that a platform exists and return its definition
190
118
  def assured_platform(purpose, name, config)
191
119
  platform_definition = config.platform_definition(name)
192
- assure("Requested #{purpose} platform '#{name}' is defined in 'platforms' YML") { !platform_definition.nil? }
120
+ @log.assure("Requested #{purpose} platform '#{name}' is defined in 'platforms' YML") { !platform_definition.nil? }
193
121
  platform_definition
194
122
  end
195
123
 
124
+ # Perform a config override while explaining it to the user
196
125
  def inform_override(from_where, &block)
197
- inform("Using configuration override from #{from_where}") do
126
+ @log.inform("Using configuration override from #{from_where}") do
198
127
  file = block.call
199
128
  file.nil? ? "<none>" : file
200
129
  end
@@ -219,29 +148,50 @@ def display_files(pathname)
219
148
  non_hidden = all_files.reject { |path| file_is_hidden_somewhere?(path) }
220
149
 
221
150
  # print files with an indent
222
- puts " Files (excluding hidden files): #{non_hidden.size}"
223
- non_hidden.each { |p| puts " #{p}" }
151
+ @log.iputs "Files (excluding hidden files): #{non_hidden.size}"
152
+ @log.indent { non_hidden.each(&@log.method(:iputs)) }
224
153
  end
225
154
 
155
+ # helper recursive function for library installation
156
+ #
157
+ # This recursively descends the dependency tree starting from an initial list,
158
+ # and either uses existing installations (based on directory naming only) or
159
+ # forcibly installs the dependency. Each child dependency logs which parent requested it
160
+ #
161
+ # @param library_names [Array<String>] the list of libraries to install
162
+ # @param on_behalf_of [String] the requestor of a given dependency
163
+ # @param already_installed [Array<String>] the set of dependencies installed by previous steps
226
164
  # @return [Array<String>] The list of installed libraries
227
- def install_arduino_library_dependencies(library_names, on_behalf_of, already_installed = [])
165
+ def install_arduino_library_dependencies_h(library_names, on_behalf_of, already_installed)
228
166
  installed = already_installed.clone
229
167
  (library_names.map { |n| @backend.library_of_name(n) } - installed).each do |l|
230
168
  if l.installed?
231
- inform("Using pre-existing dependency of #{on_behalf_of}") { l.name }
169
+ @log.inform("Using pre-existing dependency of #{on_behalf_of}") { l.name }
232
170
  else
233
- assure("Installing dependency of #{on_behalf_of}: '#{l.name}'") do
171
+ @log.assure("Installing dependency of #{on_behalf_of}: '#{l.name}'") do
234
172
  next nil unless l.install
235
173
 
236
174
  l.name
237
175
  end
238
176
  end
239
177
  installed << l.name
240
- installed += install_arduino_library_dependencies(l.arduino_library_dependencies, l.name, installed)
178
+ installed += install_arduino_library_dependencies_h(l.arduino_library_dependencies, l.name, installed)
241
179
  end
242
180
  installed
243
181
  end
244
182
 
183
+ # @return [Array<String>] The list of installed libraries
184
+ def install_arduino_library_dependencies(library_names, on_behalf_of)
185
+ if library_names.empty?
186
+ @log.inform("Arduino library dependencies (configured in #{on_behalf_of}) to resolve") { library_names.length }
187
+ return []
188
+ end
189
+
190
+ @log.inform_multiline("Resolving #{library_names.length} Arduino library dependencies configured in #{on_behalf_of})") do
191
+ install_arduino_library_dependencies_h(library_names, on_behalf_of, [])
192
+ end
193
+ end
194
+
245
195
  # @param platforms [Array<String>] list of platforms to consider
246
196
  # @param specific_config [CIConfig] configuration to use
247
197
  def install_all_packages(platforms, specific_config)
@@ -252,28 +202,29 @@ def install_all_packages(platforms, specific_config)
252
202
  all_packages.each do |pkg|
253
203
  next if @backend.boards_installed?(pkg)
254
204
 
255
- url = assure("Board package #{pkg} has a defined URL") { specific_config.package_url(pkg) }
205
+ url = @log.assure("Board package #{pkg} has a defined URL") { specific_config.package_url(pkg) }
256
206
  @backend.board_manager_urls = [url]
257
- assure("Installing board package #{pkg}") { @backend.install_boards(pkg) }
207
+ @log.assure("Installing board package #{pkg}") { @backend.install_boards(pkg) }
258
208
  end
259
209
  end
260
210
 
261
211
  # @param expectation_envvar [String] the name of the env var to check
262
212
  # @param operation [String] a description of what operation we might be skipping
213
+ # @param howto_skip [String] a description of how the runner can skip this
263
214
  # @param filegroup_name [String] a description of the set of files without which we effectively skip the operation
264
215
  # @param dir_description [String] a description of the directory where we looked for the files
265
216
  # @param dir [Pathname] the directory where we looked for the files
266
- def handle_expectation_of_files(expectation_envvar, operation, filegroup_name, dir_description, dir_path)
217
+ def handle_expectation_of_files(expectation_envvar, operation, howto_skip, filegroup_name, dir_description, dir_path)
267
218
  # alert future me about running the script from the wrong directory, instead of doing the huge file dump
268
219
  # otherwise, assume that the user might be running the script on a library with no actual unit tests
269
220
  if Pathname.new(__dir__).parent == Pathname.new(Dir.pwd)
270
- inform_multiline("arduino_ci seems to be trying to test itself") do
221
+ @log.inform_multiline("arduino_ci seems to be trying to test itself") do
271
222
  [
272
223
  "arduino_ci (the ruby gem) isn't an arduino project itself, so running the CI test script against",
273
224
  "the core library isn't really a valid thing to do... but it's easy for a developer (including the",
274
225
  "owner) to mistakenly do just that. Hello future me, you probably meant to run this against one of",
275
226
  "the sample projects in SampleProjects/ ... if not, please submit a bug report; what a wild case!"
276
- ].each { |l| puts " #{l}" }
227
+ ].each(&@log.method(:iputs))
277
228
  false
278
229
  end
279
230
  exit(1)
@@ -286,25 +237,27 @@ def handle_expectation_of_files(expectation_envvar, operation, filegroup_name, d
286
237
  ["No #{dir_description} at", "base directory", dir_path.parent]
287
238
  end
288
239
 
289
- inform(problem) { dir_path }
290
- explain_and_exercise_envvar(expectation_envvar, operation, "contents of #{dir_desc}") { display_files(dir) }
240
+ @log.inform(problem) { dir_path }
241
+ explain_and_exercise_envvar(expectation_envvar, operation, howto_skip, "contents of #{dir_desc}") { display_files(dir) }
291
242
  end
292
243
 
293
244
  # @param expectation_envvar [String] the name of the env var to check
294
245
  # @param operation [String] a description of what operation we might be skipping
246
+ # @param howto_skip [String] a description of how the runner can skip this
295
247
  # @param block_desc [String] a description of what information will be dumped to assist the user
296
248
  # @param block [Proc] a function that dumps information
297
- def explain_and_exercise_envvar(expectation_envvar, operation, block_desc, &block)
298
- inform("Environment variable #{expectation_envvar} is") { "(#{ENV[expectation_envvar].class}) #{ENV[expectation_envvar]}" }
249
+ def explain_and_exercise_envvar(expectation_envvar, operation, howto_skip, block_desc, &block)
250
+ @log.inform("Environment variable #{expectation_envvar} is") { "(#{ENV[expectation_envvar].class}) #{ENV[expectation_envvar]}" }
299
251
  if ENV[expectation_envvar].nil?
300
- inform_multiline("Skipping #{operation}") do
301
- puts " In case that's an error, displaying #{block_desc}:"
252
+ @log.inform_multiline("Skipping #{operation}") do
253
+ @log.iputs "In case that's an error, displaying #{block_desc}:"
302
254
  block.call
303
- puts " To force an error in this case, set the environment variable #{expectation_envvar}"
255
+ @log.iputs "To force an error in this case, set the environment variable #{expectation_envvar}"
256
+ @log.iputs "To explicitly skip this check, use #{howto_skip}"
304
257
  true
305
258
  end
306
259
  else
307
- assure_multiline("Displaying #{block_desc} before exit") do
260
+ @log.assure_multiline("Displaying #{block_desc} before exit") do
308
261
  block.call
309
262
  false
310
263
  end
@@ -315,16 +268,16 @@ end
315
268
  def get_annotated_compilers(config, cpp_library)
316
269
  # check GCC
317
270
  compilers = config.compilers_to_use
318
- assure("The set of compilers (#{compilers.length}) isn't empty") { !compilers.empty? }
271
+ @log.assure("The set of compilers (#{compilers.length}) isn't empty") { !compilers.empty? }
319
272
  compilers.each do |gcc_binary|
320
- attempt_multiline("Checking #{gcc_binary} version") do
273
+ @log.attempt_multiline("Checking #{gcc_binary} version") do
321
274
  version = cpp_library.gcc_version(gcc_binary)
322
275
  next nil unless version
323
276
 
324
- puts version.split("\n").map { |l| " #{l}" }.join("\n")
277
+ @log.iputs(version)
325
278
  version
326
279
  end
327
- inform("libasan availability for #{gcc_binary}") { cpp_library.libasan?(gcc_binary) }
280
+ @log.inform("libasan availability for #{gcc_binary}") { cpp_library.libasan?(gcc_binary) }
328
281
  end
329
282
  compilers
330
283
  end
@@ -336,22 +289,81 @@ end
336
289
  # In this case, the user provided script would fetch a git repo or some other method
337
290
  def perform_custom_initialization(_config)
338
291
  script_path = ENV[VAR_CUSTOM_INIT_SCRIPT]
339
- inform("Environment variable #{VAR_CUSTOM_INIT_SCRIPT}") { "'#{script_path}'" }
292
+ @log.inform("Environment variable #{VAR_CUSTOM_INIT_SCRIPT}") { "'#{script_path}'" }
340
293
  return if script_path.nil?
341
294
  return if script_path.empty?
342
295
 
343
296
  script_pathname = Pathname.getwd + script_path
344
- assure("Script at #{VAR_CUSTOM_INIT_SCRIPT} exists") { script_pathname.exist? }
297
+ @log.assure("Script at #{VAR_CUSTOM_INIT_SCRIPT} exists") { script_pathname.exist? }
345
298
 
346
- assure_multiline("Running #{script_pathname} with sh in libraries working dir") do
299
+ @log.assure_multiline("Running #{script_pathname} with sh in libraries working dir") do
347
300
  Dir.chdir(@backend.lib_dir) do
348
301
  IO.popen(["/bin/sh", script_pathname.to_s], err: [:child, :out]) do |io|
349
- io.each_line { |line| puts " #{line}" }
302
+ @log.indent { io.each_line(&@log.method(:iputs)) }
350
303
  end
351
304
  end
352
305
  end
353
306
  end
354
307
 
308
+ # Kick off the arduino_ci test process by explaining and adjusting the environment
309
+ #
310
+ # @return Hash of things needed for later steps
311
+ def perform_bootstrap
312
+ @log.inform("Host OS") { ArduinoCI::Host.os }
313
+ @log.inform("Working directory") { Dir.pwd }
314
+
315
+ # initialize command and config
316
+ default_config = ArduinoCI::CIConfig.default
317
+ inform_override("project") { default_config.override_file_from_project_library }
318
+ config = default_config.from_project_library
319
+
320
+ backend = ArduinoCI::ArduinoInstallation.autolocate!
321
+ @log.inform("Located arduino-cli binary") { backend.binary_path.to_s }
322
+ @log.inform("Using arduino-cli version") { backend.version.to_s }
323
+ if backend.lib_dir.exist?
324
+ @log.inform("Found libraries directory") { backend.lib_dir }
325
+ else
326
+ @log.assure("Creating libraries directory") { backend.lib_dir.mkpath || true }
327
+ end
328
+
329
+ # run any library init scripts from the library itself.
330
+ perform_custom_initialization(config)
331
+
332
+ # initialize library under test
333
+ @log.inform("Environment variable #{VAR_USE_SUBDIR}") { "'#{ENV[VAR_USE_SUBDIR]}'" }
334
+ cpp_library_path = Pathname.new(ENV[VAR_USE_SUBDIR].nil? ? "." : ENV[VAR_USE_SUBDIR])
335
+ cpp_library = @log.assure("Installing library under test") do
336
+ backend.install_local_library(cpp_library_path)
337
+ end
338
+
339
+ # Warn if the library name isn't obvious
340
+ assumed_name = backend.name_of_library(cpp_library_path)
341
+ ondisk_name = cpp_library_path.realpath.basename.to_s
342
+ @log.warn("Installed library named '#{assumed_name}' has directory name '#{ondisk_name}'") if assumed_name != ondisk_name
343
+
344
+ if !cpp_library.nil?
345
+ @log.inform("Library installed at") { cpp_library.path.to_s }
346
+ else
347
+ # this is a longwinded way of failing, we aren't really "assuring" anything at this point
348
+ @log.assure_multiline("Library installed successfully") do
349
+ @log.iputs backend.last_msg
350
+ false
351
+ end
352
+ end
353
+
354
+ install_arduino_library_dependencies(
355
+ cpp_library.arduino_library_dependencies,
356
+ "<#{ArduinoCI::CppLibrary::LIBRARY_PROPERTIES_FILE}>"
357
+ )
358
+
359
+ # return all objects needed by other steps
360
+ {
361
+ backend: backend,
362
+ cpp_library: cpp_library,
363
+ config: config,
364
+ }
365
+ end
366
+
355
367
  # Auto-select some platforms to test based on the information available
356
368
  #
357
369
  # Top choice is always library.properties -- otherwise use the default.
@@ -368,14 +380,14 @@ def choose_platform_set(config, reason, desired_platforms, library_properties)
368
380
  if library_properties.nil? || library_properties.architectures.nil? || library_properties.architectures.empty?
369
381
  # verify that all platforms exist
370
382
  desired_platforms.each { |p| assured_platform(reason, p, config) }
371
- return inform_multiline("No architectures listed in library.properties, using configured platforms") do
372
- desired_platforms.each { |p| puts " #{p}" } # this returns desired_platforms
383
+ return @log.inform_multiline("No architectures listed in library.properties, using configured platforms") do
384
+ desired_platforms.each(&@log.method(:iputs)) # this returns desired_platforms
373
385
  end
374
386
  end
375
387
 
376
388
  if library_properties.architectures.include?("*")
377
- return inform_multiline("Wildcard architecture in library.properties, using configured platforms") do
378
- desired_platforms.each { |p| puts " #{p}" } # this returns desired_platforms
389
+ return @log.inform_multiline("Wildcard architecture in library.properties, using configured platforms") do
390
+ desired_platforms.each(&@log.method(:iputs)) # this returns desired_platforms
379
391
  end
380
392
  end
381
393
 
@@ -385,76 +397,81 @@ def choose_platform_set(config, reason, desired_platforms, library_properties)
385
397
  if config.is_default
386
398
  # completely ignore default config, opting for brute-force library matches
387
399
  # OTOH, we don't need to assure platforms because we defined them
388
- return inform_multiline("Default config, platforms matching architectures in library.properties") do
400
+ return @log.inform_multiline("Default config, platforms matching architectures in library.properties") do
389
401
  supported_platforms.keys.each do |p| # rubocop:disable Style/HashEachMethods
390
- puts " #{p}"
402
+ @log.iputs(p)
391
403
  end # this returns supported_platforms
392
404
  end
393
405
  end
394
406
 
395
407
  desired_supported_platforms = supported_platforms.select { |p, _| desired_platforms.include?(p) }.keys
396
408
  desired_supported_platforms.each { |p| assured_platform(reason, p, config) }
397
- inform_multiline("Configured platforms that match architectures in library.properties") do
409
+ @log.inform_multiline("Configured platforms that match architectures in library.properties") do
398
410
  desired_supported_platforms.each do |p|
399
- puts " #{p}"
411
+ @log.iputs(p)
400
412
  end # this returns supported_platforms
401
413
  end
402
414
  end
403
415
 
404
416
  # Unit test procedure
405
417
  def perform_unit_tests(cpp_library, file_config)
406
- phase("Unit testing")
418
+ @log.phase("Unit testing")
407
419
  if @cli_options[:skip_unittests]
408
- inform("Skipping unit tests") { "as requested via command line" }
420
+ @log.inform("Skipping unit tests") { "as requested via command line" }
409
421
  return
410
422
  end
411
423
 
412
424
  config = file_config.with_override_config(@cli_options[:ci_config])
413
425
  compilers = get_annotated_compilers(config, cpp_library)
414
426
 
415
- inform("Library conforms to Arduino library specification") { cpp_library.one_point_five? ? "1.5" : "1.0" }
427
+ @log.inform("Library conforms to Arduino library specification") { cpp_library.one_point_five? ? "1.5" : "1.0" }
416
428
 
417
429
  # Handle lack of test files
418
430
  if cpp_library.test_files.empty?
419
- handle_expectation_of_files(VAR_EXPECT_UNITTESTS, "unit tests", "test files", "tests directory", cpp_library.tests_dir)
431
+ handle_expectation_of_files(
432
+ VAR_EXPECT_UNITTESTS,
433
+ "unit tests",
434
+ CLI_SKIP_UNITTESTS,
435
+ "test files",
436
+ "tests directory",
437
+ cpp_library.tests_dir
438
+ )
420
439
  return
421
440
  end
422
441
 
423
442
  # Get platforms, handle lack of them
424
443
  platforms = choose_platform_set(config, "unittest", config.platforms_to_unittest, cpp_library.library_properties)
425
444
  if platforms.empty?
426
- explain_and_exercise_envvar(VAR_EXPECT_UNITTESTS, "unit tests", "platforms and architectures") do
427
- puts " Configured platforms: #{config.platforms_to_unittest}"
428
- puts " Configuration is default: #{config.is_default}"
445
+ explain_and_exercise_envvar(VAR_EXPECT_UNITTESTS, "unit tests", CLI_SKIP_UNITTESTS, "platforms and architectures") do
446
+ @log.iputs "Configured platforms: #{config.platforms_to_unittest}"
447
+ @log.iputs "Configuration is default: #{config.is_default}"
429
448
  arches = cpp_library.library_properties.nil? ? nil : cpp_library.library_properties.architectures
430
- puts " Architectures in library.properties: #{arches}"
449
+ @log.iputs "Architectures in library.properties: #{arches}"
431
450
  end
432
451
  end
433
452
 
434
453
  # having undefined platforms is a config error
435
454
  platforms.select { |p| config.platform_info[p].nil? }.each do |p|
436
- assure("Platform '#{p}' is defined in configuration files") { false }
455
+ @log.assure("Platform '#{p}' is defined in configuration files") { false }
437
456
  end
438
457
 
439
458
  install_arduino_library_dependencies(config.aux_libraries_for_unittest, "<unittest/libraries>")
440
459
 
441
460
  platforms.each do |p|
442
- puts
461
+ @log.iputs
443
462
  compilers.each do |gcc_binary|
444
463
  # before compiling the tests, build a shared library of everything except the test code
445
- next @failure_count += 1 unless build_shared_library(gcc_binary, p, config, cpp_library)
464
+ next @log.failure_count += 1 unless build_shared_library(gcc_binary, p, config, cpp_library)
446
465
 
447
466
  # now build and run each test using the shared library build above
448
467
  config.allowable_unittest_files(cpp_library.test_files).each do |unittest_path|
449
468
  unittest_name = unittest_path.basename.to_s
450
- puts "--------------------------------------------------------------------------------"
451
- attempt_multiline("Unit testing #{unittest_name} with #{gcc_binary} for #{p}") do
469
+ @log.rule "-"
470
+ @log.attempt_multiline("Unit testing #{unittest_name} with #{gcc_binary} for #{p}") do
452
471
  exe = cpp_library.build_for_test(unittest_path, gcc_binary)
453
- puts
472
+ @log.iputs
454
473
  unless exe
455
- puts "Last command: #{cpp_library.last_cmd}"
456
- puts cpp_library.last_out
457
- puts cpp_library.last_err
474
+ describe_last_command(cpp_library)
458
475
  next false
459
476
  end
460
477
  cpp_library.run_test_file(exe)
@@ -465,33 +482,36 @@ def perform_unit_tests(cpp_library, file_config)
465
482
  end
466
483
 
467
484
  def build_shared_library(gcc_binary, platform, config, cpp_library)
468
- attempt_multiline("Build shared library with #{gcc_binary} for #{platform}") do
485
+ @log.attempt_multiline("Build shared library with #{gcc_binary} for #{platform}") do
469
486
  exe = cpp_library.build_shared_library(
470
487
  config.aux_libraries_for_unittest,
471
488
  gcc_binary,
472
489
  config.gcc_config(platform)
473
490
  )
474
- puts
475
- unless exe
476
- puts "Last command: #{cpp_library.last_cmd}"
477
- puts cpp_library.last_out
478
- puts cpp_library.last_err
479
- end
491
+ @log.iputs
492
+ describe_last_command(cpp_library) unless exe
480
493
  exe
481
494
  end
482
495
  end
483
496
 
484
497
  def perform_example_compilation_tests(cpp_library, config)
485
- phase("Compilation of example sketches")
498
+ @log.phase("Compilation of example sketches")
486
499
  if @cli_options[:skip_compilation]
487
- inform("Skipping compilation of examples") { "as requested via command line" }
500
+ @log.inform("Skipping compilation of examples") { "as requested via command line" }
488
501
  return
489
502
  end
490
503
 
491
504
  library_examples = cpp_library.example_sketches
492
505
 
493
506
  if library_examples.empty?
494
- handle_expectation_of_files(VAR_EXPECT_EXAMPLES, "builds", "examples", "the examples directory", cpp_library.examples_dir)
507
+ handle_expectation_of_files(
508
+ VAR_EXPECT_EXAMPLES,
509
+ "builds",
510
+ CLI_SKIP_EXAMPLES_COMPILATION,
511
+ "examples",
512
+ "the examples directory",
513
+ cpp_library.examples_dir
514
+ )
495
515
  return
496
516
  end
497
517
 
@@ -500,8 +520,8 @@ def perform_example_compilation_tests(cpp_library, config)
500
520
 
501
521
  library_examples.each do |example_path|
502
522
  example_name = File.basename(example_path)
503
- puts
504
- inform("Discovered example sketch") { example_name }
523
+ @log.iputs
524
+ @log.inform("Discovered example sketch") { example_name }
505
525
 
506
526
  inform_override("example") { ex_config.override_file_from_example(example_path) }
507
527
  ovr_config = ex_config.from_example(example_path)
@@ -510,17 +530,22 @@ def perform_example_compilation_tests(cpp_library, config)
510
530
 
511
531
  # having no platforms defined is probably an error
512
532
  if platforms.empty?
513
- explain_and_exercise_envvar(VAR_EXPECT_EXAMPLES, "examples compilation", "platforms and architectures") do
514
- puts " Configured platforms: #{ovr_config.platforms_to_build}"
515
- puts " Configuration is default: #{ovr_config.is_default}"
533
+ explain_and_exercise_envvar(
534
+ VAR_EXPECT_EXAMPLES,
535
+ "examples compilation",
536
+ CLI_SKIP_EXAMPLES_COMPILATION,
537
+ "platforms and architectures"
538
+ ) do
539
+ @log.iputs "Configured platforms: #{ovr_config.platforms_to_build}"
540
+ @log.iputs "Configuration is default: #{ovr_config.is_default}"
516
541
  arches = cpp_library.library_properties.nil? ? nil : cpp_library.library_properties.architectures
517
- puts " Architectures in library.properties: #{arches}"
542
+ @log.iputs "Architectures in library.properties: #{arches}"
518
543
  end
519
544
  end
520
545
 
521
546
  # having undefined platforms is a config error
522
547
  platforms.select { |p| ovr_config.platform_info[p].nil? }.each do |p|
523
- assure("Platform '#{p}' is defined in configuration files") { false }
548
+ @log.assure("Platform '#{p}' is defined in configuration files") { false }
524
549
  end
525
550
 
526
551
  install_all_packages(platforms, ovr_config)
@@ -528,75 +553,58 @@ def perform_example_compilation_tests(cpp_library, config)
528
553
 
529
554
  platforms.each do |p|
530
555
  board = ovr_config.platform_info[p][:board] # assured to exist, above
531
- attempt("Compiling #{example_name} for #{board}") do
532
- ret = @backend.compile_sketch(example_path, board)
533
- unless ret
534
- puts "Last command: #{@backend.last_msg}"
535
- puts @backend.last_err
556
+ compiled_ok = @log.attempt("Compiling #{example_name} for #{board}") do
557
+ @backend.compile_sketch(example_path, board)
558
+ end
559
+
560
+ # decode the JSON output of the compiler a little bit
561
+ unless compiled_ok
562
+ @log.inform_multiline("Compilation failure details") do
563
+ begin
564
+ # parse the JSON, and print out only the nonempty keys. indent them with 4 spaces in their own labelled sections
565
+ msg_json = JSON.parse(@backend.last_msg)
566
+ msg_json.each do |k, v|
567
+ val = if v.is_a?(Hash) || v.is_a?(Array)
568
+ JSON.pretty_generate(v)
569
+ else
570
+ v.to_s
571
+ end
572
+ @log.inform_multiline(k) { @log.iputs(val) } unless val.strip.empty?
573
+ end
574
+ rescue JSON::ParserError
575
+ # worst case: dump it
576
+ @log.iputs "Last command: #{@backend.last_msg}"
577
+ end
578
+ @log.iputs @backend.last_err
536
579
  end
537
- ret
538
580
  end
539
581
 
582
+ # reporting or enforcing of free space
583
+ usage = @backend.last_bytes_usage
584
+ @log.inform("Free space (bytes) after compilation") { usage[:free] }
540
585
  next if @cli_options[:min_free_space].nil?
541
586
 
542
- usage = @backend.last_bytes_usage
543
587
  min_free_space = @cli_options[:min_free_space]
544
- attempt("Checking that free space of #{usage[:free]} is less than desired minimum #{min_free_space}") do
588
+ @log.attempt("Free space exceeds desired minimum #{min_free_space}") do
545
589
  min_free_space <= usage[:free]
546
590
  end
547
591
  end
548
592
  end
549
593
  end
550
594
 
551
- banner
552
- inform("Host OS") { ArduinoCI::Host.os }
553
- inform("Working directory") { Dir.pwd }
554
-
555
- # initialize command and config
556
- default_config = ArduinoCI::CIConfig.default
557
- inform_override("project") { default_config.override_file_from_project_library }
558
- config = default_config.from_project_library
559
-
560
- @backend = ArduinoCI::ArduinoInstallation.autolocate!
561
- inform("Located arduino-cli binary") { @backend.binary_path.to_s }
562
- inform("Using arduino-cli version") { @backend.version.to_s }
563
- if @backend.lib_dir.exist?
564
- inform("Found libraries directory") { @backend.lib_dir }
565
- else
566
- assure("Creating libraries directory") { @backend.lib_dir.mkpath || true }
567
- end
568
-
569
- # run any library init scripts from the library itself.
570
- perform_custom_initialization(config)
571
-
572
- # initialize library under test
573
- inform("Environment variable #{VAR_USE_SUBDIR}") { "'#{ENV[VAR_USE_SUBDIR]}'" }
574
- cpp_library_path = Pathname.new(ENV[VAR_USE_SUBDIR].nil? ? "." : ENV[VAR_USE_SUBDIR])
575
- cpp_library = assure("Installing library under test") do
576
- @backend.install_local_library(cpp_library_path)
577
- end
595
+ ###############################################################
596
+ # script execution
597
+ #
578
598
 
579
- # Warn if the library name isn't obvious
580
- assumed_name = @backend.name_of_library(cpp_library_path)
581
- ondisk_name = cpp_library_path.realpath.basename.to_s
582
- warn("Installed library named '#{assumed_name}' has directory name '#{ondisk_name}'") if assumed_name != ondisk_name
583
-
584
- if !cpp_library.nil?
585
- inform("Library installed at") { cpp_library.path.to_s }
586
- else
587
- # this is a longwinded way of failing, we aren't really "assuring" anything at this point
588
- assure_multiline("Library installed successfully") do
589
- puts @backend.last_msg
590
- false
591
- end
592
- end
599
+ # Read in command line options and make them read-only
600
+ @cli_options = Parser.parse(ARGV).freeze
593
601
 
594
- install_arduino_library_dependencies(
595
- cpp_library.arduino_library_dependencies,
596
- "<#{ArduinoCI::CppLibrary::LIBRARY_PROPERTIES_FILE}>"
597
- )
602
+ @log = ArduinoCI::Logger.auto_width
603
+ @log.banner
598
604
 
599
- perform_unit_tests(cpp_library, config)
600
- perform_example_compilation_tests(cpp_library, config)
605
+ strap = perform_bootstrap
606
+ @backend = strap[:backend]
607
+ perform_unit_tests(strap[:cpp_library], strap[:config])
608
+ perform_example_compilation_tests(strap[:cpp_library], strap[:config])
601
609
 
602
610
  terminate(true)