arduino_ci 0.1.21 → 1.0.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 (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