arduino_ci 0.1.21 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +49 -20
  3. data/REFERENCE.md +625 -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 +38 -19
  9. data/cpp/arduino/Godmode.h +88 -22
  10. data/cpp/arduino/HardwareSerial.h +9 -28
  11. data/cpp/arduino/IPAddress.h +59 -0
  12. data/cpp/arduino/MockEventQueue.h +86 -0
  13. data/cpp/arduino/PinHistory.h +64 -24
  14. data/cpp/arduino/Print.h +9 -12
  15. data/cpp/arduino/Printable.h +8 -0
  16. data/cpp/arduino/SPI.h +11 -3
  17. data/cpp/arduino/Server.h +5 -0
  18. data/cpp/arduino/Udp.h +27 -0
  19. data/cpp/arduino/Wire.h +234 -0
  20. data/cpp/arduino/avr/io.h +10 -1
  21. data/cpp/arduino/avr/pgmspace.h +76 -46
  22. data/cpp/arduino/ci/StreamTape.h +36 -0
  23. data/cpp/unittest/OstreamHelpers.h +4 -0
  24. data/exe/arduino_ci.rb +400 -0
  25. data/exe/arduino_ci_remote.rb +2 -385
  26. data/exe/arduino_library_location.rb +2 -2
  27. data/lib/arduino_ci.rb +1 -0
  28. data/lib/arduino_ci/arduino_backend.rb +218 -0
  29. data/lib/arduino_ci/arduino_downloader.rb +42 -72
  30. data/lib/arduino_ci/arduino_downloader_linux.rb +17 -55
  31. data/lib/arduino_ci/arduino_downloader_osx.rb +21 -33
  32. data/lib/arduino_ci/arduino_downloader_windows.rb +11 -53
  33. data/lib/arduino_ci/arduino_installation.rb +18 -80
  34. data/lib/arduino_ci/ci_config.rb +12 -7
  35. data/lib/arduino_ci/cpp_library.rb +262 -48
  36. data/lib/arduino_ci/host.rb +59 -4
  37. data/lib/arduino_ci/library_properties.rb +96 -0
  38. data/lib/arduino_ci/version.rb +1 -1
  39. data/misc/default.yml +55 -4
  40. metadata +18 -83
  41. data/cpp/arduino/Arduino.h.orig +0 -143
  42. data/cpp/arduino/ci/Queue.h +0 -73
  43. data/exe/libasan.rb +0 -29
  44. data/lib/arduino_ci/arduino_cmd.rb +0 -328
  45. data/lib/arduino_ci/arduino_cmd_linux.rb +0 -17
  46. data/lib/arduino_ci/arduino_cmd_linux_builder.rb +0 -19
  47. data/lib/arduino_ci/arduino_cmd_osx.rb +0 -17
  48. data/lib/arduino_ci/arduino_cmd_windows.rb +0 -17
@@ -8,54 +8,42 @@ module ArduinoCI
8
8
  # The local filename of the desired IDE package (zip/tar/etc)
9
9
  # @return [string]
10
10
  def package_file
11
- "arduino-#{@desired_ide_version}-macosx.zip"
11
+ "arduino-cli_#{@desired_version}_macOS_64bit.tar.gz"
12
12
  end
13
13
 
14
14
  # The local file (dir) name of the extracted IDE package (zip/tar/etc)
15
15
  # @return [string]
16
- def extracted_file
17
- "Arduino.app"
16
+ def self.extracted_file
17
+ "arduino-cli"
18
18
  end
19
19
 
20
- # @return [String] The location where a forced install will go
21
- def self.force_install_location
22
- # include the .app extension
23
- File.join(ENV['HOME'], 'Arduino.app')
24
- end
25
-
26
- # An existing Arduino directory in one of the given directories, or nil
27
- # @param Array<string> a list of places to look
20
+ # The executable Arduino file in an existing installation, or nil
28
21
  # @return [string]
29
- def self.find_existing_arduino_dir(paths)
30
- paths.find(&File.method(:exist?))
22
+ def self.existing_executable
23
+ Host.which("arduino-cli")
31
24
  end
