bee 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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