arduino_ci 0.3.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +125 -69
  3. data/REFERENCE.md +711 -0
  4. data/cpp/arduino/Arduino.h +1 -7
  5. data/cpp/arduino/ArduinoDefines.h +3 -0
  6. data/cpp/arduino/AvrMath.h +117 -17
  7. data/cpp/arduino/Client.h +27 -0
  8. data/cpp/arduino/EEPROM.h +64 -0
  9. data/cpp/arduino/Godmode.cpp +7 -0
  10. data/cpp/arduino/Godmode.h +121 -15
  11. data/cpp/arduino/HardwareSerial.h +4 -4
  12. data/cpp/arduino/IPAddress.h +59 -0
  13. data/cpp/arduino/Print.h +9 -12
  14. data/cpp/arduino/Printable.h +8 -0
  15. data/cpp/arduino/SPI.h +11 -3
  16. data/cpp/arduino/Server.h +5 -0
  17. data/cpp/arduino/Udp.h +27 -0
  18. data/cpp/arduino/Wire.h +197 -77
  19. data/cpp/arduino/avr/io.h +10 -1
  20. data/cpp/arduino/avr/pgmspace.h +76 -46
  21. data/cpp/unittest/ArduinoUnitTests.h +32 -0
  22. data/cpp/unittest/Assertion.h +54 -26
  23. data/cpp/unittest/Compare.h +58 -51
  24. data/cpp/unittest/OstreamHelpers.h +4 -0
  25. data/exe/arduino_ci.rb +538 -0
  26. data/exe/arduino_ci_remote.rb +2 -393
  27. data/exe/arduino_library_location.rb +2 -2
  28. data/exe/ensure_arduino_installation.rb +7 -1
  29. data/lib/arduino_ci.rb +1 -0
  30. data/lib/arduino_ci/arduino_backend.rb +238 -0
  31. data/lib/arduino_ci/arduino_downloader.rb +43 -73
  32. data/lib/arduino_ci/arduino_downloader_linux.rb +17 -55
  33. data/lib/arduino_ci/arduino_downloader_osx.rb +21 -33
  34. data/lib/arduino_ci/arduino_downloader_windows.rb +11 -53
  35. data/lib/arduino_ci/arduino_installation.rb +18 -80
  36. data/lib/arduino_ci/ci_config.rb +8 -11
  37. data/lib/arduino_ci/cpp_library.rb +250 -59
  38. data/lib/arduino_ci/host.rb +59 -4
  39. data/lib/arduino_ci/library_properties.rb +101 -0
  40. data/lib/arduino_ci/version.rb +1 -1
  41. data/misc/default.yml +57 -6
  42. metadata +19 -87
  43. data/cpp/arduino/Arduino.h.orig +0 -143
  44. data/exe/libasan.rb +0 -29
  45. data/lib/arduino_ci/arduino_cmd.rb +0 -332
  46. data/lib/arduino_ci/arduino_cmd_linux.rb +0 -17
  47. data/lib/arduino_ci/arduino_cmd_linux_builder.rb +0 -19
  48. data/lib/arduino_ci/arduino_cmd_osx.rb +0 -17
  49. data/lib/arduino_ci/arduino_cmd_windows.rb +0 -17