32
25
 
33
- # An existing Arduino file in one of the given directories, or nil
34
- # @param Array<string> a list of places to look for the executable
35
- # @return [string]
36
- def self.find_existing_arduino_exe(paths)
37
- paths.find do |path|
38
- exe = File.join(path, "MacOS", "Arduino")
39
- File.exist? exe
26
+ # Make any preparations or run any checks prior to making changes
27
+ # @return [string] Error message, or nil if success
28
+ def prepare
29
+ reqs = [self.class.extracter]
30
+ reqs.each do |req|
31
+ return "#{req} does not appear to be installed!" unless Host.which(req)
40
32
  end
33
+ nil
41
34
  end
42
35
 
43
- # The path to the directory of an existing installation, or nil
36
+ # The technology that will be used to extract the download
37
+ # (for logging purposes)
44
38
  # @return [string]
45
- def self.existing_installation
46
- self.find_existing_arduino_dir(["/Applications/Arduino.app"])
39
+ def self.extracter
40
+ "tar"
47
41
  end
48
42
 
49
- # The executable Arduino file in an existing installation, or nil
50
- # @return [string]
51
- def self.existing_executable
52
- self.find_existing_arduino_exe(["/Applications/Arduino.app"])
53
- end
54
-
55
- # The executable Arduino file in a forced installation, or nil
56
- # @return [string]
57
- def self.force_installed_executable
58
- self.find_existing_arduino_exe([self.force_install_location])
43
+ # Extract the package_file to extracted_file
44
+ # @return [bool] whether successful
45
+ def self.extract(package_file)
46
+ system(extracter, "xf", package_file, extracted_file)
59
47
  end
60
48
 
61
49
  end
@@ -10,19 +10,6 @@ module ArduinoCI
10
10
  # Manage the POSIX download & install of Arduino
11
11
  class ArduinoDownloaderWindows < ArduinoDownloader
12
12
 
13
- # Make any preparations or run any checks prior to making changes
14
- # @return [string] Error message, or nil if success
15
- def prepare
16
- nil
17
- end
18
-
19
- # The technology that will be used to complete the download
20
- # (for logging purposes)
21
- # @return [string]
22
- def downloader
23
- "open-uri"
24
- end
25
-
26
13
  # Download the package_url to package_file
27
14
  # @return [bool] whether successful
28
15
  def download
@@ -35,29 +22,28 @@ module ArduinoCI
35
22
  @output.puts "\nArduino force-install failed downloading #{package_url}: #{e}"
36
23
  end
37
24
 
38
- # Move the extracted package file from extracted_file to the force_install_location
39
- # @return [bool] whether successful
40
- def install
41
- # Move only the content of the directory
42
- FileUtils.mv extracted_file, self.class.force_install_location
43
- end
44
-
45
25
  # The local filename of the desired IDE package (zip/tar/etc)
46
26
  # @return [string]
47
27
  def package_file
48
- "#{extracted_file}-windows.zip"
28
+ "arduino-cli_#{@desired_version}_Windows_64bit.zip"
29
+ end
30
+
31
+ # The executable Arduino file in an existing installation, or nil
32
+ # @return [string]
33
+ def self.existing_executable
34
+ Host.which("arduino-cli")
49
35
  end
50
36
 
51
37
  # The technology that will be used to extract the download
52
38
  # (for logging purposes)
53
39
  # @return [string]
54
- def extracter
40
+ def self.extracter
55
41
  "Expand-Archive"
56
42
  end
57
43
 
58
44
  # Extract the package_file to extracted_file
59
45
  # @return [bool] whether successful
60
- def extract
46
+ def self.extract(package_file)
61
47
  Zip::File.open(package_file) do |zip|
62
48
  zip.each do |file|
63
49
  file.extract(file.name)
@@ -67,36 +53,8 @@ module ArduinoCI
67
53
 
68
54
  # The local file (dir) name of the extracted IDE package (zip/tar/etc)
