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
@@ -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
@@ -28,6 +28,7 @@ UNITTEST_SCHEMA = {
28
28
  compilers: Array,
29
29
  platforms: Array,
30
30
  libraries: Array,
31
+ exclude_dirs: Array,
31
32
  testfiles: {
32
33
  select: Array,
33
34
  reject: Array,
@@ -65,6 +66,7 @@ module ArduinoCI
65
66
  attr_accessor :platform_info
66
67
  attr_accessor :compile_info
67
68
  attr_accessor :unittest_info
69
+
68
70
  def initialize
69
71
  @package_info = {}
70
72
  @platform_info = {}
@@ -106,7 +108,7 @@ module ArduinoCI
106
108
  good_data = {}
107
109
  source.each do |key, value|
108
110
  ksym = key.to_sym
109
- expected_type = schema[ksym].class == Class ? schema[ksym] : Hash
111
+ expected_type = schema[ksym].instance_of?(Class) ? schema[ksym] : Hash
110
112
  if !schema.include?(ksym)
111
113
  puts "Warning: unknown field '#{ksym}' under definition for #{rootname}"
112
114
  elsif value.nil?
@@ -114,7 +116,7 @@ module ArduinoCI
114
116
  elsif value.class != expected_type
115
117
  puts "Warning: expected field '#{ksym}' of #{rootname} to be '#{expected_type}', got '#{value.class}'"
116
118
  else
117
- good_data[ksym] = value.class == Hash ? validate_data(key, value, schema[ksym]) : value
119
+ good_data[ksym] = value.instance_of?(Hash) ? validate_data(key, value, schema[ksym]) : value
118
120
  end
119
121
  end
120
122
  good_data
@@ -231,13 +233,6 @@ module ArduinoCI
231
233
  deep_clone(defn)
232
234
  end
233
235
 
234
- # Whether a package is built-in to arduino
235
- # @param package [String] the package name (e.g. "arduino:avr")
236
- # @return [bool]
237
- def package_builtin?(package)
238
- package.start_with?("arduino:")
239
- end
240
-
241
236
  # the URL that gives the download info for a given package (a JSON file).
242
237
  # this is NOT where the package comes from.
243
238
  # @param package [String] the package name (e.g. "arduino:avr")
@@ -256,6 +251,14 @@ module ArduinoCI
256
251
  @unittest_info[:compilers]
257
252
  end
258
253
 
254
+ # paths to exclude all files in for building and unitttests
255
+ # @return [Array<String>] The directories (relative to base dir) to exclude
256
+ def exclude_dirs
257
+ return [] if @unittest_info[:exclude_dirs].nil?
258
+
259
+ @unittest_info[:exclude_dirs]
260
+ end
261
+
259
262
  # platforms to build [the examples on]
260
263
  # @return [Array<String>] The platforms to build
261
264
  def platforms_to_build
@@ -293,9 +296,12 @@ module ArduinoCI
293
296
  return paths if @unittest_info[:testfiles].nil?
294
297
 
295
298
  ret = paths
299
+ # Check for array emptiness, otherwise nothing will be selected!
296
300
  unless @unittest_info[:testfiles][:select].nil? || @unittest_info[:testfiles][:select].empty?
297
301
  ret.select! { |p| unittest_info[:testfiles][:select].any? { |glob| p.basename.fnmatch(glob) } }
298
302
  end
303
+
304
+ # It's OK for the :reject array to be empty, that means nothing will be rejected by default
299
305
  unless @unittest_info[:testfiles][:reject].nil?
300
306
  ret.reject! { |p| unittest_info[:testfiles][:reject].any? { |glob| p.basename.fnmatch(glob) } }
301
307
  end
@@ -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,20 +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
- def initialize(base_dir, arduino_lib_dir)
41
- raise ArgumentError, 'base_dir is not a Pathname' unless base_dir.is_a? Pathname
42
- raise ArgumentError, 'arduino_lib_dir is not a Pathname' unless arduino_lib_dir.is_a? Pathname
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
43
49
 
44
- @base_dir = base_dir
45
- @arduino_lib_dir = arduino_lib_dir.expand_path
50
+ @name = friendly_name
51
+ @backend = backend
52
+ @info_cache = nil
46
53
  @artifacts = []
47
54
  @last_err = ""
48
55
  @last_out = ""
49
56
  @last_msg = ""
50
57
  @has_libasan_cache = {}
51
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?
52
176
  end
53
177
 
54
178
  # Guess whether a file is part of the vendor bundle (indicating we should ignore it).
@@ -58,9 +182,9 @@ module ArduinoCI
58
182
  # That gets us the vendor directory (or multiple directories). We can check
59
183
  # if the given path is contained by any of those.
60
184
  #
61
- # @param path [Pathname] The path to check
185
+ # @param some_path [Pathname] The path to check
62
186
  # @return [bool]
63
- def vendor_bundle?(path)
187
+ def vendor_bundle?(some_path)
64
188
  # Cache bundle information, as it is (1) time consuming to fetch and (2) not going to change while we run
65
189
  if @vendor_bundle_cache.nil?
66
190
  bundle_info = Host.run_and_capture("bundle show --paths")
@@ -95,7 +219,7 @@ module ArduinoCI
95
219
 
96
220
  # With vendor bundles located, check this file against those
97
221
  @vendor_bundle_cache.any? do |gem_path|
98
- path.ascend do |part|
222
+ some_path.ascend do |part|
99
223
  break true if gem_path == part
100
224
  end
101
225
  end
@@ -105,29 +229,41 @@ module ArduinoCI
105
229
  #
106
230
  # @param path [Pathname] The path to check
107
231
  # @return [bool]
108
- def in_tests_dir?(path)
232
+ def in_tests_dir?(sourcefile_path)
233
+ return false unless tests_dir.exist?
234
+
109
235
  tests_dir_aliases = [tests_dir, tests_dir.realpath]
110
236
  # we could do this but some rubies don't return an enumerator for ascend
111
237
  # path.ascend.any? { |part| tests_dir_aliases.include?(part) }
112
- path.ascend do |part|
238
+ sourcefile_path.ascend do |part|
113
239
  return true if tests_dir_aliases.include?(part)
114
240
  end
115
241
  false
116
242
  end
117
243
 
244
+ # Guess whether a file is part of any @excludes_dir dir (indicating library compilation should ignore it).
245
+ #
246
+ # @param path [Pathname] The path to check
247
+ # @return [bool]
248
+ def in_exclude_dir?(sourcefile_path)
249
+ # we could do this but some rubies don't return an enumerator for ascend
250
+ # path.ascend.any? { |part| tests_dir_aliases.include?(part) }
251
+ sourcefile_path.ascend do |part|
252
+ return true if exclude_dir.any? { |p| p.realpath == part.realpath }
253
+ end
254
+ false
255
+ end
256
+
118
257
  # Check whether libasan (and by extension -fsanitizer=address) is supported
119
258
  #
120
259
  # This requires compilation of a sample program, and will be cached
121
260
  # @param gcc_binary [String]
122
261
  def libasan?(gcc_binary)
123
262
  unless @has_libasan_cache.key?(gcc_binary)
124
- file = Tempfile.new(["arduino_ci_libasan_check", ".cpp"])
125
- begin
263
+ Tempfile.create(["arduino_ci_libasan_check", ".cpp"]) do |file|
126
264
  file.write "int main(){}"
127
265
  file.close
128
266
  @has_libasan_cache[gcc_binary] = run_gcc(gcc_binary, "-o", "/dev/null", "-fsanitize=address", file.path)
129
- ensure
130
- file.delete
131
267
  end
132
268
  end
133
269
  @has_libasan_cache[gcc_binary]
@@ -135,62 +271,102 @@ module ArduinoCI
135
271
 
136
272
  # Get a list of all CPP source files in a directory and its subdirectories
137
273
  # @param some_dir [Pathname] The directory in which to begin the search
274
+ # @param extensions [Array<Sring>] The set of allowable file extensions
138
275
  # @return [Array<Pathname>] The paths of the found files
139
- def cpp_files_in(some_dir)
276
+ def code_files_in(some_dir, extensions)
140
277
  raise ArgumentError, 'some_dir is not a Pathname' unless some_dir.is_a? Pathname
141
- return [] unless some_dir.exist? && some_dir.directory?
142
278
 
143
- real = some_dir.realpath
144
- files = Find.find(real).map { |p| Pathname.new(p) }.reject(&:directory?)
145
- 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) }
146
284
  not_hidden = cpp.reject { |path| path.basename.to_s.start_with?(".") }