@@ -0,0 +1,538 @@
1
+ #!/usr/bin/env ruby
2
+ require 'arduino_ci'
3
+ require 'set'
4
+ require 'pathname'
5
+ require 'optparse'
6
+
7
+ WIDTH = 80
8
+ VAR_CUSTOM_INIT_SCRIPT = "CUSTOM_INIT_SCRIPT".freeze
9
+ VAR_USE_SUBDIR = "USE_SUBDIR".freeze
10
+ VAR_EXPECT_EXAMPLES = "EXPECT_EXAMPLES".freeze
11
+ VAR_EXPECT_UNITTESTS = "EXPECT_UNITTESTS".freeze
12
+
13
+ @failure_count = 0
14
+ @passfail = proc { |result| result ? "✓" : "✗" }
15
+ @backend = nil
16
+
17
+ # Use some basic parsing to allow command-line overrides of config
18
+ class Parser
19
+ def self.parse(options)
20
+ unit_config = {}
21
+ output_options = {
22
+ skip_unittests: false,
23
+ skip_compilation: false,
24
+ ci_config: {
25
+ "unittest" => unit_config
26
+ },
27
+ }
28
+
29
+ opt_parser = OptionParser.new do |opts|
30
+ opts.banner = "Usage: #{File.basename(__FILE__)} [options]"
31
+
32
+ opts.on("--skip-unittests", "Don't run unit tests") do |p|
33
+ output_options[:skip_unittests] = p
34
+ end
35
+
36
+ opts.on("--skip-examples-compilation", "Don't compile example sketches") do |p|
37
+ output_options[:skip_compilation] = p
38
+ end
39
+
40
+ opts.on("--testfile-select=GLOB", "Unit test file (or glob) to select") do |p|
41
+ unit_config["testfiles"] ||= {}
42
+ unit_config["testfiles"]["select"] ||= []
43
+ unit_config["testfiles"]["select"] << p
44
+ end
45
+
46
+ opts.on("--testfile-reject=GLOB", "Unit test file (or glob) to reject") do |p|
47
+ unit_config["testfiles"] ||= {}
48
+ unit_config["testfiles"]["reject"] ||= []
49
+ unit_config["testfiles"]["reject"] << p
50
+ end
51
+
52
+ opts.on("-h", "--help", "Prints this help") do
53
+ puts opts
54
+ puts
55
+ puts "Additionally, the following environment variables control the script:"
56
+ puts " - #{VAR_CUSTOM_INIT_SCRIPT} - if set, this script will be run from the Arduino/libraries directory"
57
+ puts " prior to any automated library installation or testing (e.g. to install unoffical libraries)"
58
+ puts " - #{VAR_USE_SUBDIR} - if set, the script will install the library from this subdirectory of the cwd"
59
+ puts " - #{VAR_EXPECT_EXAMPLES} - if set, testing will fail if no example sketches are present"
60
+ puts " - #{VAR_EXPECT_UNITTESTS} - if set, testing will fail if no unit tests are present"
61
+ puts " - #{VAR_SKIP_LIBPROPS} - if set, testing will skip [experimental] library.properties validation"
62
+ exit
63
+ end
64
+ end
65
+
66
+ opt_parser.parse!(options)
67
+ output_options
68
+ end
69
+ end
70
+
71
+ # Read in command line options and make them read-only
72
+ @cli_options = (Parser.parse ARGV).freeze
73
+
74
+ # terminate after printing any debug info. TODO: capture debug info
75
+ def terminate(final = nil)
76
+ puts "Failures: #{@failure_count}"
77
+ unless @failure_count.zero? || final || @backend.nil?
78
+ puts "========== Last backend command (if relevant):"
79
+ puts @backend.last_msg.to_s
80
+ puts "========== Backend Stdout:"
81
+ puts @backend.last_out
82
+ puts "========== Backend Stderr:"
83
+ puts @backend.last_err
84
+ end
85
+ retcode = @failure_count.zero? ? 0 : 1
86
+ exit(retcode)
87
+ end
88
+
89
+ # make a nice status line for an action and react to the action
90
+ # TODO / note to self: inform_multline is tougher to write
91
+ # without altering the signature because it only leaves space
92
+ # for the checkmark _after_ the multiline, it doesn't know how
93
+ # to make that conditionally the body
94
+ # @param message String the text of the progress indicator
95
+ # @param multiline boolean whether multiline output is expected
96
+ # @param mark_fn block (string) -> string that says how to describe the result
97
+ # @param on_fail_msg String custom message for failure
98
+ # @param tally_on_fail boolean whether to increment @failure_count
99
+ # @param abort_on_fail boolean whether to abort immediately on failure (i.e. if this is a fatal error)
100
+ def perform_action(message, multiline, mark_fn, on_fail_msg, tally_on_fail, abort_on_fail)
101
+ line = "#{message}... "
102
+ endline = "...#{message} "
103
+ if multiline
104
+ puts line
105
+ else
106
+ print line
107
+ end
108
+ $stdout.flush
109
+ result = yield
110
+ mark = mark_fn.nil? ? "" : mark_fn.call(result)
111
+ # if multline, put checkmark at full width
112
+ print endline if multiline
113
+ puts mark.to_s.rjust(WIDTH - line.length, " ")
114
+ unless result
115
+ puts on_fail_msg unless on_fail_msg.nil?
116
+ @failure_count += 1 if tally_on_fail
117
+ # print out error messaging here if we've captured it
118
+ terminate if abort_on_fail
119
+ end
120
+ result
121
+ end
122
+
123
+ # Make a nice status for something that defers any failure code until script exit
124
+ def attempt(message, &block)
125
+ perform_action(message, false, @passfail, nil, true, false, &block)
126
+ end
127
+
128
+ # Make a nice status for something that defers any failure code until script exit
129
+ def attempt_multiline(message, &block)
130
+ perform_action(message, true, @passfail, nil, true, false, &block)
131
+ end
132
+
133
+ # Make a nice status for something that kills the script immediately on failure
134
+ FAILED_ASSURANCE_MESSAGE = "This may indicate a problem with your configuration; halting here".freeze
135
+ def assure(message, &block)
136
+ perform_action(message, false, @passfail, FAILED_ASSURANCE_MESSAGE, true, true, &block)
137
+ end
138
+
139
+ def assure_multiline(message, &block)
140
+ perform_action(message, true, @passfail, FAILED_ASSURANCE_MESSAGE, true, true, &block)
141
+ end
142
+
143
+ def inform(message, &block)
144
+ perform_action(message, false, proc { |x| x }, nil, false, false, &block)
145
+ end
146
+
147
+ def inform_multiline(message, &block)
148
+ perform_action(message, true, nil, nil, false, false, &block)
149
+ end
150
+
151
+ def rule(char)
152
+ puts char[0] * WIDTH
153
+ end
154
+
155
+ def warn(message)
156
+ inform("WARNING") { message }
157
+ end
158
+
159
+ def phase(name)
160
+ puts
161
+ rule("=")
162
+ inform("Beginning the next phase of testing") { name }
163
+ end
164
+
165
+ def banner
166
+ art = [
167
+ " . __ ___",
168
+ " _, ,_ _| , . * ._ _ / ` | ",
169
+ "(_| [ `(_] (_| | [ ) (_) \\__. _|_ v#{ArduinoCI::VERSION}",
170
+ ]
171
+
172
+ pad = " " * ((WIDTH - art[2].length) / 2)
173
+ art.each { |l| puts "#{pad}#{l}" }
174
+ puts
175
+ end
176
+
177
+ # Assure that a platform exists and return its definition
178
+ def assured_platform(purpose, name, config)
179
+ platform_definition = config.platform_definition(name)
180
+ assure("Requested #{purpose} platform '#{name}' is defined in 'platforms' YML") { !platform_definition.nil? }
181
+ platform_definition
182
+ end
183
+
184
+ # Return true if the file (or one of the dirs containing it) is hidden
185
+ def file_is_hidden_somewhere?(path)
186
+ # this is clunkly but pre-2.2-ish ruby doesn't return ascend as an enumerator
187
+ path.ascend do |part|
188
+ return true if part.basename.to_s.start_with? "."
189
+ end
190
+ false
191
+ end
192
+
193
+ # print out some files
194
+ def display_files(pathname)
195
+ # `find` doesn't follow symlinks, so we should instead
196
+ realpath = ArduinoCI::Host.symlink?(pathname) ? ArduinoCI::Host.readlink(pathname) : pathname
197
+
198
+ # suppress directories and dotfile-based things
199
+ all_files = realpath.find.select(&:file?)
200
+ non_hidden = all_files.reject { |path| file_is_hidden_somewhere?(path) }
201
+
202
+ # print files with an indent
203
+ puts " Files (excluding hidden files): #{non_hidden.size}"
204
+ non_hidden.each { |p| puts " #{p}" }
205
+ end
206
+
207
+ # @return [Array<String>] The list of installed libraries
208
+ def install_arduino_library_dependencies(library_names, on_behalf_of, already_installed = [])
209
+ installed = already_installed.clone
210
+ (library_names.map { |n| @backend.library_of_name(n) } - installed).each do |l|
211
+ if l.installed?
212
+ inform("Using pre-existing dependency of #{on_behalf_of}") { l.name }
213
+ else
214
+ assure("Installing dependency of #{on_behalf_of}: '#{l.name}'") do
215
+ next nil unless l.install
216
+
217
+ l.name
218
+ end
219
+ end
220
+ installed << l.name
221
+ installed += install_arduino_library_dependencies(l.arduino_library_dependencies, l.name, installed)
222
+ end
223
+ installed
224
+ end
225
+
226
+ # @param platforms [Array<String>] list of platforms to consider
227
+ # @param specific_config [CIConfig] configuration to use
228
+ def install_all_packages(platforms, specific_config)
229
+
230
+ # get packages from platforms
231
+ all_packages = specific_config.platform_info.select { |p, _| platforms.include?(p) }.values.map { |v| v[:package] }.compact.uniq
232
+
233
+ all_packages.each do |pkg|
234
+ next if @backend.boards_installed?(pkg)
235
+
236
+ url = assure("Board package #{pkg} has a defined URL") { specific_config.package_url(pkg) }
237
+ @backend.board_manager_urls = [url]
238
+ assure("Installing board package #{pkg}") { @backend.install_boards(pkg) }
239
+ end
240
+ end
241
+
242
+ # @param expectation_envvar [String] the name of the env var to check
243
+ # @param operation [String] a description of what operation we might be skipping
244
+ # @param filegroup_name [String] a description of the set of files without which we effectively skip the operation
245
+ # @param dir_description [String] a description of the directory where we looked for the files
246
+ # @param dir [Pathname] the directory where we looked for the files
247
+ def handle_expectation_of_files(expectation_envvar, operation, filegroup_name, dir_description, dir_path)
248
+ # alert future me about running the script from the wrong directory, instead of doing the huge file dump
249
+ # otherwise, assume that the user might be running the script on a library with no actual unit tests
250
+ if Pathname.new(__dir__).parent == Pathname.new(Dir.pwd)
251
+ inform_multiline("arduino_ci seems to be trying to test itself") do
252
+ [
253
+ "arduino_ci (the ruby gem) isn't an arduino project itself, so running the CI test script against",
254
+ "the core library isn't really a valid thing to do... but it's easy for a developer (including the",
255
+ "owner) to mistakenly do just that. Hello future me, you probably meant to run this against one of",
256
+ "the sample projects in SampleProjects/ ... if not, please submit a bug report; what a wild case!"
257
+ ].each { |l| puts " #{l}" }
258
+ false
259
+ end
260
+ exit(1)
261
+ end
262
+
263
+ # either the directory is empty, or it doesn't exist at all. message accordingly.
264
+ (problem, dir_desc, dir) = if dir_path.exist?
265
+ ["No #{filegroup_name} were found in", dir_description, dir_path]
266
+ else
267
+ ["No #{dir_description} at", "base directory", dir_path.parent]
268
+ end
269
+
270
+ inform(problem) { dir_path }
271
+ explain_and_exercise_envvar(expectation_envvar, operation, "contents of #{dir_desc}") { display_files(dir) }
272
+ end
273
+
274
+ # @param expectation_envvar [String] the name of the env var to check
275
+ # @param operation [String] a description of what operation we might be skipping
276
+ # @param block_desc [String] a description of what information will be dumped to assist the user
277
+ # @param block [Proc] a function that dumps information
278
+ def explain_and_exercise_envvar(expectation_envvar, operation, block_desc, &block)
279
+ inform("Environment variable #{expectation_envvar} is") { "(#{ENV[expectation_envvar].class}) #{ENV[expectation_envvar]}" }
280
+ if ENV[expectation_envvar].nil?
281
+ inform_multiline("Skipping #{operation}") do
282
+ puts " In case that's an error, displaying #{block_desc}:"
283
+ block.call
284
+ puts " To force an error in this case, set the environment variable #{expectation_envvar}"
285
+ true
286
+ end
287
+ else
288
+ assure_multiline("Displaying #{block_desc} before exit") do
289
+ block.call
290
+ false
291
+ end
292
+ end
293
+ end
294
+
295
+ # report and return the set of compilers
296
+ def get_annotated_compilers(config, cpp_library)
297
+ # check GCC
298
+ compilers = config.compilers_to_use
299
+ assure("The set of compilers (#{compilers.length}) isn't empty") { !compilers.empty? }
300
+ compilers.each do |gcc_binary|
301
+ attempt_multiline("Checking #{gcc_binary} version") do
302
+ version = cpp_library.gcc_version(gcc_binary)
303
+ next nil unless version
304
+
305
+ puts version.split("\n").map { |l| " #{l}" }.join("\n")
306
+ version
307
+ end
308
+ inform("libasan availability for #{gcc_binary}") { cpp_library.libasan?(gcc_binary) }
309
+ end
310
+ compilers
311
+ end
312
+
313
+ # Handle existence or nonexistence of custom initialization script -- run it if you have it
314
+ #
315
+ # This feature is to drive GitHub actions / docker image installation where the container is
316
+ # in a clean-slate state but needs some way to have custom library versions injected into it.
317
+ # In this case, the user provided script would fetch a git repo or some other method
318
+ def perform_custom_initialization(_config)
319
+ script_path = ENV[VAR_CUSTOM_INIT_SCRIPT]
320
+ inform("Environment variable #{VAR_CUSTOM_INIT_SCRIPT}") { "'#{script_path}'" }
321
+ return if script_path.nil?
322
+ return if script_path.empty?
323
+
324
+ script_pathname = Pathname.getwd + script_path
325
+ assure("Script at #{VAR_CUSTOM_INIT_SCRIPT} exists") { script_pathname.exist? }
326
+
327
+ assure_multiline("Running #{script_pathname} with sh in libraries working dir") do
328
+ Dir.chdir(@backend.lib_dir) do
329
+ IO.popen(["/bin/sh", script_pathname.to_s], err: [:child, :out]) do |io|
330
+ io.each_line { |line| puts " #{line}" }
331
+ end
332
+ end
333
+ end
334
+ end
335
+
336
+ # Auto-select some platforms to test based on the information available
337
+ #
338
+ # Top choice is always library.properties -- otherwise use the default.
339
+ # But filter that through any non-default config
340
+ #
341
+ # @param config [CIConfig] the overridden config object
342
+ # @param reason [String] description of why we might use this platform (i.e. unittest or compilation)
343
+ # @param desired_platforms [Array<String>] the platform names specified
344
+ # @param library_properties [Hash] the library properties defined by the library
345
+ # @return [Array<String>] platforms to use
346
+ def choose_platform_set(config, reason, desired_platforms, library_properties)
347
+
348
+ # if there are no properties or no architectures, defer entirely to desired platforms
349
+ if library_properties.nil? || library_properties.architectures.nil? || library_properties.architectures.empty?
350
+ # verify that all platforms exist
351
+ desired_platforms.each { |p| assured_platform(reason, p, config) }
352
+ return inform_multiline("No architectures listed in library.properties, using configured platforms") do
353
+ desired_platforms.each { |p| puts " #{p}" } # this returns desired_platforms
354
+ end
355
+ end
356
+
357
+ if library_properties.architectures.include?("*")
358
+ return inform_multiline("Wildcard architecture in library.properties, using configured platforms") do
359
+ desired_platforms.each { |p| puts " #{p}" } # this returns desired_platforms
360
+ end
361
+ end
362
+
363
+ platform_architecture = config.platform_info.transform_values { |v| v[:board].split(":")[1] }
364
+ supported_platforms = platform_architecture.select { |_, a| library_properties.architectures.include?(a) }
365
+
366
+ if config.is_default
367
+ # completely ignore default config, opting for brute-force library matches
368
+ # OTOH, we don't need to assure platforms because we defined them
369
+ return inform_multiline("Default config, platforms matching architectures in library.properties") do
370
+ supported_platforms.keys.each do |p| # rubocop:disable Style/HashEachMethods
371
+ puts " #{p}"
372
+ end # this returns supported_platforms
373
+ end
374
+ end
375
+
376
+ desired_supported_platforms = supported_platforms.select { |p, _| desired_platforms.include?(p) }.keys
377
+ desired_supported_platforms.each { |p| assured_platform(reason, p, config) }
378
+ inform_multiline("Configured platforms that match architectures in library.properties") do
379
+ desired_supported_platforms.each do |p|
380
+ puts " #{p}"
381
+ end # this returns supported_platforms
382
+ end
383
+ end
384
+
385
+ # Unit test procedure
386
+ def perform_unit_tests(cpp_library, file_config)
387
+ phase("Unit testing")
388
+ if @cli_options[:skip_unittests]
389
+ inform("Skipping unit tests") { "as requested via command line" }
390
+ return
391
+ end
392
+
393
+ config = file_config.with_override_config(@cli_options[:ci_config])
394
+ compilers = get_annotated_compilers(config, cpp_library)
395
+
396
+ inform("Library conforms to Arduino library specification") { cpp_library.one_point_five? ? "1.5" : "1.0" }
397
+
398
+ # Handle lack of test files
399
+ if cpp_library.test_files.empty?
400
+ handle_expectation_of_files(VAR_EXPECT_UNITTESTS, "unit tests", "test files", "tests directory", cpp_library.tests_dir)
401
+ return
402
+ end
403
+
404
+ # Get platforms, handle lack of them
405
+ platforms = choose_platform_set(config, "unittest", config.platforms_to_unittest, cpp_library.library_properties)
406
+ if platforms.empty?
407
+ explain_and_exercise_envvar(VAR_EXPECT_UNITTESTS, "unit tests", "platforms and architectures") do
408
+ puts " Configured platforms: #{config.platforms_to_unittest}"
409
+ puts " Configuration is default: #{config.is_default}"
410
+ arches = cpp_library.library_properties.nil? ? nil : cpp_library.library_properties.architectures
411
+ puts " Architectures in library.properties: #{arches}"
412
+ end
413
+ end
414
+
415
+ install_arduino_library_dependencies(config.aux_libraries_for_unittest, "<unittest/libraries>")
416
+
417
+ platforms.each do |p|
418
+ puts
419
+ config.allowable_unittest_files(cpp_library.test_files).each do |unittest_path|
420
+ unittest_name = unittest_path.basename.to_s
421
+ compilers.each do |gcc_binary|
422
+ attempt_multiline("Unit testing #{unittest_name} with #{gcc_binary} for #{p}") do
423
+ exe = cpp_library.build_for_test_with_configuration(
424
+ unittest_path,
425
+ config.aux_libraries_for_unittest,
426
+ gcc_binary,
427
+ config.gcc_config(p)
428
+ )
429
+ puts
430
+ unless exe
431
+ puts "Last command: #{cpp_library.last_cmd}"
432
+ puts cpp_library.last_out
433
+ puts cpp_library.last_err
434
+ next false
435
+ end
436
+ cpp_library.run_test_file(exe)
437
+ end
438
+ end
439
+ end
440
+ end
441
+ end
442
+
443
+ def perform_example_compilation_tests(cpp_library, config)
444
+ phase("Compilation of example sketches")
445
+ if @cli_options[:skip_compilation]
446
+ inform("Skipping compilation of examples") { "as requested via command line" }
447
+ return
448
+ end
449
+
450
+ library_examples = cpp_library.example_sketches
451
+
452
+ if library_examples.empty?
453
+ handle_expectation_of_files(VAR_EXPECT_EXAMPLES, "builds", "examples", "the examples directory", cpp_library.examples_dir)
454
+ return
455
+ end
456
+
457
+ library_examples.each do |example_path|
458
+ example_name = File.basename(example_path)
459
+ puts
460
+ inform("Discovered example sketch") { example_name }
461
+
462
+ ovr_config = config.from_example(example_path)
463
+ platforms = choose_platform_set(ovr_config, "library example", ovr_config.platforms_to_build, cpp_library.library_properties)
464
+
465
+ if platforms.empty?
466
+ explain_and_exercise_envvar(VAR_EXPECT_EXAMPLES, "examples compilation", "platforms and architectures") do
467
+ puts " Configured platforms: #{ovr_config.platforms_to_build}"
468
+ puts " Configuration is default: #{ovr_config.is_default}"
469
+ arches = cpp_library.library_properties.nil? ? nil : cpp_library.library_properties.architectures
470
+ puts " Architectures in library.properties: #{arches}"
471
+ end
472
+ end
473
+
474
+ install_all_packages(platforms, ovr_config)
475
+ install_arduino_library_dependencies(ovr_config.aux_libraries_for_build, "<compile/libraries>")
476
+
477
+ platforms.each do |p|
478
+ board = ovr_config.platform_info[p][:board]
479
+ attempt("Compiling #{example_name} for #{board}") do
480
+ ret = @backend.compile_sketch(example_path, board)
481
+ unless ret
482
+ puts
483
+ puts "Last command: #{@backend.last_msg}"
484
+ puts @backend.last_err
485
+ end
486
+ ret
487
+ end
488
+ end
489
+ end
490
+ end
491
+
492
+ banner
493
+ inform("Host OS") { ArduinoCI::Host.os }
494
+
495
+ # initialize command and config
496
+ config = ArduinoCI::CIConfig.default.from_project_library
497
+ @backend = ArduinoCI::ArduinoInstallation.autolocate!
498
+ inform("Located arduino-cli binary") { @backend.binary_path.to_s }
499
+ if @backend.lib_dir.exist?
500
+ inform("Found libraries directory") { @backend.lib_dir }
501
+ else
502
+ assure("Creating libraries directory") { @backend.lib_dir.mkpath || true }
503
+ end
504
+
505
+ # run any library init scripts from the library itself.
506
+ perform_custom_initialization(config)
507
+
508
+ # initialize library under test
509
+ inform("Environment variable #{VAR_USE_SUBDIR}") { "'#{ENV[VAR_USE_SUBDIR]}'" }
510
+ cpp_library_path = Pathname.new(ENV[VAR_USE_SUBDIR].nil? ? "." : ENV[VAR_USE_SUBDIR])
511
+ cpp_library = assure("Installing library under test") do
512
+ @backend.install_local_library(cpp_library_path)
513
+ end
514
+
515
+ # Warn if the library name isn't obvious
516
+ assumed_name = @backend.name_of_library(cpp_library_path)
517
+ ondisk_name = cpp_library_path.realpath.basename.to_s
518
+ warn("Installed library named '#{assumed_name}' has directory name '#{ondisk_name}'") if assumed_name != ondisk_name
519
+
520
+ if !cpp_library.nil?
521
+ inform("Library installed at") { cpp_library.path.to_s }
522
+ else
523
+ # this is a longwinded way of failing, we aren't really "assuring" anything at this point
524
+ assure_multiline("Library installed successfully") do
525
+ puts @backend.last_msg
526
+ false
527
+ end
528
+ end
529
+
530
+ install_arduino_library_dependencies(
531
+ cpp_library.arduino_library_dependencies,
532
+ "<#{ArduinoCI::CppLibrary::LIBRARY_PROPERTIES_FILE}>"
533
+ )
534
+
535
+ perform_unit_tests(cpp_library, config)
536
+ perform_example_compilation_tests(cpp_library, config)
537
+
538
+ terminate(true)