arduino_ci 0.2.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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)