mortar 0.1.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 (61) hide show
  1. data/README.md +36 -0
  2. data/bin/mortar +13 -0
  3. data/lib/mortar.rb +23 -0
  4. data/lib/mortar/auth.rb +312 -0
  5. data/lib/mortar/cli.rb +54 -0
  6. data/lib/mortar/command.rb +267 -0
  7. data/lib/mortar/command/auth.rb +96 -0
  8. data/lib/mortar/command/base.rb +319 -0
  9. data/lib/mortar/command/clusters.rb +41 -0
  10. data/lib/mortar/command/describe.rb +97 -0
  11. data/lib/mortar/command/generate.rb +121 -0
  12. data/lib/mortar/command/help.rb +166 -0
  13. data/lib/mortar/command/illustrate.rb +97 -0
  14. data/lib/mortar/command/jobs.rb +174 -0
  15. data/lib/mortar/command/pigscripts.rb +45 -0
  16. data/lib/mortar/command/projects.rb +128 -0
  17. data/lib/mortar/command/validate.rb +94 -0
  18. data/lib/mortar/command/version.rb +42 -0
  19. data/lib/mortar/errors.rb +24 -0
  20. data/lib/mortar/generators/generator_base.rb +107 -0
  21. data/lib/mortar/generators/macro_generator.rb +37 -0
  22. data/lib/mortar/generators/pigscript_generator.rb +40 -0
  23. data/lib/mortar/generators/project_generator.rb +67 -0
  24. data/lib/mortar/generators/udf_generator.rb +28 -0
  25. data/lib/mortar/git.rb +233 -0
  26. data/lib/mortar/helpers.rb +488 -0
  27. data/lib/mortar/project.rb +156 -0
  28. data/lib/mortar/snapshot.rb +39 -0
  29. data/lib/mortar/templates/macro/macro.pig +14 -0
  30. data/lib/mortar/templates/pigscript/pigscript.pig +38 -0
  31. data/lib/mortar/templates/pigscript/python_udf.py +13 -0
  32. data/lib/mortar/templates/project/Gemfile +3 -0
  33. data/lib/mortar/templates/project/README.md +8 -0
  34. data/lib/mortar/templates/project/gitignore +4 -0
  35. data/lib/mortar/templates/project/macros/gitkeep +0 -0
  36. data/lib/mortar/templates/project/pigscripts/pigscript.pig +35 -0
  37. data/lib/mortar/templates/project/udfs/python/python_udf.py +13 -0
  38. data/lib/mortar/templates/udf/python_udf.py +13 -0
  39. data/lib/mortar/version.rb +20 -0
  40. data/lib/vendor/mortar/okjson.rb +598 -0
  41. data/lib/vendor/mortar/uuid.rb +312 -0
  42. data/spec/mortar/auth_spec.rb +156 -0
  43. data/spec/mortar/command/auth_spec.rb +46 -0
  44. data/spec/mortar/command/base_spec.rb +82 -0
  45. data/spec/mortar/command/clusters_spec.rb +61 -0
  46. data/spec/mortar/command/describe_spec.rb +135 -0
  47. data/spec/mortar/command/generate_spec.rb +139 -0
  48. data/spec/mortar/command/illustrate_spec.rb +140 -0
  49. data/spec/mortar/command/jobs_spec.rb +364 -0
  50. data/spec/mortar/command/pigscripts_spec.rb +70 -0
  51. data/spec/mortar/command/projects_spec.rb +165 -0
  52. data/spec/mortar/command/validate_spec.rb +119 -0
  53. data/spec/mortar/command_spec.rb +122 -0
  54. data/spec/mortar/git_spec.rb +278 -0
  55. data/spec/mortar/helpers_spec.rb +82 -0
  56. data/spec/mortar/project_spec.rb +76 -0
  57. data/spec/mortar/snapshot_spec.rb +46 -0
  58. data/spec/spec.opts +1 -0
  59. data/spec/spec_helper.rb +278 -0
  60. data/spec/support/display_message_matcher.rb +68 -0
  61. metadata +259 -0
