arduino_ci 0.2.0 → 1.1.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.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +49 -20
  3. data/REFERENCE.md +636 -0
  4. data/cpp/arduino/Arduino.h +1 -1
  5. data/cpp/arduino/AvrMath.h +117 -17
  6. data/cpp/arduino/Client.h +27 -0
  7. data/cpp/arduino/EEPROM.h +64 -0
  8. data/cpp/arduino/Godmode.cpp +11 -0
  9. data/cpp/arduino/Godmode.h +58 -9
  10. data/cpp/arduino/HardwareSerial.h +9 -28
  11. data/cpp/arduino/IPAddress.h +59 -0
  12. data/cpp/arduino/Print.h +9 -12
  13. data/cpp/arduino/Printable.h +8 -0
  14. data/cpp/arduino/SPI.h +11 -3
  15. data/cpp/arduino/Server.h +5 -0
  16. data/cpp/arduino/Udp.h +27 -0
  17. data/cpp/arduino/Wire.h +234 -0
  18. data/cpp/arduino/avr/io.h +10 -1
  19. data/cpp/arduino/avr/pgmspace.h +76 -46
  20. data/cpp/arduino/ci/StreamTape.h +36 -0
  21. data/cpp/unittest/OstreamHelpers.h +4 -0
  22. data/exe/arduino_ci.rb +427 -0
  23. data/exe/arduino_ci_remote.rb +2 -385
  24. data/exe/arduino_library_location.rb +2 -2
  25. data/exe/ensure_arduino_installation.rb +7 -1
  26. data/lib/arduino_ci.rb +1 -0
  27. data/lib/arduino_ci/arduino_backend.rb +222 -0
  28. data/lib/arduino_ci/arduino_downloader.rb +43 -73
  29. data/lib/arduino_ci/arduino_downloader_linux.rb +17 -55
  30. data/lib/arduino_ci/arduino_downloader_osx.rb +21 -33
  31. data/lib/arduino_ci/arduino_downloader_windows.rb +11 -53
  32. data/lib/arduino_ci/arduino_installation.rb +18 -80
  33. data/lib/arduino_ci/ci_config.rb +15 -9
  34. data/lib/arduino_ci/cpp_library.rb +266 -48
  35. data/lib/arduino_ci/host.rb +59 -4
  36. data/lib/arduino_ci/library_properties.rb +96 -0
  37. data/lib/arduino_ci/version.rb +1 -1
  38. data/misc/default.yml +55 -4
  39. metadata +21 -87
  40. data/cpp/arduino/Arduino.h.orig +0 -143
  41. data/exe/libasan.rb +0 -29
  42. data/lib/arduino_ci/arduino_cmd.rb +0 -328
  43. data/lib/arduino_ci/arduino_cmd_linux.rb +0 -17
  44. data/lib/arduino_ci/arduino_cmd_linux_builder.rb +0 -19
  45. data/lib/arduino_ci/arduino_cmd_osx.rb +0 -17
  46. data/lib/arduino_ci/arduino_cmd_windows.rb +0 -17
@@ -0,0 +1,36 @@
1
+ #pragma once
2
+
3
+ #include "../Stream.h"
4
+
5
+ /**
6
+ * Stream with godmode-controlled input and godmode-persisted output
7
+ */
8
+ class StreamTape : public Stream, public ObservableDataStream
9
+ {
10
+ protected:
11
+ String* mGodmodeDataOut;
12
+ // mGodmodeDataIn is provided by Stream
13
+
14
+ public:
15
+ StreamTape(String* dataIn, String* dataOut, unsigned long* delay): Stream(), ObservableDataStream() {
16
+ mGodmodeDataIn = dataIn;
17
+ mGodmodeDataOut = dataOut;
18
+ mGodmodeMicrosDelay = delay;
19
+ }
20
+
21
+ // virtual int available(void);
22
+ // virtual int peek(void);
23
+ // virtual int read(void);
24
+ // virtual int availableForWrite(void);
25
+ // virtual void flush(void);
26
+ virtual size_t write(uint8_t aChar) {
27
+ mGodmodeDataOut->append(String((char)aChar));
28
+ advertiseByte((unsigned char)aChar);
29
+ return 1;
30
+ }
31
+
32
+ // https://stackoverflow.com/a/4271276
33
+ using Print::write; // pull in write(str) and write(buf, size) from Print
34
+
35
+ };
36
+
@@ -2,4 +2,8 @@
2
2
 
