arduino_ci 0.2.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +49 -20
  3. data/REFERENCE.md +636 -0
  4. data/cpp/arduino/Arduino.h +1 -1
  5. data/cpp/arduino/AvrMath.h +117 -17
  6. data/cpp/arduino/Client.h +27 -0
  7. data/cpp/arduino/EEPROM.h +64 -0
  8. data/cpp/arduino/Godmode.cpp +11 -0
  9. data/cpp/arduino/Godmode.h +58 -9
  10. data/cpp/arduino/HardwareSerial.h +9 -28
  11. data/cpp/arduino/IPAddress.h +59 -0
  12. data/cpp/arduino/Print.h +9 -12
  13. data/cpp/arduino/Printable.h +8 -0
  14. data/cpp/arduino/SPI.h +11 -3
  15. data/cpp/arduino/Server.h +5 -0
  16. data/cpp/arduino/Udp.h +27 -0
  17. data/cpp/arduino/Wire.h +234 -0
  18. data/cpp/arduino/avr/io.h +10 -1
  19. data/cpp/arduino/avr/pgmspace.h +76 -46
  20. data/cpp/arduino/ci/StreamTape.h +36 -0
  21. data/cpp/unittest/OstreamHelpers.h +4 -0
  22. data/exe/arduino_ci.rb +427 -0
  23. data/exe/arduino_ci_remote.rb +2 -385
  24. data/exe/arduino_library_location.rb +2 -2
  25. data/exe/ensure_arduino_installation.rb +7 -1
  26. data/lib/arduino_ci.rb +1 -0
  27. data/lib/arduino_ci/arduino_backend.rb +222 -0
  28. data/lib/arduino_ci/arduino_downloader.rb +43 -73
  29. data/lib/arduino_ci/arduino_downloader_linux.rb +17 -55
  30. data/lib/arduino_ci/arduino_downloader_osx.rb +21 -33
  31. data/lib/arduino_ci/arduino_downloader_windows.rb +11 -53
  32. data/lib/arduino_ci/arduino_installation.rb +18 -80
  33. data/lib/arduino_ci/ci_config.rb +15 -9
  34. data/lib/arduino_ci/cpp_library.rb +266 -48
  35. data/lib/arduino_ci/host.rb +59 -4
  36. data/lib/arduino_ci/library_properties.rb +96 -0
  37. data/lib/arduino_ci/version.rb +1 -1
  38. data/misc/default.yml +55 -4
  39. metadata +21 -87
  40. data/cpp/arduino/Arduino.h.orig +0 -143
  41. data/exe/libasan.rb +0 -29
  42. data/lib/arduino_ci/arduino_cmd.rb +0 -328
  43. data/lib/arduino_ci/arduino_cmd_linux.rb +0 -17
  44. data/lib/arduino_ci/arduino_cmd_linux_builder.rb +0 -19
  45. data/lib/arduino_ci/arduino_cmd_osx.rb +0 -17
  46. data/lib/arduino_ci/arduino_cmd_windows.rb +0 -17
@@ -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