arduino_ci 1.4.0 → 1.6.0

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