arduino_ci 0.3.0 → 1.3.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 (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
@@ -1,16 +1,10 @@
1
1
  require 'pathname'
2
2
  require "arduino_ci/host"
3
- require "arduino_ci/arduino_cmd_osx"
4
- require "arduino_ci/arduino_cmd_linux"
5
- require "arduino_ci/arduino_cmd_windows"
6
- require "arduino_ci/arduino_cmd_linux_builder"
3
+ require "arduino_ci/arduino_backend"
7
4
  require "arduino_ci/arduino_downloader_osx"
8
5
  require "arduino_ci/arduino_downloader_linux"
9
-
10
6
  require "arduino_ci/arduino_downloader_windows" if ArduinoCI::Host.os == :windows
11
7
 
12
- DESIRED_ARDUINO_IDE_VERSION = "1.8.6".freeze
13
-
14
8
  module ArduinoCI
15
9
 
16
10
  class ArduinoInstallationError < StandardError; end
@@ -18,85 +12,29 @@ module ArduinoCI
18
12
  # Manage the OS-specific install location of Arduino
19
13
  class ArduinoInstallation
20
14
 
15
+ DESIRED_ARDUINO_CLI_VERSION = "0.13.0".freeze
16
+
21
17
  class << self
22
18
 
23
19
  # attempt to find a workable Arduino executable across platforms
24
20
  #
25
21
  # Autolocation assumed to be an expensive operation
26
- # @return [ArduinoCI::ArduinoCmd] an instance of the command or nil if it can't be found
22
+ # @return [ArduinoCI::ArduinoBackend] an instance of the command or nil if it can't be found
27
23
  def autolocate
28
- ret = nil
29
- case Host.os
30
- when :osx then
31
- ret = autolocate_osx
32
- when :linux then
33
- loc = ArduinoDownloaderLinux.autolocated_executable
34
- return nil if loc.nil?
35
-
36
- ret = ArduinoCmdLinux.new
37
- ret.base_cmd = [loc]
38
- ret.binary_path = Pathname.new(loc)
39
- when :windows then
40
- loc = ArduinoDownloaderWindows.autolocated_executable
41
- return nil if loc.nil?
42
-
43
- ret = ArduinoCmdWindows.new
44
- ret.base_cmd = [loc]
45
- ret.binary_path = Pathname.new(loc)
24
+ downloader_class = case Host.os
25
+ when :osx then ArduinoDownloaderOSX
26
+ when :linux then ArduinoDownloaderLinux
27
+ when :windows then ArduinoDownloaderWindows
46
28
  end
47
- ret
48
- end
49
29
 
50
- # @return [ArduinoCI::ArduinoCmdOSX] an instance of the command or nil if it can't be found
51
- def autolocate_osx
52
- osx_root = ArduinoDownloaderOSX.autolocated_installation
53
- return nil if osx_root.nil?
54
- return nil unless File.exist? osx_root
30
+ loc = downloader_class.autolocated_executable
31
+ return nil if loc.nil?
55
32
 
56
- launchers = [
57
- # try a hack that skips splash screen
58
- # from https://github.com/arduino/Arduino/issues/1970#issuecomment-321975809
59
- [
60
- "java",
61
- "-cp",
62
- "#{osx_root}/Contents/Java/*",
63
- "-DAPP_DIR=#{osx_root}/Contents/Java",
64
- "-Dfile.encoding=UTF-8",
65
- "-Dapple.awt.UIElement=true",
66
- "-Xms128M",
67
- "-Xmx512M",
68
- "processing.app.Base",
69
- ],
70
- # failsafe way
71
- [File.join(osx_root, "Contents", "MacOS", "Arduino")]
72
- ]
73
-
74
- # create return and find a command launcher that works
75
- ret = ArduinoCmdOSX.new
76
- launchers.each do |launcher|
77
- # test whether this method successfully launches the IDE
78
- # note that "successful launch" involves a command that will fail,
79
- # because that's faster than any command which succeeds. what we
80
- # don't want to see is a java error.
81
- args = launcher + ["--bogus-option"]
82
- result = Host.run_and_capture(*args)
83
-
84
- # NOTE: Was originally searching for "Error: unknown option: --bogus-option"
85
- # but also need to find "Erreur: option inconnue : --bogus-option"
86
- # and who knows how many other languages.
87
- # For now, just search for the end of the error and hope that the java-style
88
- # launch of this won't include a similar string in it
89
- next unless result[:err].include? ": --bogus-option"
90
-
91
- ret.base_cmd = launcher
92
- ret.binary_path = Pathname.new(osx_root)
93
- return ret
94
- end
95
- nil
33
+ ArduinoBackend.new(loc)
96
34
  end
97
35
 
98
36
  # Attempt to find a workable Arduino executable across platforms, and install it if we don't
99
- # @return [ArduinoCI::ArduinoCmd] an instance of a command
37
+ # @return [ArduinoCI::ArduinoBackend] an instance of a command
100
38
  def autolocate!(output = $stdout)
101
39
  candidate = autolocate
102
40
  return candidate unless candidate.nil?
@@ -109,12 +47,12 @@ module ArduinoCI
109
47
 
110
48
  # Forcibly install Arduino from the web
111
49
  # @return [bool] Whether the command succeeded
112
- def force_install(output = $stdout, version = DESIRED_ARDUINO_IDE_VERSION)
113
- worker_class = case Host.os
114
- when :osx then ArduinoDownloaderOSX
115
- when :windows then ArduinoDownloaderWindows
116
- when :linux then ArduinoDownloaderLinux
117
- end
50
+ def force_install(output = $stdout, version = DESIRED_ARDUINO_CLI_VERSION)
51
+ worker_class = case Host.os
52
+ when :osx then ArduinoDownloaderOSX
53
+ when :windows then ArduinoDownloaderWindows
54
+ when :linux then ArduinoDownloaderLinux
55
+ end
118
56
  worker = worker_class.new(version, output)
119
57
  worker.execute
120
58
  end
@@ -57,19 +57,23 @@ module ArduinoCI
57
57
  # @return [ArudinoCI::CIConfig] The configuration with defaults filled in
58
58
  def default
59
59
  ret = new
60
+ ret.instance_variable_set("@is_default", true)
60
61
  ret.load_yaml(File.expand_path("../../misc/default.yml", __dir__))
61
62
  ret
62
63
  end
63
64
  end
64
65
 
66
+ attr_reader :is_default
65
67
  attr_accessor :package_info
66
68
  attr_accessor :platform_info
67
69
  attr_accessor :compile_info
68
70
  attr_accessor :unittest_info
71
+
69
72
  def initialize
70
- @package_info = {}
73
+ @is_default = false
74
+ @package_info = {}
71
75
  @platform_info = {}
72
- @compile_info = {}
76
+ @compile_info = {}
73
77
  @unittest_info = {}
74
78
  end
75
79
 
@@ -107,7 +111,7 @@ module ArduinoCI
107
111
  good_data = {}
108
112
  source.each do |key, value|
109
113
  ksym = key.to_sym
110
- expected_type = schema[ksym].class == Class ? schema[ksym] : Hash
114
+ expected_type = schema[ksym].instance_of?(Class) ? schema[ksym] : Hash
111
115
  if !schema.include?(ksym)
112
116
  puts "Warning: unknown field '#{ksym}' under definition for #{rootname}"
113
117
  elsif value.nil?
@@ -115,7 +119,7 @@ module ArduinoCI
115
119
  elsif value.class != expected_type
116
120
  puts "Warning: expected field '#{ksym}' of #{rootname} to be '#{expected_type}', got '#{value.class}'"
117
121
  else
118
- good_data[ksym] = value.class == Hash ? validate_data(key, value, schema[ksym]) : value
122
+ good_data[ksym] = value.instance_of?(Hash) ? validate_data(key, value, schema[ksym]) : value
119
123
  end
120
124
  end
121
125
  good_data
@@ -232,13 +236,6 @@ module ArduinoCI
232
236
  deep_clone(defn)
233
237
  end
234
238
 
235
- # Whether a package is built-in to arduino
236
- # @param package [String] the package name (e.g. "arduino:avr")
237
- # @return [bool]
238
- def package_builtin?(package)
239
- package.start_with?("arduino:")
240
- end
241
-
242
239
  # the URL that gives the download info for a given package (a JSON file).
243
240
  # this is NOT where the package comes from.
244
241
  # @param package [String] the package name (e.g. "arduino:avr")
@@ -14,15 +14,21 @@ module ArduinoCI
14
14
  # Information about an Arduino CPP library, specifically for compilation purposes
15
15
  class CppLibrary
16
16
 
17
- # @return [Pathname] The path to the library being tested
18
- attr_reader :base_dir
17
+ # @return [String] The official library properties file name
18
+ LIBRARY_PROPERTIES_FILE = "library.properties".freeze
19
19
 
20
- # @return [Pathname] The path to the Arduino 3rd-party library directory
21
- attr_reader :arduino_lib_dir
20
+ # @return [String] The "official" name of the library, which can include spaces (in a way that the lib dir won't)
21
+ attr_reader :name
22
+
23
+ # @return [ArduinoBackend] The backend support for this library
24
+ attr_reader :backend
22
25
 
23
26
  # @return [Array<Pathname>] The set of artifacts created by this class (note: incomplete!)
24
27
  attr_reader :artifacts
25
28
 
29
+ # @return [Array<Pathname>] The set of directories that should be excluded from compilation
30
+ attr_reader :exclude_dirs
31
+
26
32
  # @return [String] STDERR from the last command
27
33
  attr_reader :last_err
28
34
 
@@ -35,24 +41,138 @@ module ArduinoCI
35
41
  # @return [Array<Pathname>] Directories suspected of being vendor-bundle
36
42
  attr_reader :vendor_bundle_cache
37
43
 
38
- # @param base_dir [Pathname] The path to the library being tested
39
- # @param arduino_lib_dir [Pathname] The path to the libraries directory
40
- # @param exclude_dirs [Array<Pathname>] Directories that should be excluded from compilation
41
- def initialize(base_dir, arduino_lib_dir, exclude_dirs)
42
- raise ArgumentError, 'base_dir is not a Pathname' unless base_dir.is_a? Pathname
43
- raise ArgumentError, 'arduino_lib_dir is not a Pathname' unless arduino_lib_dir.is_a? Pathname
44
- raise ArgumentError, 'exclude_dir is not an array of Pathnames' unless exclude_dirs.is_a?(Array)
45
- raise ArgumentError, 'exclude_dir array contains non-Pathname elements' unless exclude_dirs.all? { |p| p.is_a? Pathname }
46
-
47
- @base_dir = base_dir
48
- @exclude_dirs = exclude_dirs
49
- @arduino_lib_dir = arduino_lib_dir.expand_path
44
+ # @param friendly_name [String] The "official" name of the library, which can contain spaces
45
+ # @param backend [ArduinoBackend] The support backend
46
+ def initialize(friendly_name, backend)
47
+ raise ArgumentError, "friendly_name is not a String (got #{friendly_name.class})" unless friendly_name.is_a? String
48
+ raise ArgumentError, 'backend is not a ArduinoBackend' unless backend.is_a? ArduinoBackend
49
+
50
+ @name = friendly_name
51
+ @backend = backend
52
+ @info_cache = nil
50
53
  @artifacts = []
51
54
  @last_err = ""
52
55
  @last_out = ""
53
56
  @last_msg = ""
54
57
  @has_libasan_cache = {}
55
58
  @vendor_bundle_cache = nil
59
+ @exclude_dirs = []
60
+ end
61
+
62
+ # Generate a guess as to the on-disk (coerced character) name of this library
63
+ #
64
+ # @TODO: delegate this to the backend in some way? It uses "official" names for install, but dir names in lists :(
65
+ # @param friendly_name [String] The library name as it might appear in library manager
66
+ # @return [String] How the path will be stored on disk -- spaces are coerced to underscores
67
+ def self.library_directory_name(friendly_name)
68
+ friendly_name.tr(" ", "_")
69
+ end
70
+
71
+ # Generate a guess as to the on-disk (coerced character) name of this library
72
+ #
73
+ # @TODO: delegate this to the backend in some way? It uses "official" names for install, but dir names in lists :(
74
+ # @return [String] How the path will be stored on disk -- spaces are coerced to underscores
75
+ def name_on_disk
76
+ self.class.library_directory_name(@name)
77
+ end
78
+
79
+ # Get the path to this library, whether or not it exists
80
+ # @return [Pathname] The fully qualified library path
81
+ def path
82
+ @backend.lib_dir + name_on_disk
83
+ end
84
+
85
+ # @return [String] The parent directory of all examples
86
+ def examples_dir
87
+ path + "examples"
88
+ end
89
+
90
+ # Determine whether a library is present in the lib dir
91
+ #
92
+ # Note that `true` doesn't guarantee that the library is valid/installed
93
+ # and `false` doesn't guarantee that the library isn't built-in
94
+ #
95
+ # @return [bool]
96
+ def installed?
97
+ path.exist?
98
+ end
99
+
100
+ # install a library by name
101
+ # @param version [String] the version to install
102
+ # @param recursive [bool] whether to also install its dependencies
103
+ # @return [bool] whether the command succeeded
104
+ def install(version = nil, recursive = false)
105
+ return true if installed? && !recursive
106
+
107
+ fqln = version.nil? ? @name : "#{@name}@#{version}"
108
+ result = if recursive
109
+ @backend.run_and_capture("lib", "install", fqln)
110
+ else
111
+ @backend.run_and_capture("lib", "install", "--no-deps", fqln)
112
+ end
113
+ result[:success]
114
+ end
115
+
116
+ # information about the library as reported by the backend
117
+ # @return [Hash] the metadata object
118
+ def info
119
+ return nil unless installed?
120
+
121
+ # note that if the library isn't found, we're going to do a lot of cache attempts...
122
+ if @info_cache.nil?
123
+ @info_cache = @backend.installed_libraries.find do |l|
124
+ lib_info = l["library"]
125
+ Pathname.new(lib_info["install_dir"]).realpath == path.realpath
126
+ end
127
+ end
128
+
129
+ @info_cache
130
+ end
131
+
132
+ # @param installed_library_path [String] The library to query
133
+ # @return [Array<String>] Example sketch files
134
+ def example_sketches
135
+ reported_dirs = info["library"]["examples"].map(&Pathname::method(:new))
136
+ reported_dirs.map { |e| e + e.basename.sub_ext(".ino") }.select(&:exist?).sort_by(&:to_s)
137
+ end
138
+
139
+ # The expected path to the library.properties file (i.e. even if it does not exist)
140
+ # @return [Pathname]
141
+ def library_properties_path
142
+ path + LIBRARY_PROPERTIES_FILE
143
+ end
144
+
145
+ # Whether library.properties definitions for this library exist
146
+ # @return [bool]
147
+ def library_properties?
148
+ lib_props = library_properties_path
149
+ lib_props.exist? && lib_props.file?
150
+ end
151
+
152
+ # Library properties
153
+ # @return [LibraryProperties] The library.properties metadata wrapper for this library
154
+ def library_properties
155
+ return nil unless library_properties?
156
+
157
+ LibraryProperties.new(library_properties_path)
158
+ end
159
+
160
+ # Set directories that should be excluded from compilation
161
+ # @param rval [Array] Array of strings or pathnames that will be coerced to pathnames
162
+ def exclude_dirs=(rval)
163
+ @exclude_dirs = rval.map { |d| d.is_a?(Pathname) ? d : Pathname.new(d) }
164
+ end
165
+
166
+ # Decide whether this is a 1.5-compatible library
167
+ #
168
+ # This should be according to https://arduino.github.io/arduino-cli/latest/library-specification
169
+ # but we rely on the cli to decide for us
170
+ # @return [bool]
171
+ def one_point_five?
172
+ return false unless library_properties?
173
+
174
+ src_dir = path + "src"
175
+ src_dir.exist? && src_dir.directory?
56
176
  end
57
177
 
58
178
  # Guess whether a file is part of the vendor bundle (indicating we should ignore it).
@@ -62,9 +182,9 @@ module ArduinoCI
62
182
  # That gets us the vendor directory (or multiple directories). We can check
63
183
  # if the given path is contained by any of those.
64
184
  #
65
- # @param path [Pathname] The path to check
185
+ # @param some_path [Pathname] The path to check
66
186
  # @return [bool]
67
- def vendor_bundle?(path)
187
+ def vendor_bundle?(some_path)
68
188
  # Cache bundle information, as it is (1) time consuming to fetch and (2) not going to change while we run
69
189
  if @vendor_bundle_cache.nil?
70
190
  bundle_info = Host.run_and_capture("bundle show --paths")
@@ -99,7 +219,7 @@ module ArduinoCI
99
219
 
100
220
  # With vendor bundles located, check this file against those
101
221
  @vendor_bundle_cache.any? do |gem_path|
102
- path.ascend do |part|
222
+ some_path.ascend do |part|
103
223
  break true if gem_path == part
104
224
  end
105
225
  end
@@ -109,11 +229,13 @@ module ArduinoCI
109
229
  #
110
230
  # @param path [Pathname] The path to check
111
231
  # @return [bool]
112
- def in_tests_dir?(path)
232
+ def in_tests_dir?(sourcefile_path)
233
+ return false unless tests_dir.exist?
234
+
113
235
  tests_dir_aliases = [tests_dir, tests_dir.realpath]
114
236
  # we could do this but some rubies don't return an enumerator for ascend
115
237
  # path.ascend.any? { |part| tests_dir_aliases.include?(part) }
116
- path.ascend do |part|
238
+ sourcefile_path.ascend do |part|
117
239
  return true if tests_dir_aliases.include?(part)
118
240
  end
119
241
  false
@@ -123,11 +245,11 @@ module ArduinoCI
123
245
  #
124
246
  # @param path [Pathname] The path to check
125
247
  # @return [bool]
126
- def in_exclude_dir?(path)
248
+ def in_exclude_dir?(sourcefile_path)
127
249
  # we could do this but some rubies don't return an enumerator for ascend
128
250
  # path.ascend.any? { |part| tests_dir_aliases.include?(part) }
129
- path.ascend do |part|
130
- return true if exclude_dir.any? { |p| p.realpath == part }
251
+ sourcefile_path.ascend do |part|
252
+ return true if exclude_dir.any? { |p| p.realpath == part.realpath }
131
253
  end
132
254
  false
133
255
  end
@@ -138,13 +260,10 @@ module ArduinoCI
138
260
  # @param gcc_binary [String]
139
261
  def libasan?(gcc_binary)
140
262
  unless @has_libasan_cache.key?(gcc_binary)
141
- file = Tempfile.new(["arduino_ci_libasan_check", ".cpp"])
142
- begin
263
+ Tempfile.create(["arduino_ci_libasan_check", ".cpp"]) do |file|
143
264
  file.write "int main(){}"
144
265
  file.close
145
266
  @has_libasan_cache[gcc_binary] = run_gcc(gcc_binary, "-o", "/dev/null", "-fsanitize=address", file.path)
146
- ensure
147
- file.delete
148
267
  end
149
268
  end
150
269
  @has_libasan_cache[gcc_binary]
@@ -152,67 +271,100 @@ module ArduinoCI
152
271
 
153
272
  # Get a list of all CPP source files in a directory and its subdirectories
154
273
  # @param some_dir [Pathname] The directory in which to begin the search
274
+ # @param extensions [Array<Sring>] The set of allowable file extensions
155
275
  # @return [Array<Pathname>] The paths of the found files
156
- def cpp_files_in(some_dir)
276
+ def code_files_in(some_dir, extensions)
157
277
  raise ArgumentError, 'some_dir is not a Pathname' unless some_dir.is_a? Pathname
158
- return [] unless some_dir.exist? && some_dir.directory?
159
278
 
160
- real = some_dir.realpath
161
- files = Find.find(real).map { |p| Pathname.new(p) }.reject(&:directory?)
162
- cpp = files.select { |path| CPP_EXTENSIONS.include?(path.extname.downcase) }
279
+ full_dir = path + some_dir
280
+ return [] unless full_dir.exist? && full_dir.directory?
281
+
282
+ files = full_dir.children.reject(&:directory?)
283
+ cpp = files.select { |path| extensions.include?(path.extname.downcase) }
163
284
  not_hidden = cpp.reject { |path| path.basename.to_s.start_with?(".") }
164
285
  not_hidden.sort_by(&:to_s)
165
286
  end
166
287
 
288
+ # Get a list of all CPP source files in a directory and its subdirectories
289
+ # @param some_dir [Pathname] The directory in which to begin the search
290
+ # @param extensions [Array<Sring>] The set of allowable file extensions
291
+ # @return [Array<Pathname>] The paths of the found files
292
+ def code_files_in_recursive(some_dir, extensions)
293
+ raise ArgumentError, 'some_dir is not a Pathname' unless some_dir.is_a? Pathname
294
+ return [] unless some_dir.exist? && some_dir.directory?
295
+
296
+ Find.find(some_dir).map { |p| Pathname.new(p) }.select(&:directory?).map { |d| code_files_in(d, extensions) }.flatten
297
+ end
298
+
299
+ # Source files that are part of the library under test
300
+ # @param extensions [Array<String>] the allowed extensions (or, the ones we're looking for)
301
+ # @return [Array<Pathname>]
302
+ def source_files(extensions)
303
+ source_dir = Pathname.new(info["library"]["source_dir"])
304
+ ret = if one_point_five?
305
+ code_files_in_recursive(source_dir, extensions)
306
+ else
307
+ [source_dir, source_dir + "utility"].map { |d| code_files_in(d, extensions) }.flatten
308
+ end
309
+
310
+ # note to future troubleshooter: some of these tests may not be relevant, but at the moment at
311
+ # least some of them are tied to existing features
312
+ ret.reject { |p| vendor_bundle?(p) || in_tests_dir?(p) || in_exclude_dir?(p) }
313
+ end
314
+
315
+ # Header files that are part of the project library under test
316
+ # @return [Array<Pathname>]
317
+ def header_files
318
+ source_files(HPP_EXTENSIONS)
319
+ end
320
+
167
321
  # CPP files that are part of the project library under test
168
322
  # @return [Array<Pathname>]
169
323
  def cpp_files
170
- cpp_files_in(@base_dir).reject { |p| vendor_bundle?(p) || in_tests_dir?(p) || in_exclude_dir?(p) }
324
+ source_files(CPP_EXTENSIONS)
171
325
  end
172
326
 
173
327
  # CPP files that are part of the arduino mock library we're providing
174
328
  # @return [Array<Pathname>]
175
329
  def cpp_files_arduino
176
- cpp_files_in(ARDUINO_HEADER_DIR)
330
+ code_files_in(ARDUINO_HEADER_DIR, CPP_EXTENSIONS)
177
331
  end
178
332
 
179
333
  # CPP files that are part of the unit test library we're providing
180
334
  # @return [Array<Pathname>]
181
335
  def cpp_files_unittest
182
- cpp_files_in(UNITTEST_HEADER_DIR)
336
+ code_files_in(UNITTEST_HEADER_DIR, CPP_EXTENSIONS)
183
337
  end
184
338
 
185
339
  # CPP files that are part of the 3rd-party libraries we're including
186
340
  # @param [Array<String>] aux_libraries
187
341
  # @return [Array<Pathname>]
188
342
  def cpp_files_libraries(aux_libraries)
189
- arduino_library_src_dirs(aux_libraries).map { |d| cpp_files_in(d) }.flatten.uniq
343
+ arduino_library_src_dirs(aux_libraries).map { |d| code_files_in(d, CPP_EXTENSIONS) }.flatten.uniq
190
344
  end
191
345
 
192
346
  # Returns the Pathnames for all paths to exclude from testing and compilation
193
347
  # @return [Array<Pathname>]
194
348
  def exclude_dir
195
- @exclude_dirs.map { |p| Pathname.new(@base_dir) + p }.select(&:exist?)
349
+ @exclude_dirs.map { |p| Pathname.new(path) + p }.select(&:exist?)
196
350
  end
197
351
 
198
352
  # The directory where we expect to find unit test defintions provided by the user
199
353
  # @return [Pathname]
200
354
  def tests_dir
201
- Pathname.new(@base_dir) + "test"
355
+ Pathname.new(path) + "test"
202
356
  end
203
357
 
204
358
  # The files provided by the user that contain unit tests
205
359
  # @return [Array<Pathname>]
206
360
  def test_files
207
- cpp_files_in(tests_dir)
361
+ code_files_in(tests_dir, CPP_EXTENSIONS)
208
362
  end
209
363
 
210
364
  # Find all directories in the project library that include C++ header files
211
365
  # @return [Array<Pathname>]
212
366
  def header_dirs
213
- real = @base_dir.realpath
214
- all_files = Find.find(real).map { |f| Pathname.new(f) }.reject(&:directory?)
215
- unbundled = all_files.reject { |path| vendor_bundle?(path) }
367
+ unbundled = header_files.reject { |path| vendor_bundle?(path) }
216
368
  unexcluded = unbundled.reject { |path| in_exclude_dir?(path) }
217
369
  files = unexcluded.select { |path| HPP_EXTENSIONS.include?(path.extname.downcase) }
218
370
  files.map(&:dirname).uniq
@@ -236,23 +388,38 @@ module ArduinoCI
236
388
  @last_err
237
389
  end
238
390
 
239
- # Arduino library directories containing sources
240
- # @return [Array<Pathname>]
241
- def arduino_library_src_dirs(aux_libraries)
391
+ # Get a list of all dependencies as defined in library.properties
392
+ # @return [Array<String>] The library names of the dependencies (not the paths)
393
+ def arduino_library_dependencies
394
+ return [] unless library_properties?
395
+ return [] if library_properties.depends.nil?
396
+
397
+ library_properties.depends
398
+ end
399
+
400
+ # Arduino library dependencies all the way down, installing if they are not present
401
+ # @return [Array<String>] The library names of the dependencies (not the paths)
402
+ def all_arduino_library_dependencies!(additional_libraries = [])
242
403
  # Pull in all possible places that headers could live, according to the spec:
243
404
  # https://github.com/arduino/Arduino/wiki/Arduino-IDE-1.5:-Library-specification
244
- # TODO: be smart and implement library spec (library.properties, etc)?
245
- subdirs = ["", "src", "utility"]
246
- all_aux_include_dirs_nested = aux_libraries.map do |libdir|
247
- # library manager coerces spaces in package names to underscores
248
- # see https://github.com/ianfixes/arduino_ci/issues/132#issuecomment-518857059
249
- legal_libdir = libdir.tr(" ", "_")
250
- subdirs.map { |subdir| Pathname.new(@arduino_lib_dir) + legal_libdir + subdir }
251
- end
252
- all_aux_include_dirs_nested.flatten.select(&:exist?).select(&:directory?)
405
+ recursive = (additional_libraries + arduino_library_dependencies).map do |n|
406
+ other_lib = self.class.new(n, @backend)
407
+ other_lib.install unless other_lib.installed?
408
+ other_lib.all_arduino_library_dependencies!
409
+ end.flatten
410
+ (additional_libraries + recursive).uniq
411
+ end
412
+
413
+ # Arduino library directories containing sources -- only those of the dependencies
414
+ # @return [Array<Pathname>]
415
+ def arduino_library_src_dirs(aux_libraries)
416
+ all_arduino_library_dependencies!(aux_libraries).map { |l| self.class.new(l, @backend).header_dirs }.flatten.uniq
253
417
  end
254
418
 
255
419
  # GCC command line arguments for including aux libraries
420
+ #
421
+ # This function recursively collects the library directores of the dependencies
422
+ #
256
423
  # @param aux_libraries [Array<Pathname>] The external Arduino libraries required by this project
257
424
  # @return [Array<String>] The GCC command-line flags necessary to include those libraries
258
425
  def include_args(aux_libraries)
@@ -315,6 +482,9 @@ module ArduinoCI
315
482
  end
316
483
 
317
484
  # build a file for running a test of the given unit test file
485
+ #
486
+ # The dependent libraries configuration is appended with data from library.properties internal to the library under test
487
+ #
318
488
  # @param test_file [Pathname] The path to the file containing the unit tests
319
489
  # @param aux_libraries [Array<Pathname>] The external Arduino libraries required by this project
320
490
  # @param ci_gcc_config [Hash] The GCC config object
@@ -333,8 +503,12 @@ module ArduinoCI
333
503
  "-fsanitize=address"
334
504
  ]
