arduino_ci 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,186 @@
1
+ require 'find'
2
+ require "arduino_ci/host"
3
+
4
+ HPP_EXTENSIONS = [".hpp", ".hh", ".h", ".hxx", ".h++"].freeze
5
+ CPP_EXTENSIONS = [".cpp", ".cc", ".c", ".cxx", ".c++"].freeze
6
+ ARDUINO_HEADER_DIR = File.expand_path("../../../cpp/arduino", __FILE__)
7
+ UNITTEST_HEADER_DIR = File.expand_path("../../../cpp/unittest", __FILE__)
8
+
9
+ module ArduinoCI
10
+
11
+ # Information about an Arduino CPP library, specifically for compilation purposes
12
+ class CppLibrary
13
+
14
+ # @return [String] The path to the library being tested
15
+ attr_reader :base_dir
16
+
17
+ # @return [Array<String>] The set of artifacts created by this class (note: incomplete!)
18
+ attr_reader :artifacts
19
+
20
+ # @return [String] STDERR from the last command
21
+ attr_reader :last_err
22
+
23
+ # @return [String] STDOUT from the last command
24
+ attr_reader :last_out
25
+
26
+ # @return [String] the last command
27
+ attr_reader :last_cmd
28
+
29
+ # @param base_dir [String] The path to the library being tested
30
+ def initialize(base_dir)
31
+ @base_dir = File.expand_path(base_dir)
32
+ @artifacts = []
33
+ @last_err = ""
34
+ @last_out = ""
35
+ @last_msg = ""
36
+ end
37
+
38
+ # Get a list of all CPP source files in a directory and its subdirectories
39
+ # @param some_dir [String] The directory in which to begin the search
40
+ # @return [Array<String>] The paths of the found files
41
+ def cpp_files_in(some_dir)
42
+ real = File.realpath(some_dir)
43
+ Find.find(real).select { |path| CPP_EXTENSIONS.include?(File.extname(path)) }
44
+ end
45
+
46
+ # CPP files that are part of the project library under test
47
+ # @return [Array<String>]
48
+ def cpp_files
49
+ real_tests_dir = File.realpath(tests_dir)
50
+ cpp_files_in(@base_dir).reject do |p|
51
+ next true if File.dirname(p).include?(tests_dir)
52
+ next true if File.dirname(p).include?(real_tests_dir)
53
+ end
54
+ end
55
+
56
+ # CPP files that are part of the arduino mock library we're providing
57
+ # @return [Array<String>]
58
+ def cpp_files_arduino
59
+ cpp_files_in(ARDUINO_HEADER_DIR)
60
+ end
61
+
62
+ # CPP files that are part of the unit test library we're providing
63
+ # @return [Array<String>]
64
+ def cpp_files_unittest
65
+ cpp_files_in(UNITTEST_HEADER_DIR)
66
+ end
67
+
68
+ # The directory where we expect to find unit test defintions provided by the user
69
+ # @return [String]
70
+ def tests_dir
71
+ File.join(@base_dir, "test")
72
+ end
73
+
74
+ # The files provided by the user that contain unit tests
75
+ # @return [Array<String>]
76
+ def test_files
77
+ cpp_files_in(tests_dir)
78
+ end
79
+
80
+ # Find all directories in the project library that include C++ header files
81
+ # @return [Array<String>]
82
+ def header_dirs
83
+ files = Find.find(@base_dir).select { |path| HPP_EXTENSIONS.include?(File.extname(path)) }
84
+ files.map { |path| File.dirname(path) }.uniq
85
+ end
86
+
87
+ # wrapper for the GCC command
88
+ def run_gcc(*args, **kwargs)
89
+ pipe_out, pipe_out_wr = IO.pipe
90
+ pipe_err, pipe_err_wr = IO.pipe
91
+ full_args = ["g++"] + args
92
+ @last_cmd = " $ #{full_args.join(' ')}"
93
+ our_kwargs = { out: pipe_out_wr, err: pipe_err_wr }
94
+ eventual_kwargs = our_kwargs.merge(kwargs)
95
+ success = Host.run(*full_args, **eventual_kwargs)
96
+
97
+ pipe_out_wr.close
98
+ pipe_err_wr.close
99
+ str_out = pipe_out.read
100
+ str_err = pipe_err.read
101
+ pipe_out.close
102
+ pipe_err.close
103
+ @last_err = str_err
104
+ @last_out = str_out
105
+ success
106
+ end
107
+
108
+ # GCC command line arguments for including aux libraries
109
+ # @param aux_libraries [String] The external Arduino libraries required by this project
110
+ # @return [Array<String>] The GCC command-line flags necessary to include those libraries
111
+ def include_args(aux_libraries)
112
+ places = [ARDUINO_HEADER_DIR, UNITTEST_HEADER_DIR] + header_dirs + aux_libraries
113
+ places.map { |d| "-I#{d}" }
114
+ end
115
+
116
+ # GCC command line arguments for features (e.g. -fno-weak)
117
+ # @param ci_gcc_config [Hash] The GCC config object
118
+ # @return [Array<String>] GCC command-line flags
119
+ def feature_args(ci_gcc_config)
120
+ return [] if ci_gcc_config[:features].nil?
121
+ ci_gcc_config[:features].map { |f| "-f#{f}" }
122
+ end
123
+
124
+ # GCC command line arguments for warning (e.g. -Wall)
125
+ # @param ci_gcc_config [Hash] The GCC config object
126
+ # @return [Array<String>] GCC command-line flags
127
+ def warning_args(ci_gcc_config)
128
+ return [] if ci_gcc_config[:warnings].nil?
129
+ ci_gcc_config[:features].map { |w| "-W#{w}" }
130
+ end
131
+
132
+ # GCC command line arguments for defines (e.g. -Dhave_something)
133
+ # @param ci_gcc_config [Hash] The GCC config object
134
+ # @return [Array<String>] GCC command-line flags
135
+ def define_args(ci_gcc_config)
136
+ return [] if ci_gcc_config[:defines].nil?
137
+ ci_gcc_config[:defines].map { |d| "-D#{d}" }
138
+ end
139
+
140
+ # GCC command line arguments as-is
141
+ # @param ci_gcc_config [Hash] The GCC config object
142
+ # @return [Array<String>] GCC command-line flags
143
+ def flag_args(ci_gcc_config)
144
+ return [] if ci_gcc_config[:flags].nil?
145
+ ci_gcc_config[:flags]
146
+ end
147
+
148
+ # All GCC command line args for building any unit test
149
+ # @param aux_libraries [String] The external Arduino libraries required by this project
150
+ # @param ci_gcc_config [Hash] The GCC config object
151
+ # @return [Array<String>] GCC command-line flags
152
+ def test_args(aux_libraries, ci_gcc_config)
153
+ # TODO: something with libraries?
154
+ ret = include_args(aux_libraries) + cpp_files_arduino + cpp_files_unittest + cpp_files
155
+ unless ci_gcc_config.nil?
156
+ cgc = ci_gcc_config
157
+ ret = feature_args(cgc) + warning_args(cgc) + define_args(cgc) + flag_args(cgc) + ret
158
+ end
159
+ ret
160
+ end
161
+
162
+ # build a file for running a test of the given unit test file
163
+ # @param test_file [String] The path to the file containing the unit tests
164
+ # @param aux_libraries [String] The external Arduino libraries required by this project
165
+ # @param ci_gcc_config [Hash] The GCC config object
166
+ # @return [String] path to the compiled test executable
167
+ def build_for_test_with_configuration(test_file, aux_libraries, ci_gcc_config)
168
+ base = File.basename(test_file)
169
+ executable = File.expand_path("unittest_#{base}.bin")
170
+ File.delete(executable) if File.exist?(executable)
171
+ args = ["-o", executable] + test_args(aux_libraries, ci_gcc_config) + [test_file]
172
+ return nil unless run_gcc(*args)
173
+ artifacts << executable
174
+ executable
175
+ end
176
+
177
+ # run a test file
178
+ # @param [String] the path to the test file
179
+ # @return [bool] whether all tests were successful
180
+ def run_test_file(executable)
181
+ Host.run(executable)
182
+ end
183
+
184
+ end
185
+
186
+ end
@@ -0,0 +1,187 @@
1
+ require 'arduino_ci/host'
2
+ require 'singleton'
3
+ require 'timeout'
4
+
5
+ DESIRED_DISPLAY = ":1.0".freeze
6
+
7
+ module ArduinoCI
8
+
9
+ # When arduino commands run, they need a graphical display.
10
+ # This class handles the setup of that display, if needed.
11
+ class DisplayManager
12
+ include Singleton
13
+
14
+ # @return [bool] whether the display manager is currently active
15
+ attr_reader :enabled
16
+
17
+ # @return [bool] whether to log messages to the terminal
18
+ attr_accessor :debug
19
+
20
+ def initialize
21
+ @existing = existing_display?
22
+ @enabled = false
23
+ @pid = nil
24
+ @debug = false
25
+
26
+ # pipes for input and output
27
+ @xv_pipe_out_wr = nil
28
+ @xv_pipe_err_wr = nil
29
+ @xv_pipe_out = nil
30
+ @xv_pipe_err = nil
31
+ end
32
+
33
+ # attempt to determine if the machine is running a graphical display (i.e. not Travis)
34
+ # @return [bool] whether there is already a GUI that can accept windows
35
+ def existing_display?
36
+ return true if RUBY_PLATFORM.include? "darwin"
37
+ return false if ENV["DISPLAY"].nil?
38
+ return true if ENV["DISPLAY"].include? ":"
39
+ false
40
+ end
41
+
42
+ # check whether a process is alive
43
+ # https://stackoverflow.com/a/32513298/2063546
44
+ # @param pid [Int] the process ID
45
+ # @return [bool]
46
+ def alive?(pid)
47
+ Process.kill(0, pid)
48
+ true
49
+ rescue
50
+ false
51
+ end
52
+
53
+ # check whether an X server is taking connections
54
+ # @param display [String] the display variable as it would be specified in the environment
55
+ # @return [bool]
56
+ def xserver_exist?(display)
57
+ system({ "DISPLAY" => display }, "xdpyinfo", out: File::NULL, err: File::NULL)
58
+ end
59
+
60
+ # wait for the xvfb command to launch
61
+ # @param display [String] the value of the DISPLAY env var
62
+ # @param pid [Int] the process of Xvfb
63
+ # @param timeout [Int] the timeout in seconds
64
+ # @return [Bool] whether we detected a launch
65
+ def xvfb_launched?(display, pid, timeout)
66
+ Timeout.timeout(timeout) do
67
+ loop do
68
+ unless alive? pid
69
+ puts "Xvfb process has died"
70
+ return false
71
+ end
72
+ x = xserver_exist? display
73
+ puts "xdpyinfo reports X server status as #{x}" if debug
74
+ return true if x
75
+ sleep(0.1)
76
+ end
77
+ end
78
+ rescue Timeout::Error
79
+ false
80
+ end
81
+
82
+ # enable a virtual display
83
+ def enable
84
+ if @existing
85
+ puts "DisplayManager enable: no-op for what appears to be an existing display" if debug
86
+ @enabled = true
87
+ return
88
+ end
89
+
90
+ return unless @pid.nil? # TODO: disable first?
91
+ @xv_pipe_out.close unless @xv_pipe_out.nil?
92
+ @xv_pipe_err.close unless @xv_pipe_err.nil?
93
+
94
+ # open Xvfb
95
+ xvfb_cmd = [
96
+ "Xvfb",
97
+ "+extension", "RANDR",
98
+ ":1",
99
+ "-ac",
100
+ "-screen", "0",
101
+ "1280x1024x16",
102
+ ]
103
+ puts "Xvfb launching" if debug
104
+
105
+ @xv_pipe_out, @xv_pipe_out_wr = IO.pipe
106
+ @xv_pipe_err, @xv_pipe_err_wr = IO.pipe
107
+ pipe = IO.popen(xvfb_cmd, stdout: @xv_pipe_out_wr, err: @xv_pipe_err_wr)
108
+ @pid = pipe.pid
109
+ @enabled = xvfb_launched?(DESIRED_DISPLAY, @pid, 30)
110
+ end
111
+
112
+ # disable the virtual display
113
+ def disable
114
+ if @existing
115
+ puts "DisplayManager disable: no-op for what appears to be an existing display" if debug
116
+ return @enabled = false
117
+ end
118
+
119
+ return @enabled = false if @pid.nil?
120
+
121
+ # https://www.whatastruggle.com/timeout-a-subprocess-in-ruby
122
+ begin
123
+ Timeout.timeout(30) do
124
+ Process.kill("TERM", @pid)
125
+ puts "Xvfb TERMed" if debug
126
+ end
127
+ rescue Timeout::Error
128
+ Process.kill(9, @pid)
129
+ puts "Xvfb KILLed" if debug
130
+ ensure
131
+ Process.wait @pid
132
+ @enabled = false
133
+ @pid = nil
134
+
135
+ @xv_pipe_out_wr.close
136
+ @xv_pipe_err_wr.close
137
+ end
138
+ end
139
+
140
+ # Enable a virtual display for the duration of the given block
141
+ # @yield [environment] The code to execute within the display environment
142
+ # @yieldparam [Hash] the environment variables relating to the display
143
+ def with_display
144
+ was_enabled = @enabled
145
+ enable unless was_enabled
146
+ begin
147
+ yield environment
148
+ ensure
149
+ disable unless was_enabled
150
+ end
151
+ end
152
+
153
+ # run a command in a display
154
+ # @return [bool]
155
+ def run(*args, **kwargs)
156
+ ret = false
157
+ # do some work to extract & merge environment variables if they exist
158
+ has_env = !args.empty? && args[0].class == Hash
159
+ with_display do |env_vars|
160
+ env_vars = {} if env_vars.nil?
161
+ env_vars.merge!(args[0]) if has_env
162
+ actual_args = has_env ? args[1..-1] : args # need to shift over if we extracted args
163
+ full_cmd = env_vars.empty? ? actual_args : [env_vars] + actual_args
164
+ ret = Host.run(*full_cmd, **kwargs)
165
+ end
166
+ ret
167
+ end
168
+
169
+ # run a command in a display with no output
170
+ # @return [bool]
171
+ def run_silent(*args)
172
+ run(*args, out: File::NULL, err: File::NULL)
173
+ end
174
+
175
+ # @return [Hash] the environment variables for the display
176
+ def environment
177
+ return nil unless @existing || @enabled
178
+ return { "EXISTING_DISPLAY" => "YES" } if @existing
179
+ { "DISPLAY" => DESIRED_DISPLAY }
180
+ end
181
+
182
+ # On finalize, ensure child process is ended
183
+ def self.finalize
184
+ disable
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,36 @@
1
+ require 'os'
2
+
3
+ module ArduinoCI
4
+
5
+ # Tools for interacting with the host machine
6
+ class Host
7
+ # Cross-platform way of finding an executable in the $PATH.
8
+ # via https://stackoverflow.com/a/5471032/2063546
9
+ # which('ruby') #=> /usr/bin/ruby
10
+ # @param cmd [String] the command to search for
11
+ # @return [String] the full path to the command if it exists
12
+ def self.which(cmd)
13
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
14
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
15
+ exts.each do |ext|
16
+ exe = File.join(path, "#{cmd}#{ext}")
17
+ return exe if File.executable?(exe) && !File.directory?(exe)
18
+ end
19
+ end
20
+ nil
21
+ end
22
+
23
+ # run a command in a display
24
+ def self.run(*args, **kwargs)
25
+ system(*args, **kwargs)
26
+ end
27
+
28
+ # return [Symbol] the operating system of the host
29
+ def self.os
30
+ return :osx if OS.osx?
31
+ return :linux if OS.linux?
32
+ return :windows if OS.windows?
33
+ end
34
+
35
+ end
36
+ end
@@ -1,3 +1,3 @@
1
1
  module ArduinoCI