69
55
  # @return [string]
70
- def extracted_file
71
- "arduino-#{@desired_ide_version}"
72
- end
73
-
74
- # The path to the directory of an existing installation, or nil
75
- # @return [string]
76
- def self.existing_installation
77
- exe = self.existing_executable
78
- return nil if exe.nil?
79
-
80
- File.dirname(exe)
81
- end
82
-
83
- # The executable Arduino file in an existing installation, or nil
84
- # @return [string]
85
- def self.existing_executable
86
- arduino_reg = 'SOFTWARE\WOW6432Node\Arduino'
87
- Win32::Registry::HKEY_LOCAL_MACHINE.open(arduino_reg).find do |reg|
88
- path = reg.read_s('Install_Dir')
89
- exe = File.join(path, "arduino_debug.exe")
90
- File.exist? exe
91
- end
92
- rescue
93
- nil
94
- end
95
-
96
- # The executable Arduino file in a forced installation, or nil
97
- # @return [string]
98
- def self.force_installed_executable
99
- File.join(self.force_install_location, "arduino_debug.exe")
56
+ def self.extracted_file
57
+ "arduino-cli.exe"
100
58
  end
101
59
 
102
60
  end
@@ -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,
@@ -231,13 +232,6 @@ module ArduinoCI
231
232
  deep_clone(defn)
232
233
  end
233
234
 
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
235
  # the URL that gives the download info for a given package (a JSON file).
242
236
  # this is NOT where the package comes from.
243
237
  # @param package [String] the package name (e.g. "arduino:avr")
@@ -256,6 +250,14 @@ module ArduinoCI
256
250
  @unittest_info[:compilers]
257
251
  end
258
252
 
253
+ # paths to exclude all files in for building and unitttests
254
+ # @return [Array<String>] The directories (relative to base dir) to exclude
255
+ def exclude_dirs
256
+ return [] if @unittest_info[:exclude_dirs].nil?
257
+
258
+ @unittest_info[:exclude_dirs]
259
+ end
260
+
259
261
  # platforms to build [the examples on]
260
262
  # @return [Array<String>] The platforms to build
261
263
  def platforms_to_build
@@ -293,9 +295,12 @@ module ArduinoCI
293
295
  return paths if @unittest_info[:testfiles].nil?
294
296
 
295
297
  ret = paths
298
+ # Check for array emptiness, otherwise nothing will be selected!
296
299
  unless @unittest_info[:testfiles][:select].nil? || @unittest_info[:testfiles][:select].empty?
297
300
  ret.select! { |p| unittest_info[:testfiles][:select].any? { |glob| p.basename.fnmatch(glob) } }
298
301
  end
302
+
303
+ # It's OK for the :reject array to be empty, that means nothing will be rejected by default
299
304
  unless @unittest_info[:testfiles][:reject].nil?
300
305
  ret.reject! { |p| unittest_info[:testfiles][:reject].any? { |glob| p.basename.fnmatch(glob) } }
301
306
  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,133 @@ 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