335
505
  end
336
- arg_sets << test_args(aux_libraries, ci_gcc_config)
337
- arg_sets << cpp_files_libraries(aux_libraries).map(&:to_s)
506
+
507
+ # combine library.properties defs (if existing) with config file.
508
+ # TODO: as much as I'd like to rely only on the properties file(s), I think that would prevent testing 1.0-spec libs
509
+ full_dependencies = all_arduino_library_dependencies!(aux_libraries)
510
+ arg_sets << test_args(full_dependencies, ci_gcc_config)
511
+ arg_sets << cpp_files_libraries(full_dependencies).map(&:to_s)
338
512
  arg_sets << [test_file.to_s]
339
513
  args = arg_sets.flatten(1)
340
514
  return nil unless run_gcc(gcc_binary, *args)
@@ -343,14 +517,31 @@ module ArduinoCI
343
517
  executable
344
518
  end
345
519
 
520
+ # print any found stack dumps
521
+ # @param executable [Pathname] the path to the test file
522
+ def print_stack_dump(executable)
523
+ possible_dumpfiles = [
524
+ executable.sub_ext("#{executable.extname}.stackdump")
525
+ ]
526
+ possible_dumpfiles.select(&:exist?).each do |dump|
527
+ puts "========== Stack dump from #{dump}:"
528
+ File.foreach(dump) { |line| print " #{line}" }
529
+ end
530
+ end
531
+
346
532
  # run a test file
347
- # @param [Pathname] the path to the test file
533
+ # @param executable [Pathname] the path to the test file
348
534
  # @return [bool] whether all tests were successful
349
535
  def run_test_file(executable)
350
536
  @last_cmd = executable
351
537
  @last_out = ""
352
538
  @last_err = ""
353
- Host.run_and_output(executable.to_s.shellescape)
539
+ ret = Host.run_and_output(executable.to_s.shellescape)
540
+
541
+ # print any stack traces found during a failure
542
+ print_stack_dump(executable) unless ret
543
+
544
+ ret
354
545
  end
355
546
 
356
547
  end