2
- VERSION = "0.0.1".freeze
2
+ VERSION = "0.1.0".freeze
3
3
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arduino_ci
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ian Katz
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-01-10 00:00:00.000000000 Z
11
+ date: 2018-01-25 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: os
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -44,50 +58,49 @@ dependencies:
44
58
  requirements:
45
59
  - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: '0'
48
- - - ">="
49
- - !ruby/object:Gem::Version
50
- version: 0.46.0
61
+ version: 0.49.0
51
62
  type: :development
52
63
  prerelease: false
53
64
  version_requirements: !ruby/object:Gem::Requirement
54
65
  requirements:
55
66
  - - "~>"
56
67
  - !ruby/object:Gem::Version
57
- version: '0'
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: 0.46.0
68
+ version: 0.49.0
61
69
  - !ruby/object:Gem::Dependency
62
70
  name: yard
63
71
  requirement: !ruby/object:Gem::Requirement
64
72
  requirements:
65
73
  - - "~>"
66
74
  - !ruby/object:Gem::Version
67
- version: '0.8'
68
- - - ">="
69
- - !ruby/object:Gem::Version
70
- version: '0.8'
75
+ version: 0.9.11
71
76
  type: :development
72
77
  prerelease: false
73
78
  version_requirements: !ruby/object:Gem::Requirement
