bee 0.3.1 → 0.4.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.
@@ -0,0 +1,230 @@
1
+ # Copyright 2006-2007 Michel Casabianca <michel.casabianca@gmail.com>
2
+ # 2006 Avi Bryant
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ module Bee
18
+
19
+ module Util
20
+
21
+ # Get line length calling IOCTL. Return nil if call failed.
22
+ # FIXME: doesn't work on MacOSX.
23
+ def self.term_width
24
+ begin
25
+ tiocgwinsz = 0x5413
26
+ string = [0, 0, 0, 0].pack('SSSS')
27
+ if $stdin.ioctl(tiocgwinsz, string) >= 0 then
28
+ rows, cols, xpixels, ypixels = string.unpack('SSSS')
29
+ return cols
30
+ end
31
+ rescue
32
+ return nil
33
+ end
34
+ end
35
+
36
+ # Looks recursively up in file system for a file.
37
+ # - file: file name to look for.
38
+ # Return: found file or raises an exception if file was not found.
39
+ def self.find(file)
40
+ return file if File.exists?(file)
41
+ raise "File not found" if File.identical?(File.dirname(file), '/')
42
+ file = File.join('..', file)
43
+ find(file)
44
+ end
45
+
46
+ # Default package name.
47
+ DEFAULT_PACKAGE = 'default'
48
+
49
+ # Parse packaged name and return package and name.
50
+ # - packaged: packaged name (such as 'foo.bar').
51
+ # Return: package ('foo') and name ('bar').
52
+ def self.get_package_name(packaged)
53
+ if packaged =~ /\./
54
+ package, name = packaged.split('.')
55
+ else
56
+ package, name = DEFAULT_PACKAGE, packaged
57
+ end
58
+ return package, name
59
+ end
60
+
61
+ # Class that holds information about a given method.
62
+ class MethodInfo
63
+
64
+ attr_accessor :source, :comment, :defn, :params
65
+
66
+ def initialize(file, lineno)
67
+ lines = file_cache(file)
68
+ @source = match_tabs(lines, lineno, "def")
69
+ @comment = preceding_comment(lines, lineno)
70
+ @defn = lines[lineno].strip.gsub(/^def\W+(.*?\W*\((.*?)\))/){$1}
71
+ @params = $2 ? $2.split(',') : nil
72
+ end
73
+
74
+ private
75
+
76
+ @@file_cache = {}
77
+
78
+ def file_cache(file)
79
+ unless lines = @@file_cache[file]
80
+ @@file_cache[file] = lines = File.new(file).readlines
81
+ end
82
+ lines
83
+ end
84
+
85
+ def match_tabs(lines, i, keyword)
86
+ lines[i] =~ /(\W*)((#{keyword}(.*;\W*end)?)|(.*))/
87
+ return $2 if $4 or $5
88
+ tabs = $1
89
+ result = ""
90
+ lines[i..-1].each do |line|
91
+ result << line.gsub(/^#{tabs}(.*)/) { $1}
92
+ return result if $1 =~ /^end/
93
+ end
94
+ end
95
+
96
+ def preceding_comment(lines, i)
97
+ result = []
98
+ i = i-1
99
+ i = i-1 while lines[i] =~ /^\W*$/
100
+ if lines[i] =~ /^=end/
101
+ i = i-1
102
+ until lines[i] =~ /^=begin/
103
+ result.unshift lines[i]
104
+ i = i-1
105
+ end
106
+ else
107
+ while lines[i] =~ /^\W*#(.*)/
108
+ result.unshift $1[1..-1]
109
+ i = i-1
110
+ end
111
+ end
112
+ result.join("\n")
113
+ end
114
+
115
+ end
116
+
117
+ # This abstract class provides information about its methods.
118
+ class MethodInfoBase
119
+
120
+ @@minfo = {}
121
+
122
+ def self.method_info(method)
123
+ @@minfo[method.to_s]
124
+ end
125
+
126
+ private
127
+
128
+ def self.method_added(method)
129
+ super if defined? super
130
+ last = caller[0]
131
+ file = last[0, last.rindex(':')]
132
+ lineno = last[last.rindex(':')+1, last.length]
133
+ @@minfo[method.to_s] = MethodInfo.new(file, lineno.to_i - 1)
134
+ end
135
+
136
+ end
137
+
138
+ # Error raised on a user error. This error should be raised to interrupt
139
+ # the build with a message on console but with no stack trace (that should
140
+ # be displayed on an internal error only). Include BuildErrorMixin to get
141
+ # a convenient way to raise such an error.
142
+ class BuildError < RuntimeError
143
+ end
144
+
145
+ # Build error mixin provides error() function to raise a BuildError.
146
+ # Use this function to interrupt the build on a user error (bad YAML
147
+ # syntax, error running a task and so on). This will result in an
148
+ # error message on the console, with no stack trace.
149
+ module BuildErrorMixin
150
+
151
+ # Convenient method to raise a BuildError.
152
+ # - message: error message.
153
+ def error(message)
154
+ raise BuildError.new(message)
155
+ end
156
+
157
+ end
158
+
159
+ # Mixin that provides a way to check a hash entries using a description
160
+ # that associates hash keys with a :mandatory or :optional symbol. Other
161
+ # keys are not allowed.
162
+ module HashCheckerMixin
163
+
164
+ include BuildErrorMixin
165
+
166
+ # Check that all mandatory keys are in the hash and all keys in the
167
+ # hash are in description.
168
+ # - hash: hash to check.
169
+ # - description: hash keys description.
170
+ def check_hash(hash, description)
171
+ # check for mandatory keys
172
+ for key in description.keys
173
+ case description[key]
174
+ when :mandatory
175
+ error "Missing mandatory key '#{key}'" if not hash[key]
176
+ when :optional
177
+ else
178
+ error "Unknown symbol '#{description[key]}'"
179
+ end
180
+ end
181
+ # look for unknown keys in hash
182
+ for key in hash.keys
183
+ error "Unknown key '#{key}'" if not description.keys.member?(key)
184
+ end
185
+ end
186
+
187
+ end
188
+
189
+ # Module that includes a mixin that provides file selection using include
190
+ # and exclude globs.
191
+ module FileSelector
192
+
193
+ include BuildErrorMixin
194
+
195
+ # Select files with globs to include and exclude.
196
+ # - include: glob (or list of globs) for files to include.
197
+ # - exclude: glob (or list of globs) for files to exclude.
198
+ # Return: a list of files as strings.
199
+ def select_files(includes, excludes)
200
+ return [] if !includes
201
+ error "includes can't be nil" if not includes
202
+ error "includes must be a glob or a list of globs" unless
203
+ includes.kind_of?(String) or includes.kind_of?(Array)
204
+ error "excludes must be a glob or a list of globs" unless
205
+ !excludes or excludes.kind_of?(String) or excludes.kind_of?(Array)
206
+ included = []
207
+ for include in includes
208
+ error "includes must be a glob or a list of globs" unless
209
+ include.kind_of?(String)
210
+ included += Dir.glob(include)
211
+ end
212
+ if excludes
213
+ excluded = []
214
+ for exclude in excludes
215
+ error "excludes must be a glob or a list of globs" unless
216
+ exclude.kind_of?(String)
217
+ excluded += Dir.glob(exclude)
218
+ end
219
+ included = included.select do |file|
220
+ !excluded.member?(file)
221
+ end
222
+ end
223
+ return included
224
+ end
225
+
226
+ end
227
+
228
+ end
229
+
230
+ end
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Copyright 2006-2007 Michel Casabianca <michel.casabianca@gmail.com>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ $:.unshift(File.join(File.dirname(__FILE__)))
18
+ require 'tmp_test_case'
19
+ require 'test_build_listener'
20
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
21
+ require 'bee'
22
+
23
+ # Test case for console formatter.
24
+ class TestBeeBuild < TmpTestCase
25
+
26
+ def test_load
27
+ # try loading a build file that doesn't exist
28
+ begin
29
+ Bee::Build::load('foo')
30
+ flunk "Build file should not have been found!"
31
+ rescue Bee::Util::BuildError
32
+ expected = "Build file 'foo' not found"
33
+ actual = $!.message
34
+ assert_equal(expected, actual)
35
+ end
36
+ # try loading a build file that is a directory
37
+ begin
38
+ Bee::Build::load('.')
39
+ flunk "Build file may not be a directory!"
40
+ rescue Bee::Util::BuildError
41
+ expected = "Build file '.' is not a file"
42
+ actual = $!.message
43
+ assert_equal(expected, actual)
44
+ end
45
+ # try running a build that should never fail
46
+ build_file = File.join(@tmp_dir, 'build.yml')
47
+ begin
48
+ source = '- target: test
49
+ script:
50
+ - print: "Hello World!"'
51
+ File.open(build_file, 'w') {|file| file.write(source)}
52
+ listener = TestBuildListener.new
53
+ build = Bee::Build::load(build_file)
54
+ build.run('', listener)
55
+ ensure
56
+ File.delete(build_file)
57
+ end
58
+ # try loading a build file with YAML syntax error
59
+ begin
60
+ source =
61
+ '- target: test
62
+ script: \"test\"'
63
+ File.open(build_file, 'w') {|file| file.write(source)}
64
+ build = Bee::Build::load(build_file)
65
+ flunk "Should fail to parse YAML file!"
66
+ rescue Bee::Util::BuildError
67
+ expected = "YAML syntax error in build file: syntax error on line 1, " +
68
+ "col 10: ` script: \\\"test\\\"'"
69
+ actual = $!.message
70
+ assert_equal(expected, actual)
71
+ ensure
72
+ File.delete(build_file)
73
+ end
74
+ # try loading a build file that is a YAML string
75
+ begin
76
+ source = 'foo'
77
+ File.open(build_file, 'w') {|file| file.write(source)}
78
+ build = Bee::Build::load(build_file)
79
+ flunk "Build object must be a list!"
80
+ rescue Bee::Util::BuildError
81
+ expected = "Build must be a list"
82
+ actual = $!.message
83
+ assert_equal(expected, actual)
84
+ ensure
85
+ File.delete(build_file)
86
+ end
87
+ end
88
+
89
+ def test_initialize
90
+ # try loading a build object (resulting from loading YAML file) that is not
91
+ # a list
92
+ begin
93
+ Bee::Build.new(Object.new, nil)
94
+ flunk "Build object must be a list!"
95
+ rescue Bee::Util::BuildError
96
+ expected = "Build must be a list"
97
+ actual = $!.message
98
+ assert_equal(expected, actual)
99
+ end
100
+ # try loading a build object with two build info entries
101
+ object = [{'build' => 'test'}, {'build' => 'test'}]
102
+ begin
103
+ Bee::Build.new(object, nil)
104
+ flunk "Build can't have duplicate info entries!"
105
+ rescue Bee::Util::BuildError
106
+ expected = "Duplicate build info"
107
+ actual = $!.message
108
+ assert_equal(expected, actual)
109
+ end
110
+ # try loading a build object defining a context
111
+ context_file = 'context.rb'
112
+ context_path = File.join(@tmp_dir, context_file)
113
+ context_source = 'def test
114
+ set_property(:tested, true)
115
+ end'
116
+ File.open(context_path, 'w') {|file| file.write(context_source)}
117
+ object = [{'build' => 'test', 'context' => context_file},
118
+ {'target' => 'test', 'script' => [{'rb' => 'test'}]}]
119
+ listener = TestBuildListener.new
120
+ build = Bee::Build.new(object, File.join(@tmp_dir, 'build.yml'))
121
+ build.run('', listener)
122
+ assert(build.context.get_property(:tested))
123
+ assert(listener.started)
124
+ assert(listener.finished)
125
+ assert(!listener.errors)
126
+ # try loading a buggy context
127
+ context_file = 'context.rb'
128
+ context_path = File.join(@tmp_dir, context_file)
129
+ context_source = 'def test end'
130
+ File.open(context_path, 'w') {|file| file.write(context_source)}
131
+ object = [{'build' => 'test', 'context' => context_file},
132
+ {'target' => 'test', 'script' => [{'rb' => 'test'}]}]
133
+ listener = TestBuildListener.new
134
+ begin
135
+ build = Bee::Build.new(object, File.join(@tmp_dir, 'build.yml'))
136
+ flunk "Should have raised a BuildError"
137
+ rescue Bee::Util::BuildError
138
+ assert_match(/Error loading context 'context.rb': compile error/,
139
+ $!.message)
140
+ end
141
+ # load a build defining properties
142
+ object = [{'properties' => [{'foo' => 'bar'}]}]
143
+ build = Bee::Build.new(object, File.join(@tmp_dir, 'build.yml'))
144
+ assert_equal(build.context.get_property(:foo), 'bar')
145
+ # load a build defining duplicate properties
146
+ object = [{'properties' => [{'foo' => 'bar'}, {'foo' => 'bar'}]}]
147
+ begin
148
+ build = Bee::Build.new(object, File.join(@tmp_dir, 'build.yml'))
149
+ rescue Bee::Util::BuildError
150
+ assert_equal("Property 'foo' was already defined", $!.message)
151
+ end
152
+ # load a build with unknown entry
153
+ object = [{'foo' => 'bar'}]
154
+ begin
155
+ build = Bee::Build.new(object, File.join(@tmp_dir, 'build.yml'))
156
+ rescue Bee::Util::BuildError
157
+ assert_match("Unknown entry:", $!.message)
158
+ end
159
+ # try running a buggy build with a listener
160
+ object = [{'target' => 'test', 'script' => [{'foo' => 'bar'}]}]
161
+ listener = TestBuildListener.new
162
+ build = Bee::Build.new(object, File.join(@tmp_dir, 'build.yml'))
163
+ build.run('', listener)
164
+ assert(listener.started)
165
+ assert(!listener.finished)
166
+ assert(listener.errors)
167
+ # try running a buggy build without a listener
168
+ object = [{'target' => 'test', 'script' => [{'foo' => 'bar'}]}]
169
+ build = Bee::Build.new(object, File.join(@tmp_dir, 'build.yml'))
170
+ begin
171
+ build.run('', nil)
172
+ rescue Bee::Util::BuildError
173
+ assert_equal("Task 'foo' not found in package 'default'", $!.message)
174
+ end
175
+ # try running a build with dependencies
176
+ target1 = {'target' => 'test1',
177
+ 'script' => [{'rb' => 'set_property(:foo, "bar")'}]}
178
+ target2 = {'target' => 'test2', 'depends' => 'test1'}
179
+ object = [target2, target1]
180
+ listener = TestBuildListener.new
181
+ build = Bee::Build.new(object, File.join(@tmp_dir, 'build.yml'))
182
+ build.run('', listener)
183
+ assert(listener.started)
184
+ assert(listener.finished)
185
+ assert(!listener.errors)
186
+ assert_equal(['test1', 'test2'],
187
+ listener.targets.collect {|target| target.name})
188
+ # run a task with two keys
189
+ object = [{'target' => 'test',
190
+ 'script' => [{'foo' => 'bar', 'spam' => 'eggs'}]}]
191
+ build = Bee::Build.new(object, File.join(@tmp_dir, 'build.yml'))
192
+ begin
193
+ build.run('', nil)
194
+ rescue Bee::Util::BuildError
195
+ assert_equal("A task entry must be a Hash with a single key", $!.message)
196
+ end
197
+ # run a buggy Ruby script
198
+ object = [{'target' => 'test', 'script' => [{'rb' => 'bar'}]}]
199
+ build = Bee::Build.new(object, File.join(@tmp_dir, 'build.yml'))
200
+ begin
201
+ build.run('', nil)
202
+ rescue Bee::Util::BuildError
203
+ assert_match(/Error running Ruby script/, $!.message)
204
+ end
205
+ # run a buggy Ruby script (with bad property reference)
206
+ object = [{'target' => 'test', 'script' => [{'print' => :bar}]}]
207
+ build = Bee::Build.new(object, File.join(@tmp_dir, 'build.yml'))
208
+ begin
209
+ build.run('', nil)
210
+ rescue Bee::Util::BuildError
211
+ assert_match(/Property 'bar' was not set/,
212
+ $!.message)
213
+ end
214
+ end
215
+
216
+ end
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Copyright 2006-2007 Michel Casabianca <michel.casabianca@gmail.com>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'test/unit'
18
+ require 'stringio'
19
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
20
+ require 'bee_console'
21
+ $:.unshift(File.dirname(__FILE__))
22
+ require 'tmp_test_case'
23
+
24
+ # Test context.
25
+ class TestBeeConsole < TmpTestCase
26
+
27
+ def test_parse_command_line
28
+ ARGV.replace(['-h', '-b', '-k', 'foo', '-t', '-v', '-s', 'bar', '-f', 'spam', 'eggs'])
29
+ actual = Bee::Console.parse_command_line
30
+ expected = [true, true, true, 'foo', true, true, 'bar', 'spam', false, ['eggs']]
31
+ assert_equal(expected, actual)
32
+ end
33
+
34
+ def test_start_command_line
35
+ stdout = $stdout
36
+ stderr = $stderr
37
+ output = StringIO.new
38
+ $stdout = $stderr = output
39
+ begin
40
+ # nominal case starting a build from command line
41
+ build_file = 'test.yml'
42
+ build_path = File.join(@tmp_dir, build_file)
43
+ source = '- target: test
44
+ script: "cd ."'
45
+ File.open(build_path, 'w') {|file| file.write(source)}
46
+ ARGV.replace(['-f', build_path])
47
+ Bee::Console.start_command_line
48
+ # error parsing command line arguments
49
+ ARGV.replace(['-z'])
50
+ begin
51
+ Bee::Console.start_command_line
52
+ flunk "Command line parsing should have failed"
53
+ rescue SystemExit => e
54
+ assert_equal(Bee::Console::EXIT_PARSING_CMDLINE, e.status)
55
+ end
56
+ # help command
57
+ ARGV.replace(['-h'])
58
+ Bee::Console.start_command_line
59
+ # build help
60
+ build_file = 'test.yml'
61
+ build_path = File.join(@tmp_dir, build_file)
62
+ source = '- target: test
63
+ script: "cd ."'
64
+ File.open(build_path, 'w') {|file| file.write(source)}
65
+ ARGV.replace(['-f', build_path, '-b'])
66
+ Bee::Console.start_command_line
67
+ # task help
68
+ ARGV.replace(['-k', 'print'])
69
+ Bee::Console.start_command_line
70
+ # task help
71
+ ARGV.replace(['-k', '?'])
72
+ Bee::Console.start_command_line
73
+ # error task help
74
+ ARGV.replace(['-k', 'foo'])
75
+ begin
76
+ Bee::Console.start_command_line
77
+ flunk "Should have failed"
78
+ rescue Exception
79
+ end
80
+ # template generation
81
+ build_file = 'test.yml'
82
+ build_path = File.join(@tmp_dir, build_file)
83
+ File.delete(build_path)
84
+ build_path = File.join(build_path)
85
+ ARGV.replace(['-f', build_path, '-t'])
86
+ Bee::Console.start_command_line
87
+ # template generation with a build file that already exists
88
+ begin
89
+ build_file = 'test.yml'
90
+ build_path = File.join(@tmp_dir, build_file)
91
+ build_path = File.join(build_path)
92
+ ARGV.replace(['-f', build_path, '-t'])
93
+ Bee::Console.start_command_line
94
+ flunk "Template generation should have failed"
95
+ rescue SystemExit => e
96
+ assert_equal(Bee::Console::EXIT_BUILD_ERROR, e.status)
97
+ end
98
+ # test unknown internal error
99
+ # TODO
100
+ ensure
101
+ $stdout = stdout
102
+ $stderr = stderr
103
+ end
104
+ end
105
+
106
+ end