147
285
  not_hidden.sort_by(&:to_s)
148
286
  end
149
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
+
150
321
  # CPP files that are part of the project library under test
151
322
  # @return [Array<Pathname>]
152
323
  def cpp_files
153
- cpp_files_in(@base_dir).reject { |p| vendor_bundle?(p) || in_tests_dir?(p) }
324
+ source_files(CPP_EXTENSIONS)
154
325
  end
155
326
 
156
327
  # CPP files that are part of the arduino mock library we're providing
157
328
  # @return [Array<Pathname>]
158
329
  def cpp_files_arduino
159
- cpp_files_in(ARDUINO_HEADER_DIR)
330
+ code_files_in(ARDUINO_HEADER_DIR, CPP_EXTENSIONS)
160
331
  end
161
332
 
162
333
  # CPP files that are part of the unit test library we're providing
163
334
  # @return [Array<Pathname>]
164
335
  def cpp_files_unittest
165
- cpp_files_in(UNITTEST_HEADER_DIR)
336
+ code_files_in(UNITTEST_HEADER_DIR, CPP_EXTENSIONS)
166
337
  end
167
338
 
168
339
  # CPP files that are part of the 3rd-party libraries we're including
169
340
  # @param [Array<String>] aux_libraries
170
341
  # @return [Array<Pathname>]