@@ -0,0 +1,46 @@
1
+ #
2
+ # Copyright 2012 Mortar Data Inc.
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
+ # Portions of this code from heroku (https://github.com/heroku/heroku/) Copyright Heroku 2008 - 2012,
17
+ # used under an MIT license (https://github.com/heroku/heroku/blob/master/LICENSE).
18
+ #
19
+
20
+ require "spec_helper"
21
+ require "mortar/command/auth"
22
+
23
+ describe Mortar::Command::Auth do
24
+
25
+ describe "auth:key" do
26
+ it "displays the user's api key" do
27
+ mock(Mortar::Auth).password {"foo_api_key"}
28
+ stderr, stdout = execute("auth:key")
29
+ stderr.should == ""
30
+ stdout.should == <<-STDOUT
31
+ foo_api_key
32
+ STDOUT
33
+ end
34
+ end
35
+
36
+ describe "auth:whoami" do
37
+ it "displays the user's email address" do
38
+ mock(Mortar::Auth).user {"sam@mortardata.com"}
39
+ stderr, stdout = execute("auth:whoami")
40
+ stderr.should == ""
41
+ stdout.should == <<-STDOUT
42
+ sam@mortardata.com
43
+ STDOUT
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,82 @@
1
+ #
2
+ # Copyright 2012 Mortar Data Inc.
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
+ # Portions of this code from heroku (https://github.com/heroku/heroku/) Copyright Heroku 2008 - 2012,
17
+ # used under an MIT license (https://github.com/heroku/heroku/blob/master/LICENSE).
18
+ #
19
+
20
+ require "spec_helper"
21
+ require "mortar/command/base"
22
+
23
+ module Mortar::Command
24
+ describe Base do
25
+ before do
26
+ @base = Base.new
27
+ stub(@base).display
28
+ @client = Object.new
29
+ stub(@client).host {'mortar.com'}
30
+ end
31
+
32
+ context "error message context" do
33
+ it "get context for missing parameter error message" do
34
+ message = "Undefined parameter : INPUT"
35
+ @base.get_error_message_context(message).should == "Use -p, --parameter NAME=VALUE to set parameter NAME to value VALUE."
36
+ end
37
+
38
+ it "get context for unhandled error message" do
39
+ message = "special kind of error"
40
+ @base.get_error_message_context(message).should == ""
41
+ end
42
+ end
43
+
44
+ context "detecting the project" do
45
+ it "read remotes from git config" do
46
+ stub(Dir).chdir
47
+ stub(@base.git).has_dot_git? {true}
48
+ mock(@base.git).git("remote -v").returns(<<-REMOTES)
49
+ staging\tgit@github.com:mortarcode/4dbbd83cae8d5bf8a4000000_myproject-staging.git (fetch)
50
+ staging\tgit@github.com:mortarcode/4dbbd83cae8d5bf8a4000000_myproject-staging.git (push)
51
+ production\tgit@github.com:mortarcode/4dbbd83cae8d5bf8a4000000_myproject.git (fetch)
52
+ production\tgit@github.com:mortarcode/4dbbd83cae8d5bf8a4000000_myproject.git (push)
53
+ other\tgit@github.com:other.git (fetch)
54
+ other\tgit@github.com:other.git (push)
55
+ REMOTES
56
+
57
+ @mortar = Object.new
58
+ stub(@mortar).host {'mortar.com'}
59
+ stub(@base).mortar { @mortar }
60
+
61
+ # need a better way to test internal functionality
62
+ @base.git.send(:remotes, 'mortarcode').should == { 'staging' => 'myproject-staging', 'production' => 'myproject' }
63
+ end
64
+
65
+ it "gets the project from remotes when there's only one project" do
66
+ stub(@base.git).has_dot_git? {true}
67
+ stub(@base.git).remotes {{ 'mortar' => 'myproject' }}
68
+ mock(@base.git).git("config mortar.remote", false).returns("")
69
+ @base.project.name.should == 'myproject'
70
+ end
71
+
72
+ it "accepts a --remote argument to choose the project from the remote name" do
73
+ stub(@base.git).has_dot_git?.returns(true)
74
+ stub(@base.git).remotes.returns({ 'staging' => 'myproject-staging', 'production' => 'myproject' })
75
+ stub(@base).options.returns(:remote => "staging")
76
+ @base.project.name.should == 'myproject-staging'
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,61 @@
1
+ #
2
+ # Copyright 2012 Mortar Data Inc.
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
+ require 'spec_helper'
18
+ require 'mortar/command/clusters'
19
+
20
+ module Mortar::Command
21
+ describe Clusters do
22
+
23
+ before(:each) do
24
+ stub_core
25
+ end
26
+
27
+ context("index") do
28
+
29
+ it "lists running and recently started clusters" do
30
+ # stub api request
31
+ clusters = [{"cluster_id" => "50fbe5a23004292547fc2224",
32
+ "size" => 2,
33
+ "status_description" => "Running",
34
+ "start_timestamp" => "2012-08-27T21:27:15.669000+00:00",
35
+ "duration" => "2 mins"},
36
+ {"cluster_id" => "50fbe5a23004292547fc2225",
37
+ "size" => 10,
38
+ "status_description" => "Shut Down",
39
+ "start_timestamp" => "2011-08-27T21:27:15.669000+00:00",
40
+ "duration" => "20 mins"}]
41
+ mock(Mortar::Auth.api).get_clusters().returns(Excon::Response.new(:body => {"clusters" => clusters}))
42
+ stderr, stdout = execute("clusters", nil, nil)
43
+ stdout.should == <<-STDOUT
44
+ cluster_id Size (# of Nodes) Status Type Start Timestamp Elapsed Time
45
+ ------------------------ ----------------- --------- ---- -------------------------------- ------------
46
+ 50fbe5a23004292547fc2224 2 Running 2012-08-27T21:27:15.669000+00:00 2 mins
47
+ 50fbe5a23004292547fc2225 10 Shut Down 2011-08-27T21:27:15.669000+00:00 20 mins
48
+ STDOUT
49
+ end
50
+
51
+ it "handles no clusters running" do
52
+ mock(Mortar::Auth.api).get_clusters().returns(Excon::Response.new(:body => {"clusters" => []}))
53
+ stderr, stdout = execute("clusters", nil, nil)
54
+ stdout.should == <<-STDOUT
55
+ cluster_id Size (# of Nodes) Status Type Start Timestamp Elapsed Time
56
+ ---------- ----------------- ------ ---- --------------- ------------
57
+ STDOUT
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,135 @@
1
+ #
2
+ # Copyright 2012 Mortar Data Inc.
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
+ require 'spec_helper'
18
+ require 'fakefs/spec_helpers'
19
+ require 'mortar/command/describe'
20
+ require 'mortar/api/describe'
21
+ require 'launchy'
22
+
23
+ module Mortar::Command
24
+ describe Describe do
25
+
26
+ before(:each) do
27
+ stub_core
28
+ @git = Mortar::Git::Git.new
29
+ end
30
+
31
+ context("index") do
32
+
33
+ it "errors when an alias is not provided" do
34
+ with_git_initialized_project do |p|
35
+ write_file(File.join(p.pigscripts_path, "my_script.pig"))
36
+ stderr, stdout = execute("describe my_script", p)
37
+ stderr.should == <<-STDERR
38
+ ! Usage: mortar describe PIGSCRIPT ALIAS
39
+ ! Must specify PIGSCRIPT and ALIAS.
40
+ STDERR
41
+ end
42
+ end
43
+
44
+ it "errors when no remote exists in the project" do
45
+ with_git_initialized_project do |p|
46
+ @git.git('remote rm mortar')
47
+ p.remote = nil
48
+ write_file(File.join(p.pigscripts_path, "my_script.pig"))
49
+ stderr, stdout = execute("describe my_script my_alias", p, @git)
50
+ stderr.should == <<-STDERR
51
+ ! Unable to find git remote for project myproject
52
+ STDERR
53
+ end
54
+ end
55
+
56
+ it "errors when requested pigscript cannot be found" do
57
+ with_git_initialized_project do |p|
58
+ stderr, stdout = execute("describe does_not_exist my_alias", p, @git)
59
+ stderr.should == <<-STDERR
60
+ ! Unable to find pigscript does_not_exist
61
+ ! No pigscripts found
62
+ STDERR
63
+ end
64
+ end
65
+
66
+ it "requests and reports on a successful describe" do
67
+ with_git_initialized_project do |p|
68
+ # stub api requests
69
+ describe_id = "c571a8c7f76a4fd4a67c103d753e2dd5"
70
+ describe_url = "https://api.mortardata.com/describe/#{describe_id}"
71
+ parameters = ["name"=>"key", "value"=>"value" ]
72
+
73
+ mock(Mortar::Auth.api).post_describe("myproject", "my_script", "my_alias", is_a(String), :parameters => parameters) {Excon::Response.new(:body => {"describe_id" => describe_id})}
74
+ mock(Mortar::Auth.api).get_describe(describe_id, :exclude_result => true).returns(Excon::Response.new(:body => {"status_code" => Mortar::API::Describe::STATUS_QUEUED, "status_description" => "Pending"})).ordered
75
+ mock(Mortar::Auth.api).get_describe(describe_id, :exclude_result => true).returns(Excon::Response.new(:body => {"status_code" => Mortar::API::Describe::STATUS_GATEWAY_STARTING, "status_description" => "Gateway starting"})).ordered
76
+ mock(Mortar::Auth.api).get_describe(describe_id, :exclude_result => true).returns(Excon::Response.new(:body => {"status_code" => Mortar::API::Describe::STATUS_PROGRESS, "status_description" => "Starting pig"})).ordered
77
+ mock(Mortar::Auth.api).get_describe(describe_id, :exclude_result => true).returns(Excon::Response.new(:body => {"status_code" => Mortar::API::Describe::STATUS_SUCCESS, "status_description" => "Success", "web_result_url" => describe_url})).ordered
78
+
79
+ # stub launchy
80
+ mock(Launchy).open(describe_url) {Thread.new {}}
81
+
82
+ write_file(File.join(p.pigscripts_path, "my_script.pig"))
83
+ stderr, stdout = execute("describe my_script my_alias --polling_interval 0.05 -p key=value", p, @git)
84
+ stdout.should == <<-STDOUT
85
+ Taking code snapshot... done
86
+ Sending code snapshot to Mortar... done
87
+ Starting describe... done
88
+
89
+ \r\e[0KStatus: Pending... /\r\e[0KStatus: Gateway starting... -\r\e[0KStatus: Starting pig... \\\r\e[0KStatus: Success
90
+
91
+ Results available at https://api.mortardata.com/describe/c571a8c7f76a4fd4a67c103d753e2dd5
92
+ Opening web browser to show results... done
93
+ STDOUT
94
+ end
95
+ end
96
+
97
+ it "requests and reports on a failed describe" do
98
+ with_git_initialized_project do |p|
99
+ # stub api requests
100
+ describe_id = "c571a8c7f76a4fd4a67c103d753e2dd5"
101
+
102
+ error_message = "This is my error message\nWith multiple lines."
103
+ line_number = 23
104
+ column_number = 32
105
+ error_type = 'PigError'
106
+
107
+ mock(Mortar::Auth.api).post_describe("myproject", "my_script", "my_alias", is_a(String), :parameters => []) {Excon::Response.new(:body => {"describe_id" => describe_id})}
108
+ mock(Mortar::Auth.api).get_describe(describe_id, :exclude_result => true).returns(Excon::Response.new(:body => {"status_code" => Mortar::API::Describe::STATUS_QUEUED, "status_description" => "Pending"})).ordered
109
+ mock(Mortar::Auth.api).get_describe(describe_id, :exclude_result => true).returns(Excon::Response.new(:body => {"status_code" => Mortar::API::Describe::STATUS_FAILURE, "status_description" => "Failed",
110
+ "error_message" => error_message,
111
+ "line_number" => line_number,
112
+ "column_number" => column_number,
113
+ "error_type" => error_type})).ordered
114
+
115
+ write_file(File.join(p.pigscripts_path, "my_script.pig"))
116
+ stderr, stdout = execute("describe my_script my_alias --polling_interval 0.05", p, @git)
117
+ stdout.should == <<-STDOUT
118
+ Taking code snapshot... done
119
+ Sending code snapshot to Mortar... done
120
+ Starting describe... done
121
+
122
+ \r\e[0KStatus: Pending... /\r\e[0KStatus: Failed
123
+
124
+ STDOUT
125
+ stderr.should == <<-STDERR
126
+ ! Describe failed with PigError at Line 23, Column 32:
127
+ !
128
+ ! This is my error message
129
+ ! With multiple lines.
130
+ STDERR
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,139 @@
1
+ #
2
+ # Copyright 2012 Mortar Data Inc.
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
+ require "spec_helper"
18
+ require "mortar/command/generate"
19
+ require "fileutils"
20
+ require "tmpdir"
21
+
22
+ describe Mortar::Command::Generate do
23
+
24
+ before(:each) do
25
+ @tmpdir = Dir.mktmpdir
26
+ Dir.chdir(@tmpdir)
27
+ end
28
+
29
+ describe "generate:project" do
30
+ it "creates new project" do
31
+ stderr, stdout = execute("generate:project Test")
32
+ File.exists?("Test").should be_true
33
+ File.exists?("Test/macros").should be_true
34
+ File.exists?("Test/pigscripts").should be_true
35
+ File.exists?("Test/udfs").should be_true
36
+ File.exists?("Test/README.md").should be_true
37
+ File.exists?("Test/Gemfile").should be_true
38
+ #File.exists?("Test/Gemfile.lock").should be_true
39
+ File.exists?("Test/macros/.gitkeep").should be_true
40
+ File.exists?("Test/pigscripts/Test.pig").should be_true
41
+ File.exists?("Test/udfs/python/Test.py").should be_true
42
+
43
+ File.read("Test/pigscripts/Test.pig").each_line { |line| line.match(/<%.*%>/).should be_nil }
44
+ end
45
+ it "error when name isn't provided" do
46
+ stderr, stdout = execute("generate:project")
47
+ stderr.should == <<-STDERR
48
+ ! Usage: mortar new PROJECTNAME
49
+ ! Must specify PROJECTNAME.
50
+ STDERR
51
+ end
52
+ end
53
+
54
+ describe "new" do
55
+ it "create new project using alias" do
56
+ stderr, stdout = execute("new Test")
57
+ File.exists?("Test").should be_true
58
+ File.exists?("Test/macros").should be_true
59
+ File.exists?("Test/pigscripts").should be_true
60
+ File.exists?("Test/udfs").should be_true
61
+ File.exists?("Test/README.md").should be_true
62
+ File.exists?("Test/Gemfile").should be_true
63
+ #File.exists?("Test/Gemfile.lock").should be_true
64
+ File.exists?("Test/macros/.gitkeep").should be_true
65
+ File.exists?("Test/pigscripts/Test.pig").should be_true
66
+ File.exists?("Test/udfs/python/Test.py").should be_true
67
+
68
+ File.read("Test/pigscripts/Test.pig").each_line { |line| line.match(/<%.*%>/).should be_nil }
69
+ end
70
+
71
+ it "error when name isn't provided" do
72
+ stderr, stdout = execute("new")
73
+ stderr.should == <<-STDERR
74
+ ! Usage: mortar new PROJECTNAME
75
+ ! Must specify PROJECTNAME.
76
+ STDERR
77
+ end
78
+ end
79
+
80
+ describe "generate:pigscript" do
81
+ it "Generate a new pigscript in a project" do
82
+ with_blank_project do |p|
83
+ stderr, stdout = execute("generate:pigscript Oink", p)
84
+ File.exists?(File.join(p.root_path, "pigscripts/Oink.pig")).should be_true
85
+ File.read("pigscripts/Oink.pig").each_line { |line| line.match(/<%.*%>/).should be_nil }
86
+ end
87
+ end
88
+
89
+ it "error when pigscript name isn't provided" do
90
+ with_blank_project do |p|
91
+ stderr, stdout = execute("generate:pigscript")
92
+ stderr.should == <<-STDERR
93
+ ! Usage: mortar generate:pigscript SCRIPTNAME
94
+ ! Must specify SCRIPTNAME.
95
+ STDERR
96
+ end
97
+ end
98
+ end
99
+
100
+ describe "generate:python_udf" do
101
+ it "Generate a new python udf in a project" do
102
+ with_blank_project do |p|
103
+ stderr, stdout = execute("generate:python_udf slither", p)
104
+ File.exists?(File.join(p.root_path, "udfs/python/slither.py")).should be_true
105
+ File.read("udfs/python/slither.py").each_line { |line| line.match(/<%.*%>/).should be_nil }
106
+ end
107
+ end
108
+
109
+ it "error when udf name isn't provided" do
110
+ with_blank_project do |p|
111
+ stderr, stdout = execute("generate:python_udf")
112
+ stderr.should == <<-STDERR
113
+ ! Usage: mortar generate:python_udf UDFNAME
114
+ ! Must specify UDFNAME.
115
+ STDERR
116
+ end
117
+ end
118
+ end
119
+
120
+ describe "generate:macro" do
121
+ it "Generate a new macro in a project" do
122
+ with_blank_project do |p|
123
+ stderr, stdout = execute("generate:macro big_mac", p)
124
+ File.exists?(File.join(p.root_path, "macros/big_mac.pig")).should be_true
125
+ File.read("macros/big_mac.pig").each_line { |line| line.match(/<%.*%>/).should be_nil }
126
+ end
127
+ end
128
+
129
+ it "error when udf name isn't provided" do
130
+ with_blank_project do |p|
131
+ stderr, stdout = execute("generate:macro")
132
+ stderr.should == <<-STDERR
133
+ ! Usage: mortar generate:macro MACRONAME
134
+ ! Must specify MACRONAME.
135
+ STDERR
136
+ end
137
+ end
138
+ end
139
+ end