arduino_ci 1.4.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,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)