171
342
  def cpp_files_libraries(aux_libraries)
172
- 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
344
+ end
345
+
346
+ # Returns the Pathnames for all paths to exclude from testing and compilation
347
+ # @return [Array<Pathname>]
348
+ def exclude_dir
349
+ @exclude_dirs.map { |p| Pathname.new(path) + p }.select(&:exist?)
173
350
  end
174
351
 
175
352
  # The directory where we expect to find unit test defintions provided by the user
176
353
  # @return [Pathname]
177
354
  def tests_dir
178
- Pathname.new(@base_dir) + "test"
355
+ Pathname.new(path) + "test"
179
356
  end
180
357
 
181
358
  # The files provided by the user that contain unit tests
182
359
  # @return [Array<Pathname>]
183
360
  def test_files
184
- cpp_files_in(tests_dir)
361
+ code_files_in(tests_dir, CPP_EXTENSIONS)
185
362
  end
186
363
 
187
364
  # Find all directories in the project library that include C++ header files
188
365
  # @return [Array<Pathname>]
189
366
  def header_dirs
190
- real = @base_dir.realpath
191
- all_files = Find.find(real).map { |f| Pathname.new(f) }.reject(&:directory?)
192
- unbundled = all_files.reject { |path| vendor_bundle?(path) }
193
- files = unbundled.select { |path| HPP_EXTENSIONS.include?(path.extname.downcase) }
367
+ unbundled = header_files.reject { |path| vendor_bundle?(path) }
368
+ unexcluded = unbundled.reject { |path| in_exclude_dir?(path) }
369
+ files = unexcluded.select { |path| HPP_EXTENSIONS.include?(path.extname.downcase) }
194
370
  files.map(&:dirname).uniq
195
371
  end
196
372
 
@@ -212,20 +388,38 @@ module ArduinoCI
212
388
  @last_err
213
389
  end
214
390
 
215
- # Arduino library directories containing sources
216
- # @return [Array<Pathname>]
217
- 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 = [])
218
403
  # Pull in all possible places that headers could live, according to the spec:
219
404
  # https://github.com/arduino/Arduino/wiki/Arduino-IDE-1.5:-Library-specification
220
- # TODO: be smart and implement library spec (library.properties, etc)?
221
- subdirs = ["", "src", "utility"]
222
- all_aux_include_dirs_nested = aux_libraries.map do |libdir|
223
- subdirs.map { |subdir| Pathname.new(@arduino_lib_dir) + libdir + subdir }
224
- end
225
- 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
226
417
  end
227
418
 
228
419
  # GCC command line arguments for including aux libraries
420
+ #
421
+ # This function recursively collects the library directores of the dependencies
422
+ #
229
423
  # @param aux_libraries [Array<Pathname>] The external Arduino libraries required by this project
230
424
  # @return [Array<String>] The GCC command-line flags necessary to include those libraries
231
425
  def include_args(aux_libraries)
@@ -288,6 +482,9 @@ module ArduinoCI
288
482
  end
289
483
 
290
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
+ #
291
488
  # @param test_file [Pathname] The path to the file containing the unit tests
292
489
  # @param aux_libraries [Array<Pathname>] The external Arduino libraries required by this project
293
490
  # @param ci_gcc_config [Hash] The GCC config object
@@ -306,8 +503,12 @@ module ArduinoCI
306
503
  "-fsanitize=address"
307
504
  ]
308
505
  end
309
- arg_sets << test_args(aux_libraries, ci_gcc_config)
310
- 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)
311
512
  arg_sets << [test_file.to_s]
312
513
  args = arg_sets.flatten(1)
313
514
  return nil unless run_gcc(gcc_binary, *args)
@@ -316,14 +517,31 @@ module ArduinoCI
316
517
  executable
317
518
  end
318
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
+
319
532
  # run a test file
320
- # @param [Pathname] the path to the test file
533
+ # @param executable [Pathname] the path to the test file
321
534
  # @return [bool] whether all tests were successful
322
535
  def run_test_file(executable)
323
536
  @last_cmd = executable
324
537
  @last_out = ""
325
538
  @last_err = ""
326
- 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
327
545
  end
328
546
 
329
547
  end