3
3
  #include <ostream>
4
4
 
5
+ #if (defined __apple_build_version__) && (__apple_build_version__ >= 12000000)
6
+ // defined in /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../include/c++/v1/ostream:223:20
7
+ #else
5
8
  inline std::ostream& operator << (std::ostream& out, const std::nullptr_t &np) { return out << "nullptr"; }
9
+ #endif
@@ -0,0 +1,427 @@
1
+ #!/usr/bin/env ruby
2
+ require 'arduino_ci'
3
+ require 'set'
4
+ require 'pathname'
5
+ require 'optparse'
6
+
7
+ WIDTH = 80
8
+ VAR_EXPECT_EXAMPLES = "EXPECT_EXAMPLES".freeze
9
+ VAR_EXPECT_UNITTESTS = "EXPECT_UNITTESTS".freeze
10
+
11
+ @failure_count = 0
12
+ @passfail = proc { |result| result ? "✓" : "✗" }
13
+ @backend = nil
14
+
15
+ # Use some basic parsing to allow command-line overrides of config
16
+ class Parser
17
+ def self.parse(options)
18
+ unit_config = {}
19
+ output_options = {
20
+ skip_unittests: false,
21
+ skip_compilation: false,
22
+ ci_config: {
23
+ "unittest" => unit_config
24
+ },
25
+ }
26
+
27
+ opt_parser = OptionParser.new do |opts|
28
+ opts.banner = "Usage: #{File.basename(__FILE__)} [options]"
29
+
30
+ opts.on("--skip-unittests", "Don't run unit tests") do |p|
31
+ output_options[:skip_unittests] = p
32
+ end
33
+
34
+ opts.on("--skip-examples-compilation", "Don't compile example sketches") do |p|
35
+ output_options[:skip_compilation] = p
36
+ end
37
+
38
+ opts.on("--testfile-select=GLOB", "Unit test file (or glob) to select") do |p|
39
+ unit_config["testfiles"] ||= {}
40
+ unit_config["testfiles"]["select"] ||= []
41
+ unit_config["testfiles"]["select"] << p
42
+ end
43
+
44
+ opts.on("--testfile-reject=GLOB", "Unit test file (or glob) to reject") do |p|
45
+ unit_config["testfiles"] ||= {}
46
+ unit_config["testfiles"]["reject"] ||= []
47
+ unit_config["testfiles"]["reject"] << p
48
+ end
49
+
50
+ opts.on("-h", "--help", "Prints this help") do
51
+ puts opts
52
+ puts
53
+ puts "Additionally, the following environment variables control the script:"
54
+ puts " - #{VAR_EXPECT_EXAMPLES} - if set, testing will fail if no example sketches are present"
55
+ puts " - #{VAR_EXPECT_UNITTESTS} - if set, testing will fail if no unit tests are present"
56
+ exit
57
+ end
58
+ end
59
+
60
+ opt_parser.parse!(options)
61
+ output_options
62
+ end
63
+ end
64
+
65
+ # Read in command line options and make them read-only
66
+ @cli_options = (Parser.parse ARGV).freeze
67
+
68
+ # terminate after printing any debug info. TODO: capture debug info
69
+ def terminate(final = nil)
70
+ puts "Failures: #{@failure_count}"
71
+ unless @failure_count.zero? || final
72
+ puts "Last message: #{@backend.last_msg}"
73
+ puts "========== Stdout:"
74
+ puts @backend.last_out
75
+ puts "========== Stderr:"
76
+ puts @backend.last_err
77
+ end
78
+ retcode = @failure_count.zero? ? 0 : 1
79
+ exit(retcode)
80
+ end
81
+
82
+ # make a nice status line for an action and react to the action
83
+ # TODO / note to self: inform_multline is tougher to write
84
+ # without altering the signature because it only leaves space
85
+ # for the checkmark _after_ the multiline, it doesn't know how
86
+ # to make that conditionally the body
87
+ # @param message String the text of the progress indicator
88
+ # @param multiline boolean whether multiline output is expected
89
+ # @param mark_fn block (string) -> string that says how to describe the result
90
+ # @param on_fail_msg String custom message for failure
91
+ # @param tally_on_fail boolean whether to increment @failure_count
92
+ # @param abort_on_fail boolean whether to abort immediately on failure (i.e. if this is a fatal error)
93
+ def perform_action(message, multiline, mark_fn, on_fail_msg, tally_on_fail, abort_on_fail)
94
+ line = "#{message}... "
95
+ endline = "...#{message} "
96
+ if multiline
97
+ puts line
98
+ else
99
+ print line
100
+ end
101
+ $stdout.flush
102
+ result = yield
103
+ mark = mark_fn.nil? ? "" : mark_fn.call(result)
104
+ # if multline, put checkmark at full width
105
+ print endline if multiline
106
+ puts mark.to_s.rjust(WIDTH - line.length, " ")
107
+ unless result
108
+ puts on_fail_msg unless on_fail_msg.nil?
109
+ @failure_count += 1 if tally_on_fail
110
+ # print out error messaging here if we've captured it
111
+ terminate if abort_on_fail
112
+ end
113
+ result
114
+ end
115
+
116
+ # Make a nice status for something that defers any failure code until script exit
117
+ def attempt(message, &block)
118
+ perform_action(message, false, @passfail, nil, true, false, &block)
119
+ end
120
+
121
+ # Make a nice status for something that defers any failure code until script exit
122
+ def attempt_multiline(message, &block)
123
+ perform_action(message, true, @passfail, nil, true, false, &block)
124
+ end
125
+
126
+ # Make a nice status for something that kills the script immediately on failure
127
+ FAILED_ASSURANCE_MESSAGE = "This may indicate a problem with your configuration; halting here".freeze
128
+ def assure(message, &block)
129
+ perform_action(message, false, @passfail, FAILED_ASSURANCE_MESSAGE, true, true, &block)
130
+ end
131
+
132
+ def assure_multiline(message, &block)
133
+ perform_action(message, true, @passfail, FAILED_ASSURANCE_MESSAGE, true, true, &block)
134
+ end
135
+
136
+ def inform(message, &block)
137
+ perform_action(message, false, proc { |x| x }, nil, false, false, &block)
138
+ end
139
+
140
+ def inform_multiline(message, &block)
141
+ perform_action(message, true, nil, nil, false, false, &block)
142
+ end
143
+
144
+ # Assure that a platform exists and return its definition
145
+ def assured_platform(purpose, name, config)
146
+ platform_definition = config.platform_definition(name)
147
+ assure("Requested #{purpose} platform '#{name}' is defined in 'platforms' YML") { !platform_definition.nil? }
148
+ platform_definition
149
+ end
150
+
151
+ # Return true if the file (or one of the dirs containing it) is hidden
152
+ def file_is_hidden_somewhere?(path)
153
+ # this is clunkly but pre-2.2-ish ruby doesn't return ascend as an enumerator
154
+ path.ascend do |part|
155
+ return true if part.basename.to_s.start_with? "."
156
+ end
157
+ false
158
+ end
159
+
160
+ # print out some files
161
+ def display_files(pathname)
162
+ # `find` doesn't follow symlinks, so we should instead
163
+ realpath = ArduinoCI::Host.symlink?(pathname) ? ArduinoCI::Host.readlink(pathname) : pathname
164
+
165
+ # suppress directories and dotfile-based things
166
+ all_files = realpath.find.select(&:file?)
167
+ non_hidden = all_files.reject { |path| file_is_hidden_somewhere?(path) }
168
+
169
+ # print files with an indent
170
+ puts " Files (excluding hidden files): #{non_hidden.size}"
171
+ non_hidden.each { |p| puts " #{p}" }
172
+ end
173
+
174
+ # @return [Array<String>] The list of installed libraries
175
+ def install_arduino_library_dependencies(library_names, on_behalf_of, already_installed = [])
176
+ installed = already_installed.clone
177
+ (library_names.map { |n| @backend.library_of_name(n) } - installed).each do |l|
178
+ if l.installed?
179
+ inform("Using pre-existing dependency of #{on_behalf_of}") { l.name }
180
+ else
181
+ assure("Installing dependency of #{on_behalf_of}: '#{l.name}'") do
182
+ next nil unless l.install
183
+
184
+ l.name
185
+ end
186
+ end
187
+ installed << l.name
188
+ installed += install_arduino_library_dependencies(l.arduino_library_dependencies, l.name, installed)
189
+ end
190
+ installed
191
+ end
192
+
193
+ # @param example_platform_info [Hash] mapping of platform name to package information
194
+ # @param board_package_url [Hash] mapping of package name to URL
195
+ def install_all_packages(example_platform_info, board_package_url)
196
+ # with all platform info, we can extract unique packages and their urls
197
+ # do that, set the URLs, and download the packages
198
+ all_packages = example_platform_info.values.map { |v| v[:package] }.uniq.reject(&:nil?)
199
+
200
+ # make sure any non-builtin package has a URL defined
201
+ all_packages.each { |p| assure("Board package #{p} has a defined URL") { board_package_url[p] } }
202
+
203
+ # set up all the board manager URLs.
204
+ # we can safely reject nils now, they would be for the builtins
205
+ all_urls = all_packages.map { |p| board_package_url[p] }.uniq.reject(&:nil?)
206
+ unless all_urls.empty?
207
+ assure_multiline("Setting board manager URLs") do
208
+ @backend.board_manager_urls = all_urls
209
+ result = @backend.board_manager_urls
210
+ result.each { |u| puts " #{u}" }
211
+ (all_urls - result).empty? # check that all_urls is completely contained in the result
212
+ end
213
+ end
214
+ all_packages.each { |p| assure("Installing board package #{p}") { @backend.install_boards(p) } }
215
+ end
216
+
217
+ # @param expectation_envvar [String] the name of the env var to check
218
+ # @param operation [String] a description of what operation we might be skipping
219
+ # @param filegroup_name [String] a description of the set of files without which we effectively skip the operation
220
+ # @param dir_description [String] a description of the directory where we looked for the files
221
+ # @param dir [Pathname] the directory where we looked for the files
222
+ def handle_expectation_of_files(expectation_envvar, operation, filegroup_name, dir_description, dir_path)
223
+ # alert future me about running the script from the wrong directory, instead of doing the huge file dump
224
+ # otherwise, assume that the user might be running the script on a library with no actual unit tests
225
+ if Pathname.new(__dir__).parent == Pathname.new(Dir.pwd)
226
+ inform_multiline("arduino_ci seems to be trying to test itself") do
227
+ [
228
+ "arduino_ci (the ruby gem) isn't an arduino project itself, so running the CI test script against",
229
+ "the core library isn't really a valid thing to do... but it's easy for a developer (including the",
230
+ "owner) to mistakenly do just that. Hello future me, you probably meant to run this against one of",
231
+ "the sample projects in SampleProjects/ ... if not, please submit a bug report; what a wild case!"
232
+ ].each { |l| puts " #{l}" }
233
+ false
234
+ end
235
+ exit(1)
236
+ end
237
+
238
+ # either the directory is empty, or it doesn't exist at all. message accordingly.
239
+ (problem, dir_desc, dir) = if dir_path.exist?
240
+ ["No #{filegroup_name} were found in", dir_description, dir_path]
241
+ else
242
+ ["No #{dir_description} at", "base directory", dir_path.parent]
243
+ end
244
+
245
+ inform(problem) { dir_path }
246
+ inform("Environment variable #{expectation_envvar} is") { "(#{ENV[expectation_envvar].class}) #{ENV[expectation_envvar]}" }
247
+ if ENV[expectation_envvar].nil?
248
+ inform_multiline("Skipping #{operation}") do
249
+ puts " In case that's an error, this is what was found in the #{dir_desc}:"
250
+ display_files(dir)
251
+ puts " To force an error in this case, set the environment variable #{expectation_envvar}"
252
+ true
253
+ end
254
+ else
255
+ assure_multiline("Dumping project's #{dir_desc} before exit") do
256
+ display_files(dir)
257
+ false
258
+ end
259
+ end
260
+ end
261
+
262
+ # report and return the set of compilers
263
+ def get_annotated_compilers(config, cpp_library)
264
+ # check GCC
265
+ compilers = config.compilers_to_use
266
+ assure("The set of compilers (#{compilers.length}) isn't empty") { !compilers.empty? }
267
+ compilers.each do |gcc_binary|
268
+ attempt_multiline("Checking #{gcc_binary} version") do
269
+ version = cpp_library.gcc_version(gcc_binary)
270
+ next nil unless version
271
+
272
+ puts version.split("\n").map { |l| " #{l}" }.join("\n")
273
+ version
274
+ end
275
+ inform("libasan availability for #{gcc_binary}") { cpp_library.libasan?(gcc_binary) }
276
+ end
277
+ compilers
278
+ end
279
+
280
+ def perform_unit_tests(cpp_library, file_config)
281
+ if @cli_options[:skip_unittests]
282
+ inform("Skipping unit tests") { "as requested via command line" }
283
+ return
284
+ end
285
+
286
+ config = file_config.with_override_config(@cli_options[:ci_config])
287
+ compilers = get_annotated_compilers(config, cpp_library)
288
+ config.platforms_to_unittest.each_with_object({}) { |p, acc| acc[p] = assured_platform("unittest", p, config) }
289
+
290
+ inform("Library conforms to Arduino library specification") { cpp_library.one_point_five? ? "1.5" : "1.0" }
291
+
292
+ # Handle lack of test files
293
+ if cpp_library.test_files.empty?
294
+ handle_expectation_of_files(VAR_EXPECT_UNITTESTS, "unit tests", "test files", "tests directory", cpp_library.tests_dir)
295
+ return
296
+ end
297
+
298
+ # Handle lack of platforms
299
+ if config.platforms_to_unittest.empty?
300
+ inform("Skipping unit tests") { "no platforms were requested" }
301
+ return
302
+ end
303
+
304
+ install_arduino_library_dependencies(config.aux_libraries_for_unittest, "<unittest/libraries>")
305
+
306
+ config.platforms_to_unittest.each do |p|
307
+ config.allowable_unittest_files(cpp_library.test_files).each do |unittest_path|
308
+ unittest_name = unittest_path.basename.to_s
309
+ compilers.each do |gcc_binary|
310
+ attempt_multiline("Unit testing #{unittest_name} with #{gcc_binary} for #{p}") do
311
+ exe = cpp_library.build_for_test_with_configuration(
312
+ unittest_path,
313
+ config.aux_libraries_for_unittest,
314
+ gcc_binary,
315
+ config.gcc_config(p)
316
+ )
317
+ puts
318
+ unless exe
319
+ puts "Last command: #{cpp_library.last_cmd}"
320
+ puts cpp_library.last_out
321
+ puts cpp_library.last_err
322
+ next false
323
+ end
324
+ cpp_library.run_test_file(exe)
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end
330
+
331
+ def perform_example_compilation_tests(cpp_library, config)
332
+ if @cli_options[:skip_compilation]
333
+ inform("Skipping compilation of examples") { "as requested via command line" }
334
+ return
335
+ end
336
+
337
+ # gather up all required boards for compilation so we can install them up front.
338
+ # start with the "platforms to unittest" and add the examples
339
+ # while we're doing that, get the aux libraries as well
340
+ example_platform_info = {}
341
+ board_package_url = {}
342
+ aux_libraries = Set.new(config.aux_libraries_for_build)
343
+ # while collecting the platforms, ensure they're defined
344
+
345
+ library_examples = cpp_library.example_sketches
346
+ library_examples.each do |path|
347
+ ovr_config = config.from_example(path)
348
+ ovr_config.platforms_to_build.each do |platform|
349
+ # assure the platform if we haven't already
350
+ next if example_platform_info.key?(platform)
351
+
352
+ platform_info = assured_platform("library example", platform, config)
353
+ next if platform_info.nil?
354
+
355
+ example_platform_info[platform] = platform_info
356
+ package = platform_info[:package]
357
+ board_package_url[package] = ovr_config.package_url(package)
358
+ end
359
+ aux_libraries.merge(ovr_config.aux_libraries_for_build)
360
+ end
361
+
362
+ install_all_packages(example_platform_info, board_package_url)
363
+ install_arduino_library_dependencies(aux_libraries, "<compile/libraries>")
364
+
365
+ if config.platforms_to_build.empty?
366
+ inform("Skipping builds") { "no platforms were requested" }
367
+ return
368
+ elsif library_examples.empty?
369
+ handle_expectation_of_files(VAR_EXPECT_EXAMPLES, "builds", "examples", "the examples directory", cpp_library.examples_dir)
370
+ return
371
+ end
372
+
373
+ library_examples.each do |example_path|
374
+ ovr_config = config.from_example(example_path)
375
+ ovr_config.platforms_to_build.each do |p|
376
+ board = example_platform_info[p][:board]
377
+ example_name = File.basename(example_path)
378
+ attempt("Compiling #{example_name} for #{board}") do
379
+ ret = @backend.compile_sketch(example_path, board)
380
+ unless ret
381
+ puts
382
+ puts "Last command: #{@backend.last_msg}"
383
+ puts @backend.last_err
384
+ end
385
+ ret
386
+ end
387
+ end
388
+ end
389
+ end
390
+
391
+ # initialize command and config
392
+ config = ArduinoCI::CIConfig.default.from_project_library
393
+
394
+ @backend = ArduinoCI::ArduinoInstallation.autolocate!
395
+ inform("Located arduino-cli binary") { @backend.binary_path.to_s }
396
+
397
+ # initialize library under test
398
+ cpp_library_path = Pathname.new(".")
399
+ cpp_library = assure("Installing library under test") do
400
+ @backend.install_local_library(cpp_library_path)
401
+ end
402
+
403
+ assumed_name = @backend.name_of_library(cpp_library_path)
404
+ ondisk_name = cpp_library_path.realpath.basename
405
+ if assumed_name != ondisk_name
406
+ inform("WARNING") { "Installed library named '#{assumed_name}' has directory name '#{ondisk_name}'" }
407
+ end
408
+
409
+ if !cpp_library.nil?
410
+ inform("Library installed at") { cpp_library.path.to_s }
411
+ else
412
+ # this is a longwinded way of failing, we aren't really "assuring" anything at this point
413
+ assure_multiline("Library installed successfully") do
414
+ puts @backend.last_msg
415
+ false
416
+ end
417
+ end
418
+
419
+ install_arduino_library_dependencies(
420
+ cpp_library.arduino_library_dependencies,
421
+ "<#{ArduinoCI::CppLibrary::LIBRARY_PROPERTIES_FILE}>"
422
+ )
423
+
424
+ perform_unit_tests(cpp_library, config)
425
+ perform_example_compilation_tests(cpp_library, config)
426
+
427
+ terminate(true)