+ # Determine whether a library is present in the lib dir
86
+ #
87
+ # Note that `true` doesn't guarantee that the library is valid/installed
88
+ # and `false` doesn't guarantee that the library isn't built-in
89
+ #
90
+ # @return [bool]
91
+ def installed?
92
+ path.exist?
93
+ end
94
+
95
+ # install a library by name
96
+ # @param version [String] the version to install
97
+ # @param recursive [bool] whether to also install its dependencies
98
+ # @return [bool] whether the command succeeded
99
+ def install(version = nil, recursive = false)
100
+ return true if installed? && !recursive
101
+
102
+ fqln = version.nil? ? @name : "#{@name}@#{version}"
103
+ result = if recursive
104
+ @backend.run_and_capture("lib", "install", fqln)
105
+ else
106
+ @backend.run_and_capture("lib", "install", "--no-deps", fqln)
107
+ end
108
+ result[:success]
109
+ end
110
+
111
+ # information about the library as reported by the backend
112
+ # @return [Hash] the metadata object
113
+ def info
114
+ return nil unless installed?
115
+
116
+ # note that if the library isn't found, we're going to do a lot of cache attempts...
117
+ if @info_cache.nil?
118
+ @info_cache = @backend.installed_libraries.find do |l|
119
+ lib_info = l["library"]
120
+ Pathname.new(lib_info["install_dir"]).realpath == path.realpath
121
+ end
122
+ end
123
+
124
+ @info_cache
125
+ end
126
+
127
+ # @param installed_library_path [String] The library to query
128
+ # @return [Array<String>] Example sketch files
129
+ def example_sketches
130
+ reported_dirs = info["library"]["examples"].map(&Pathname::method(:new))
131
+ reported_dirs.map { |e| e + e.basename.sub_ext(".ino") }.select(&:exist?).sort_by(&:to_s)
132
+ end
133
+
134
+ # The expected path to the library.properties file (i.e. even if it does not exist)
135
+ # @return [Pathname]
136
+ def library_properties_path
137
+ path + LIBRARY_PROPERTIES_FILE
138
+ end
139
+
140
+ # Whether library.properties definitions for this library exist
141
+ # @return [bool]
142
+ def library_properties?
143
+ lib_props = library_properties_path
144
+ lib_props.exist? && lib_props.file?
145
+ end
146
+
147
+ # Library properties
148
+ # @return [LibraryProperties] The library.properties metadata wrapper for this library
149
+ def library_properties
150
+ return nil unless library_properties?
151
+
152
+ LibraryProperties.new(library_properties_path)
153
+ end
154
+
155
+ # Set directories that should be excluded from compilation
156
+ # @param rval [Array] Array of strings or pathnames that will be coerced to pathnames
157
+ def exclude_dirs=(rval)
158
+ @exclude_dirs = rval.map { |d| d.is_a?(Pathname) ? d : Pathname.new(d) }
159
+ end
160
+
161
+ # Decide whether this is a 1.5-compatible library
162
+ #
163
+ # This should be according to https://arduino.github.io/arduino-cli/latest/library-specification
164
+ # but we rely on the cli to decide for us
165
+ # @return [bool]
166
+ def one_point_five?
167
+ return false unless library_properties?
168
+
169
+ src_dir = path + "src"
170
+ src_dir.exist? && src_dir.directory?
52
171
  end
53
172
 
54
173
  # Guess whether a file is part of the vendor bundle (indicating we should ignore it).
@@ -58,9 +177,9 @@ module ArduinoCI
58
177
  # That gets us the vendor directory (or multiple directories). We can check
59
178
  # if the given path is contained by any of those.
60
179
  #
61
- # @param path [Pathname] The path to check
180
+ # @param some_path [Pathname] The path to check
62
181
  # @return [bool]
63
- def vendor_bundle?(path)
182
+ def vendor_bundle?(some_path)
64
183
  # Cache bundle information, as it is (1) time consuming to fetch and (2) not going to change while we run
65
184
  if @vendor_bundle_cache.nil?
66
185
  bundle_info = Host.run_and_capture("bundle show --paths")
@@ -95,7 +214,7 @@ module ArduinoCI
95
214
 
96
215
  # With vendor bundles located, check this file against those
97
216
  @vendor_bundle_cache.any? do |gem_path|
98
- path.ascend do |part|
217
+ some_path.ascend do |part|
99
218
  break true if gem_path == part
100
219
  end
101
220
  end
@@ -105,29 +224,41 @@ module ArduinoCI
105
224
  #
106
225
  # @param path [Pathname] The path to check
107
226
  # @return [bool]
108
- def in_tests_dir?(path)
227
+ def in_tests_dir?(sourcefile_path)
228
+ return false unless tests_dir.exist?
229
+
109
230
  tests_dir_aliases = [tests_dir, tests_dir.realpath]
110
231
  # we could do this but some rubies don't return an enumerator for ascend
111
232
  # path.ascend.any? { |part| tests_dir_aliases.include?(part) }