74
79
  requirements:
75
80
  - - "~>"
76
81
  - !ruby/object:Gem::Version
77
- version: '0.8'
78
- - - ">="
79
- - !ruby/object:Gem::Version
80
- version: '0.8'
82
+ version: 0.9.11
81
83
  description: ''
82
84
  email:
83
85
  - ifreecarve@gmail.com
84
- executables: []
86
+ executables:
87
+ - arduino_ci_remote.rb
85
88
  extensions: []
86
89
  extra_rdoc_files: []
87
90
  files:
88
91
  - ".yardopts"
89
92
  - README.md
93
+ - exe/arduino_ci_remote.rb
90
94
  - lib/arduino_ci.rb
95
+ - lib/arduino_ci/arduino_cmd.rb
96
+ - lib/arduino_ci/arduino_cmd_linux.rb
97
+ - lib/arduino_ci/arduino_cmd_linux_builder.rb
98
+ - lib/arduino_ci/arduino_cmd_osx.rb
99
+ - lib/arduino_ci/arduino_installation.rb
100
+ - lib/arduino_ci/ci_config.rb
101
+ - lib/arduino_ci/cpp_library.rb
102
+ - lib/arduino_ci/display_manager.rb
103
+ - lib/arduino_ci/host.rb
91
104
  - lib/arduino_ci/version.rb
92
105
  homepage: http://github.com/ifreecarve/arduino_ci
93
106
  licenses: