arduino_ci 1.5.0 → 1.6.1

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.
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)