112
- path.ascend do |part|
233
+ sourcefile_path.ascend do |part|
113
234
  return true if tests_dir_aliases.include?(part)
114
235
  end
115
236
  false
116
237
  end
117
238
 
239
+ # Guess whether a file is part of any @excludes_dir dir (indicating library compilation should ignore it).
240
+ #
241
+ # @param path [Pathname] The path to check
242
+ # @return [bool]
243
+ def in_exclude_dir?(sourcefile_path)
244
+ # we could do this but some rubies don't return an enumerator for ascend
245
+ # path.ascend.any? { |part| tests_dir_aliases.include?(part) }
246
+ sourcefile_path.ascend do |part|
247
+ return true if exclude_dir.any? { |p| p.realpath == part.realpath }
248
+ end
249
+ false
250
+ end
251
+
118
252
  # Check whether libasan (and by extension -fsanitizer=address) is supported
119
253
  #
120
254
  # This requires compilation of a sample program, and will be cached
121
255
  # @param gcc_binary [String]
122
256
  def libasan?(gcc_binary)
123
257
  unless @has_libasan_cache.key?(gcc_binary)
124
- file = Tempfile.new(["arduino_ci_libasan_check", ".cpp"])
125
- begin
258
+ Tempfile.create(["arduino_ci_libasan_check", ".cpp"]) do |file|
126
259
  file.write "int main(){}"
127
260
  file.close
128
261
  @has_libasan_cache[gcc_binary] = run_gcc(gcc_binary, "-o", "/dev/null", "-fsanitize=address", file.path)
129
- ensure
130
- file.delete
131
262
  end
132
263
  end
133
264
  @has_libasan_cache[gcc_binary]
@@ -135,62 +266,102 @@ module ArduinoCI
135
266
 
136
267
  # Get a list of all CPP source files in a directory and its subdirectories
137
268
  # @param some_dir [Pathname] The directory in which to begin the search
269
+ # @param extensions [Array<Sring>] The set of allowable file extensions
138
270
  # @return [Array<Pathname>] The paths of the found files
139
- def cpp_files_in(some_dir)
271
+ def code_files_in(some_dir, extensions)
140
272
  raise ArgumentError, 'some_dir is not a Pathname' unless some_dir.is_a? Pathname
141
- return [] unless some_dir.exist? && some_dir.directory?
142
273
 
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) }
274
+ full_dir = path + some_dir
275
+ return [] unless full_dir.exist? && full_dir.directory?
276
+
277
+ files = full_dir.children.reject(&:directory?)
278
+ cpp = files.select { |path| extensions.include?(path.extname.downcase) }
146
279
  not_hidden = cpp.reject { |path| path.basename.to_s.start_with?(".") }
147
280
  not_hidden.sort_by(&:to_s)
148
281
  end
149
282
 
283
+ # Get a list of all CPP source files in a directory and its subdirectories
284
+ # @param some_dir [Pathname] The directory in which to begin the search
285
+ # @param extensions [Array<Sring>] The set of allowable file extensions
286
+ # @return [Array<Pathname>] The paths of the found files
287
+ def code_files_in_recursive(some_dir, extensions)
288
+ raise ArgumentError, 'some_dir is not a Pathname' unless some_dir.is_a? Pathname
289
+ return [] unless some_dir.exist? && some_dir.directory?
290
+
291
+ Find.find(some_dir).map { |p| Pathname.new(p) }.select(&:directory?).map { |d| code_files_in(d, extensions) }.flatten
292
+ end
293
+
294
+ # Source files that are part of the library under test
295
+ # @param extensions [Array<String>] the allowed extensions (or, the ones we're looking for)
296
+ # @return [Array<Pathname>]
297
+ def source_files(extensions)
298
+ source_dir = Pathname.new(info["library"]["source_dir"])
299
+ ret = if one_point_five?
300
+ code_files_in_recursive(source_dir, extensions)
301
+ else
302
+ [source_dir, source_dir + "utility"].map { |d| code_files_in(d, extensions) }.flatten
303
+ end
304
+
305
+ # note to future troubleshooter: some of these tests may not be relevant, but at the moment at
306
+ # least some of them are tied to existing features
307
+ ret.reject { |p| vendor_bundle?(p) || in_tests_dir?(p) || in_exclude_dir?(p) }
308
+ end
309
+
310
+ # Header files that are part of the project library under test
311
+ # @return [Array<Pathname>]
312
+ def header_files
313
+ source_files(HPP_EXTENSIONS)
314
+ end
315
+
150
316
  # CPP files that are part of the project library under test
