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
@@ -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