151
317
  # @return [Array<Pathname>]
152
318
  def cpp_files
153
- cpp_files_in(@base_dir).reject { |p| vendor_bundle?(p) || in_tests_dir?(p) }
319
+ source_files(CPP_EXTENSIONS)
154
320
  end
155
321
 
156
322
  # CPP files that are part of the arduino mock library we're providing
157
323
  # @return [Array<Pathname>]
158
324
  def cpp_files_arduino
159
- cpp_files_in(ARDUINO_HEADER_DIR)
325
+ code_files_in(ARDUINO_HEADER_DIR, CPP_EXTENSIONS)
160
326
  end
161
327
 
162
328
  # CPP files that are part of the unit test library we're providing
163
329
  # @return [Array<Pathname>]
164
330
  def cpp_files_unittest
165
- cpp_files_in(UNITTEST_HEADER_DIR)
331
+ code_files_in(UNITTEST_HEADER_DIR, CPP_EXTENSIONS)
166
332
  end
167
333
 
168
334
  # CPP files that are part of the 3rd-party libraries we're including
169
335
  # @param [Array<String>] aux_libraries
170
336
  # @return [Array<Pathname>]
171
337
  def cpp_files_libraries(aux_libraries)
172
- arduino_library_src_dirs(aux_libraries).map { |d| cpp_files_in(d) }.flatten.uniq
338
+ arduino_library_src_dirs(aux_libraries).map { |d| code_files_in(d, CPP_EXTENSIONS) }.flatten.uniq
339
+ end
340
+
341
+ # Returns the Pathnames for all paths to exclude from testing and compilation
342
+ # @return [Array<Pathname>]
343
+ def exclude_dir
344
+ @exclude_dirs.map { |p| Pathname.new(path) + p }.select(&:exist?)
173
345
  end
174
346
 
175
347
  # The directory where we expect to find unit test defintions provided by the user
176
348
  # @return [Pathname]
177
349
  def tests_dir
178
- Pathname.new(@base_dir) + "test"
350
+ Pathname.new(path) + "test"
179
351
  end
180
352
 
181
353
  # The files provided by the user that contain unit tests
182
354
  # @return [Array<Pathname>]
183
355
  def test_files
184
- cpp_files_in(tests_dir)
356
+ code_files_in(tests_dir, CPP_EXTENSIONS)
185
357
  end
186
358
 
187
359
  # Find all directories in the project library that include C++ header files
188
360
  # @return [Array<Pathname>]
189
361
  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) }
362
+ unbundled = header_files.reject { |path| vendor_bundle?(path) }
363
+ unexcluded = unbundled.reject { |path| in_exclude_dir?(path) }
364
+ files = unexcluded.select { |path| HPP_EXTENSIONS.include?(path.extname.downcase) }
194
365
  files.map(&:dirname).uniq
195
366
  end
196
367
 
@@ -212,20 +383,39 @@ module ArduinoCI
212
383
  @last_err
213
384
  end
214
385
 
215
- # Arduino library directories containing sources
216
- # @return [Array<Pathname>]
217
- def arduino_library_src_dirs(aux_libraries)
386
+ # Get a list of all dependencies as defined in library.properties
387
+ # @return [Array<String>] The library names of the dependencies (not the paths)
388
+ def arduino_library_dependencies
389
+ return [] unless library_properties?
390
+ return [] if library_properties.depends.nil?
391
+
392
+ library_properties.depends
393
+ end
394
+
395
+ # Arduino library dependencies all the way down, installing if they are not present
396
+ # @return [Array<String>] The library names of the dependencies (not the paths)
397
+ def all_arduino_library_dependencies!(additional_libraries = [])
218
398
  # Pull in all possible places that headers could live, according to the spec:
219
399
  # 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?)
400
+ recursive = (additional_libraries + arduino_library_dependencies).map do |n|
401
+ other_lib = self.class.new(n, @backend)
402
+ other_lib.install unless other_lib.installed?
403
+ other_lib.all_arduino_library_dependencies!
404
+ end.flatten
405
+ ret = (additional_libraries + recursive).uniq
406
+ ret
407
+ end
408
+
409
+ # Arduino library directories containing sources -- only those of the dependencies
410
+ # @return [Array<Pathname>]
411
+ def arduino_library_src_dirs(aux_libraries)
412
+ all_arduino_library_dependencies!(aux_libraries).map { |l| self.class.new(l, @backend).header_dirs }.flatten.uniq
226
413
  end
227
414
 
228
415
  # GCC command line arguments for including aux libraries
416
+ #
417
+ # This function recursively collects the library directores of the dependencies
418
+ #
229
419
  # @param aux_libraries [Array<Pathname>] The external Arduino libraries required by this project
230
420
  # @return [Array<String>] The GCC command-line flags necessary to include those libraries
231
421
  def include_args(aux_libraries)
@@ -288,6 +478,9 @@ module ArduinoCI
288
478
  end
289
479
 
290
480
  # build a file for running a test of the given unit test file
481
+ #
482
+ # The dependent libraries configuration is appended with data from library.properties internal to the library under test
483
+ #
291
484
  # @param test_file [Pathname] The path to the file containing the unit tests
292
485
  # @param aux_libraries [Array<Pathname>] The external Arduino libraries required by this project
293
486
  # @param ci_gcc_config [Hash] The GCC config object
@@ -306,8 +499,12 @@ module ArduinoCI
306
499
  "-fsanitize=address"
307
500
  ]
308
501
  end
309
- arg_sets << test_args(aux_libraries, ci_gcc_config)
310
- arg_sets << cpp_files_libraries(aux_libraries).map(&:to_s)
502
+
503
+ # combine library.properties defs (if existing) with config file.
504
+ # 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
505
+ full_dependencies = all_arduino_library_dependencies!(aux_libraries)
506
+ arg_sets << test_args(full_dependencies, ci_gcc_config)
507
+ arg_sets << cpp_files_libraries(full_dependencies).map(&:to_s)
311
508
  arg_sets << [test_file.to_s]
312
509
  args = arg_sets.flatten(1)
313
510
  return nil unless run_gcc(gcc_binary, *args)
@@ -316,14 +513,31 @@ module ArduinoCI
316
513
  executable
317
514
  end
318
515
 
516
+ # print any found stack dumps
517
+ # @param executable [Pathname] the path to the test file
518
+ def print_stack_dump(executable)
519
+ possible_dumpfiles = [
520
+ executable.sub_ext(executable.extname + ".stackdump")
521
+ ]
522
+ possible_dumpfiles.select(&:exist?).each do |dump|
523
+ puts "========== Stack dump from #{dump}:"
524
+ File.foreach(dump) { |line| print " #{line}" }
525
+ end
526
+ end
527
+
319
528
  # run a test file
320
- # @param [Pathname] the path to the test file
529
+ # @param executable [Pathname] the path to the test file
321
530
  # @return [bool] whether all tests were successful
322
531
  def run_test_file(executable)
323
532
  @last_cmd = executable
324
533
  @last_out = ""
325
534
  @last_err = ""
326
- Host.run_and_output(executable.to_s.shellescape)
535
+ ret = Host.run_and_output(executable.to_s.shellescape)
536
+
537
+ # print any stack traces found during a failure
538
+ print_stack_dump(executable) unless ret
539
+
540
+ ret
327
541
  end
328
542
 
329
543
  end