flux 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.flux ADDED
@@ -0,0 +1,9 @@
1
+ # -*- mode: yaml -*-
2
+
3
+ trackers:
4
+ adapter: pivotal_tracker
5
+ project_id: 128808
6
+ rcs:
7
+ adapter: git
8
+ workflows:
9
+ adapter: mojotech
@@ -0,0 +1,3 @@
1
+ trackers:
2
+ token: abcdef1234567890
3
+ email: david@mojotech.com
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+ gem "thor", "~> 0.14.0"
6
+ gem "pivotal-tracker", "~> 0.4.0"
7
+
8
+ # Add dependencies to develop your gem here.
9
+ # Include everything needed to run rake, tests, features, etc.
10
+ group :development do
11
+ gem "grit", "~> 2.4.0"
12
+ gem "rspec", "~> 2.0"
13
+ gem "bundler", "~> 1.0.0"
14
+ gem "jeweler", "~> 1.6.2"
15
+ gem "rcov", ">= 0"
16
+ gem "rr", "~> 1.0.0"
17
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 MojoTech
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,48 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "flux"
18
+ gem.homepage = "http://github.com/mojotech/flux"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Command line workflow manager.}
21
+ gem.email = "david@mojotech.com"
22
+ gem.authors = ["David Leal"]
23
+ # dependencies defined in Gemfile
24
+ end
25
+ Jeweler::RubygemsDotOrgTasks.new
26
+
27
+ require 'rspec/core'
28
+ require 'rspec/core/rake_task'
29
+ RSpec::Core::RakeTask.new(:spec) do |spec|
30
+ spec.pattern = FileList['spec/**/*_spec.rb']
31
+ end
32
+
33
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
34
+ spec.pattern = 'spec/**/*_spec.rb'
35
+ spec.rcov = true
36
+ end
37
+
38
+ task :default => :spec
39
+
40
+ require 'rake/rdoctask'
41
+ Rake::RDocTask.new do |rdoc|
42
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
43
+
44
+ rdoc.rdoc_dir = 'rdoc'
45
+ rdoc.title = "flux #{version}"
46
+ rdoc.rdoc_files.include('README*')
47
+ rdoc.rdoc_files.include('lib/**/*.rb')
48
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/bin/flux ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'thor/runner'
4
+
5
+ require 'flux'
6
+
7
+ class Flux::Runner < Thor::Runner
8
+ alias_method :help, :list
9
+
10
+ private
11
+
12
+ def initialize_thorfiles(whocares = nil, nevermind = nil)
13
+ # birds chirping
14
+ end
15
+ end
16
+
17
+ begin
18
+ $thor_runner = true
19
+ Flux::Runner.start(ARGV, {'environment' => Flux.setup})
20
+ rescue Flux::FluxError => e
21
+ $stderr.puts e.message
22
+ exit 1
23
+ end
data/lib/flux.rb ADDED
@@ -0,0 +1,65 @@
1
+ require 'ostruct'
2
+ require 'pathname'
3
+ require 'thor'
4
+ require 'yaml'
5
+
6
+ require 'flux/util'
7
+
8
+ module Flux
9
+ RC = '.flux'
10
+ RC_LOCAL = RC + '.local'
11
+
12
+ class FluxError < StandardError; end
13
+ class TrackerError < FluxError; end
14
+
15
+ class << self
16
+ attr_accessor :environment
17
+
18
+ def setup
19
+ self.environment = load_environment
20
+
21
+ environment.each { |k, v|
22
+ load_adapter k, v['adapter'] if v.is_a?(Hash) && v['adapter']
23
+ }
24
+ end
25
+
26
+ def find_upwards(object, start_dir)
27
+ p = Pathname(start_dir)
28
+ f = (p + object).expand_path.to_s
29
+
30
+ if File.exist?(f)
31
+ f
32
+ elsif p == p.parent
33
+ nil
34
+ else
35
+ find_upwards(p.parent)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def load_adapter(kind, adapter)
42
+ adapter_path = "flux/#{kind}/#{adapter}"
43
+
44
+ begin
45
+ require adapter_path
46
+ rescue LoadError
47
+ raise FluxError, "Could not load `#{adapter_path}'."
48
+ end
49
+ end
50
+
51
+ def load_environment
52
+ rc = find_upwards(RC, Dir.pwd) or
53
+ raise FluxError, "Could not find a '#{RC}' " <<
54
+ "file in the current filesystem hierarchy."
55
+ rc_l = File.join(File.dirname(rc), RC_LOCAL)
56
+
57
+ env = YAML.load_file(rc)
58
+ env_l = File.exist?(rc_l) ? YAML.load_file(rc_l) : {}
59
+
60
+ env_l.each { |k, v| env[k].merge!(v) if env[k].respond_to?(:merge) }
61
+
62
+ env
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,13 @@
1
+ module Flux
2
+ module PivotalTracker
3
+ module IterationExt
4
+ def current_backlog(project, options = {})
5
+ params = ::PivotalTracker.encode_options(options)
6
+ path = "/projects/#{project.id}/iterations/current_backlog#{params}"
7
+ parse(::PivotalTracker::Client.connection[path].get)
8
+ end
9
+
10
+ ::PivotalTracker::Iteration.send(:extend, self)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ require 'grit'
2
+
3
+ module Flux
4
+ module RCS
5
+ class Branches < Thor
6
+ namespace :branches
7
+
8
+ desc "current", "show the current branch"
9
+ def current
10
+ repo.head.name.tap { |h| $stdout.puts h }
11
+ end
12
+
13
+ private
14
+
15
+ def repo
16
+ @repo ||=
17
+ begin
18
+ repo = Flux.find_upwards('.git', Dir.pwd) or
19
+ raise RCSError, "Couldn't find git repo starting at #{Dir.pwd}."
20
+
21
+ Grit::Repo.new(repo)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,129 @@
1
+ require 'pivotal-tracker'
2
+ require 'flux/ext/pivotal-tracker'
3
+
4
+ module Flux
5
+ module Trackers
6
+ class PivotalTracker < Thor
7
+ include Flux::Util
8
+
9
+ namespace :stories
10
+
11
+ default_task :list
12
+
13
+ desc "list", "list stories, excluding icebox by default"
14
+ def list
15
+ list_stories pt::Iteration.
16
+ current_backlog(fake_project).map { |i| i.stories }.flatten
17
+ end
18
+
19
+ desc "finish STORY_ID", "finish the given story"
20
+ method_option :estimate, :type => :numeric, :aliases => '-e'
21
+ def finish(story_id)
22
+ update_state story_id, 'finished'
23
+ end
24
+
25
+ desc "grab STORY_ID", "assign yourself the given story"
26
+ def grab(story_id)
27
+ invoke :update, [story_id], :attributes => {:owner => me.name}
28
+ end
29
+
30
+ desc "start STORY_ID", "start the given story"
31
+ method_option :estimate, :type => :numeric, :aliases => '-e'
32
+ def start(story_id)
33
+ update_state story_id, 'started'
34
+ end
35
+
36
+ STATES_W_ESTIMATE = %w(started finished delivered accepted rejected)
37
+ STATES = %w(unscheduled unstarted) + STATES_W_ESTIMATE
38
+
39
+ MAPPINGS = Hash.new { |h, k| h[k] = k }
40
+ MAPPINGS['owner'] = 'owned_by'
41
+ MAPPINGS['state'] = 'current_state'
42
+
43
+ desc "update STORY_ID", "update a story's attributes"
44
+ method_option :attributes, :type => :hash, :required => true
45
+ def update(story_id)
46
+ story = story!(story_id)
47
+ native, custom =
48
+ options[:attributes].
49
+ map { |k, v| [MAPPINGS[k.to_s], v] }.
50
+ partition { |(k, v)| story.respond_to?("#{k}=") }
51
+
52
+ native_h = Hash[*native.flatten]
53
+
54
+ if native_h['current_state']
55
+ unless STATES.include?(native_h['current_state'])
56
+ raise TrackerError, "Invalid state: #{native_h['current_state']}"
57
+ end
58
+
59
+ if STATES_W_ESTIMATE.include?(native_h['current_state']) &&
60
+ ! story.estimate &&
61
+ ! native_h['estimate']
62
+ raise TrackerError,
63
+ "Need an estimate for state `#{native_h['current_state']}'."
64
+ end
65
+ end
66
+
67
+ story.update(native_h) unless native.empty?
68
+
69
+ unless custom.empty?
70
+ str = YAML.dump(Hash[*custom.flatten])
71
+
72
+ pt::Note.new(:owner => story, :text => str).create
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def config
79
+ Flux.environment['trackers']
80
+ end
81
+
82
+ def fake_project
83
+ @fp ||= OpenStruct.new(:id => config['project_id'])
84
+ end
85
+
86
+ def fake_story(id)
87
+ pt::Story.new(:id => id,
88
+ :project_id => config['project_id'])
89
+ end
90
+
91
+ STORY_LIST_HEADERS = %w(ID STATE ASSIGNEE STORY)
92
+
93
+ def list_stories(stories)
94
+ puts_table [STORY_LIST_HEADERS] + stories.map { |s|
95
+ [s.id, s.current_state, s.owned_by, s.name]
96
+ }
97
+ end
98
+
99
+ def login
100
+ ::PivotalTracker::Client.token = config['token']
101
+ end
102
+
103
+ def me
104
+ ms = pt::Membership.all(fake_project)
105
+
106
+ ms.find { |m| m.email == config['email'] }
107
+ end
108
+
109
+ def pt
110
+ login and return ::PivotalTracker
111
+ end
112
+
113
+ def story(id)
114
+ pt::Story.find(id, config['project_id'])
115
+ end
116
+
117
+ def story!(id)
118
+ story(id) or raise TrackerError, "Couldn't find story #{id}."
119
+ end
120
+
121
+ def update_state(story_id, state)
122
+ attrs = {:state => state}
123
+ attrs[:estimate] = options[:estimate] if options[:estimate]
124
+
125
+ invoke :update, [story_id], :attributes => attrs
126
+ end
127
+ end
128
+ end
129
+ end
data/lib/flux/util.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'flux/util/table'
2
+
@@ -0,0 +1,99 @@
1
+ module Flux
2
+ module Util
3
+ # Initialize a new Table.
4
+ #
5
+ # @param [Array<Array>] raw_data the table's annotated data
6
+ # @param field_sep the string used to separate columns
7
+ # @param [Integer] max_width the maximum width for this table.
8
+ # Lines exceeding this width will be truncated.
9
+ # @param shell the shell object used by the current task
10
+ def puts_table(raw_data, field_sep = ' ', max_width = nil)
11
+ $stdout.puts Table.new(shell, raw_data, field_sep, max_width)
12
+ end
13
+
14
+ # A table pretty printer that allows us to take a peek at the data after
15
+ # it's been stripped of formatting information. It exists to make testing
16
+ # easier.
17
+ #
18
+ # If any cell in the first row is prefixed with '>', then that column's
19
+ # contents will be right-aligned. This is useful when showing numbers.
20
+ class Table
21
+ ALIGN_RIGHT = '>'
22
+
23
+ attr_reader :field_sep, :raw_data, :shell, :max_width
24
+
25
+ # @see Flux::Util#puts_table
26
+ def initialize(shell, raw_data, field_sep, max_width)
27
+ @shell = shell
28
+ @raw_data = raw_data
29
+ @field_sep = field_sep
30
+ @max_width = max_width || terminal_width
31
+ end
32
+
33
+ # @return whether a given column's contents will be aligned to the right
34
+ def align_right?(index)
35
+ meta[index].include?(ALIGN_RIGHT)
36
+ end
37
+
38
+ # @return all rows but the first
39
+ def body
40
+ @body ||= raw_data[1..-1]
41
+ end
42
+
43
+ # @return the table data, stripped of format information
44
+ def data
45
+ @data ||= body.unshift(headers)
46
+ end
47
+
48
+ # @return the table's first row
49
+ def headers
50
+ @headers = raw_data.first.each_with_index.map { |e, i|
51
+ align_right?(i) ? e[1..-1] : e
52
+ }
53
+ end
54
+
55
+ # @return the table's format information
56
+ def meta
57
+ @meta ||= @raw_data.first.inject([]) { |a, e|
58
+ a << (e =~ /^>/ ? ALIGN_RIGHT : '')
59
+ }
60
+ end
61
+
62
+ def to_s
63
+ @s ||=
64
+ begin
65
+ widths = data.transpose.inject([]) { |a, col|
66
+ a << col.max { |a, b| a.to_s.size <=> b.to_s.size }.to_s.size
67
+ }
68
+
69
+ # we don't need the last column's width if it is to be left-aligned
70
+ widths[-1] = nil unless align_right?(-1)
71
+
72
+ format = meta.each_with_index.inject('') { |a, (e, i)|
73
+ w = widths[i] ? widths[i].to_s : ''
74
+ align = align_right?(i) ? '' : '-'
75
+
76
+ a << "#{field_sep}%" << align << w << 's'
77
+ }.strip
78
+
79
+ max_width
80
+
81
+ data.inject('') { |a, r|
82
+ l =
83
+ a << truncate(format % r, max_width) << "\n"
84
+ }
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def truncate(string, width)
91
+ shell.send(:truncate, string, width)
92
+ end
93
+
94
+ def terminal_width
95
+ shell.send(:terminal_width)
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,31 @@
1
+ module Flux
2
+ module Workflows
3
+ module MojoTech
4
+ class Developer < Thor
5
+ namespace :dev
6
+
7
+ desc "link STORY_ID", "link a story to a branch"
8
+ def link(story_id)
9
+ invoke 'stories:update',
10
+ [story_id],
11
+ :attributes => {'branch' => current_branch_id}
12
+ end
13
+
14
+ private
15
+
16
+ def current_branch_id
17
+ silence { @current_branch_id ||= invoke('branches:current', []) }
18
+ end
19
+
20
+ def silence
21
+ old_stdout = $stdout
22
+ $stdout = StringIO.new
23
+
24
+ yield
25
+ ensure
26
+ $stdout = old_stdout
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,155 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ require 'flux/trackers/pivotal_tracker'
4
+
5
+ describe Flux::Trackers::PivotalTracker do
6
+ let(:environment) {
7
+ {'trackers' => {'project_id' => PROJECT.id,
8
+ 'token' => PROJECT.token,
9
+ 'email' => 'david@mojotech.com'}}
10
+ }
11
+
12
+ before { Flux.environment = environment }
13
+
14
+ it "lists stories by iteration" do
15
+ mock(PivotalTracker::Iteration).current_backlog(fake_project) {
16
+ [OpenStruct.new(:stories => [
17
+ OpenStruct.new(:id => 1,
18
+ :current_state => 'unstarted',
19
+ :owned_by => '',
20
+ :name => 'first story')]),
21
+ OpenStruct.new(:stories => [
22
+ OpenStruct.new(:id => 2,
23
+ :current_state => 'started',
24
+ :owned_by => 'David Leal',
25
+ :name => 'second story')])]
26
+ }
27
+
28
+ t = [['ID', 'STATE', 'ASSIGNEE', 'STORY'],
29
+ [1, 'unstarted', '', 'first story'],
30
+ [2, 'started', 'David Leal', 'second story']]
31
+
32
+ lambda { subject.invoke :list }.should print_table(t)
33
+ end
34
+
35
+ it "grabs a story" do
36
+ prepare_myself
37
+
38
+ mock_update(123, 'owned_by' => 'David Leal')
39
+
40
+ subject.invoke :grab, [123]
41
+ end
42
+
43
+ [%w(starts started start),
44
+ %w(finishes finished finish)].each do |(action, state, task)|
45
+ it "#{action} a story that was already estimated" do
46
+ s = mock_update(123, 'current_state' => state)
47
+ s.estimate = 2
48
+
49
+ subject.invoke task, [123]
50
+ end
51
+
52
+ it "#{action} a story that needs to be estimated" do
53
+ mock_update(123, 'current_state' => state, 'estimate' => 2)
54
+
55
+ subject.invoke task, [123], :estimate => 2
56
+ end
57
+ end
58
+
59
+ describe "attributes" do
60
+ it "updates custom attributes" do
61
+ attrs = {'branch' => 'blah', 'other' => 'yay'}
62
+
63
+ mock_find(123)
64
+ mock(PivotalTracker::Note).new(observe { |h|
65
+ h[:owner].id == 123 &&
66
+ h[:text] == YAML.dump(attrs)
67
+ }).mock!.create
68
+
69
+ subject.invoke :update, [123], :attributes => attrs
70
+ end
71
+
72
+ it "updates native attributes" do
73
+ attrs = {'owner' => 'David Leal',
74
+ 'state' => 'started',
75
+ 'estimate' => 2,
76
+ 'name' => 'My story'}
77
+
78
+ mock_update(123,
79
+ 'owned_by' => 'David Leal',
80
+ 'current_state' => 'started',
81
+ 'estimate' => 2,
82
+ 'name' => 'My story')
83
+
84
+ subject.invoke :update, [123], :attributes => attrs
85
+ end
86
+
87
+ it "updates valid states only" do
88
+ attrs = {'state' => 'sitting'}
89
+
90
+ s = mock_find(123)
91
+
92
+ lambda { subject.invoke :update, [123], :attributes => attrs }.
93
+ should raise_error(Flux::TrackerError, /Invalid state/)
94
+ end
95
+
96
+ it "updates state when story has estimate" do
97
+ attrs = {'state' => 'started'}
98
+
99
+ s = mock_update(123, 'current_state' => 'started')
100
+ s.estimate = 2
101
+
102
+ subject.invoke :update, [123], :attributes => attrs
103
+ end
104
+
105
+ it "updates state when story estimate is given" do
106
+ attrs = {'state' => 'started', 'estimate' => 2}
107
+
108
+ s = mock_update(123, 'current_state' => 'started',
109
+ 'estimate' => 2)
110
+
111
+ subject.invoke :update, [123], :attributes => attrs
112
+ end
113
+
114
+ Flux::Trackers::PivotalTracker::STATES_W_ESTIMATE.each do |state|
115
+ it "fails to update state `#{state}' if no estimate given" do
116
+ attrs = {'state' => state}
117
+
118
+ s = mock_find(123)
119
+
120
+ lambda { subject.invoke :update, [123], :attributes => attrs }.
121
+ should raise_error(Flux::TrackerError, /Need an estimate/)
122
+ end
123
+ end
124
+ end
125
+
126
+ def iteration(which, data)
127
+ mock(PivotalTracker::Iteration).__send__(which, fake_project) {
128
+ OpenStruct.new(:stories => data.map { |d| OpenStruct.new(d) })
129
+ }
130
+ end
131
+
132
+ def fake_project
133
+ OpenStruct.new(:id => PROJECT.id)
134
+ end
135
+
136
+ def fake_story(id)
137
+ PivotalTracker::Story.new(:id => id, :project_id => PROJECT.id)
138
+ end
139
+
140
+ def mock_find(story_id)
141
+ fake_story(story_id).tap { |s|
142
+ mock(PivotalTracker::Story).find(story_id, PROJECT.id) { s }
143
+ }
144
+ end
145
+
146
+ def mock_update(story_id, attrs)
147
+ mock_find(story_id).tap { |s| mock(s).update(attrs) }
148
+ end
149
+
150
+ def prepare_myself
151
+ mock(PivotalTracker::Membership).all(fake_project) {
152
+ [OpenStruct.new(:name => 'David Leal', :email => 'david@mojotech.com')]
153
+ }
154
+ end
155
+ end
@@ -0,0 +1,47 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ describe Flux::Util::Table do
4
+ let(:body) {
5
+ [[123, 'unstarted', 'David Leal', 'First story'],
6
+ [456, 'unscheduled', '', 'Second story']]
7
+ }
8
+
9
+ it "is converted to a string" do
10
+ t = Flux::Util::Table.new(Thor::Shell::Basic.new,
11
+ [%w(ID STATE ASSIGNEE STORY)] + body,
12
+ ' ',
13
+ nil)
14
+
15
+ t.to_s.should == <<EOT
16
+ ID STATE ASSIGNEE STORY
17
+ 123 unstarted David Leal First story
18
+ 456 unscheduled Second story
19
+ EOT
20
+ end
21
+
22
+ it "aligns its columns to the right" do
23
+ t = Flux::Util::Table.new(Thor::Shell::Basic.new,
24
+ [%w(>ID >STATE >ASSIGNEE >STORY)] + body,
25
+ ' ',
26
+ nil)
27
+
28
+ t.to_s.should == <<EOT
29
+ ID STATE ASSIGNEE STORY
30
+ 123 unstarted David Leal First story
31
+ 456 unscheduled Second story
32
+ EOT
33
+ end
34
+
35
+ it "truncates lines to a given width" do
36
+ t = Flux::Util::Table.new(Thor::Shell::Basic.new,
37
+ [%w(ID STATE ASSIGNEE STORY)] + body,
38
+ ' ',
39
+ 7)
40
+
41
+ t.to_s.should == <<EOT
42
+ ID ...
43
+ 123 ...
44
+ 456 ...
45
+ EOT
46
+ end
47
+ end
data/spec/flux_spec.rb ADDED
@@ -0,0 +1,43 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ require 'flux'
4
+
5
+ describe Flux do
6
+ context "with environment files" do
7
+ let(:root) { File.expand_path(File.dirname(__FILE__ + '/../../..')) }
8
+ let(:flux_env) { File.join(root, Flux::RC) }
9
+ let(:flux_env_l) { File.join(root, Flux::RC_LOCAL) }
10
+
11
+ before {
12
+ mock(File).exist?(is_a(String)).any_times { false }
13
+
14
+ mock(File).exist?(flux_env) { true }
15
+ }
16
+
17
+ it "merges the environment" do
18
+ mock(File).exist?(flux_env_l) { true }
19
+
20
+ mock(YAML).load_file(flux_env) {
21
+ {'trackers' => {'project_id' => 123},
22
+ 'workflow' => 'mojotech'}
23
+ }
24
+ mock(YAML).load_file(flux_env_l) {
25
+ {'trackers' => {'token' => 'mytoken'}}
26
+ }
27
+
28
+ Flux.setup.should ==
29
+ {'trackers' => {'project_id' => 123, 'token' => 'mytoken'},
30
+ 'workflow' => 'mojotech'}
31
+ end
32
+
33
+ it "instantiates an adapter" do
34
+ mock(YAML).load_file(flux_env) {
35
+ {'trackers' => {'adapter' => 'pivotal_tracker'}}
36
+ }
37
+
38
+ Flux.setup
39
+
40
+ defined?(Flux::Trackers::PivotalTracker).should be_true
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'flux'
5
+
6
+ PROJECT = OpenStruct.new(:id => 1, :token => 'tok')
7
+
8
+ # Requires supporting files with custom matchers and macros, etc,
9
+ # in ./support/ and its subdirectories.
10
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
11
+
12
+ RSpec.configure do |config|
13
+ config.mock_with :rr
14
+ end
@@ -0,0 +1,46 @@
1
+ RSpec::Matchers.define :print_table do |data|
2
+ class FakeStdout
3
+ attr_reader :table
4
+
5
+ def initialize(stdout)
6
+ @stdout = stdout
7
+ end
8
+
9
+ def puts(*args)
10
+ case args.first
11
+ when Flux::Util::Table
12
+ @table = args.first
13
+ else
14
+ @stdout.puts *args
15
+ end
16
+ end
17
+
18
+ def write(data)
19
+ @stdout.write data
20
+ end
21
+ end
22
+
23
+
24
+ match do |block|
25
+ stdout = $stdout
26
+ $stdout = @stdout = FakeStdout.new(stdout)
27
+
28
+ begin
29
+ block.call
30
+
31
+ @stdout.table.should_not be_nil
32
+ @stdout.table.data.should == data
33
+ ensure
34
+ $stdout = stdout
35
+ end
36
+ end
37
+
38
+ failure_message_for_should do |block|
39
+ "Expected table:\n\n#{Flux::Util::Table.new(data, ' ', nil)}\n" <<
40
+ "but got:\n\n#{@stdout.table ? @stdout.table : 'nil'}"
41
+ end
42
+
43
+ failure_message_for_should_not do |block|
44
+ raise NotImplementedError
45
+ end
46
+ end
@@ -0,0 +1,8 @@
1
+ require 'rr'
2
+
3
+ module RR::Adapters::RRMethods
4
+ def observe(&block)
5
+ RR::WildcardMatchers::Satisfy.new(block)
6
+ end
7
+ end
8
+
metadata ADDED
@@ -0,0 +1,167 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flux
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - David Leal
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-09-05 00:00:00 +01:00
14
+ default_executable: flux
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: thor
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: 0.14.0
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: pivotal-tracker
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ~>
33
+ - !ruby/object:Gem::Version
34
+ version: 0.4.0
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: grit
40
+ requirement: &id003 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 2.4.0
46
+ type: :development
47
+ prerelease: false
48
+ version_requirements: *id003
49
+ - !ruby/object:Gem::Dependency
50
+ name: rspec
51
+ requirement: &id004 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ~>
55
+ - !ruby/object:Gem::Version
56
+ version: "2.0"
57
+ type: :development
58
+ prerelease: false
59
+ version_requirements: *id004
60
+ - !ruby/object:Gem::Dependency
61
+ name: bundler
62
+ requirement: &id005 !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ~>
66
+ - !ruby/object:Gem::Version
67
+ version: 1.0.0
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: *id005
71
+ - !ruby/object:Gem::Dependency
72
+ name: jeweler
73
+ requirement: &id006 !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ~>
77
+ - !ruby/object:Gem::Version
78
+ version: 1.6.2
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: *id006
82
+ - !ruby/object:Gem::Dependency
83
+ name: rcov
84
+ requirement: &id007 !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: "0"
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: *id007
93
+ - !ruby/object:Gem::Dependency
94
+ name: rr
95
+ requirement: &id008 !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ~>
99
+ - !ruby/object:Gem::Version
100
+ version: 1.0.0
101
+ type: :development
102
+ prerelease: false
103
+ version_requirements: *id008
104
+ description:
105
+ email: david@mojotech.com
106
+ executables:
107
+ - flux
108
+ extensions: []
109
+
110
+ extra_rdoc_files:
111
+ - LICENSE.txt
112
+ files:
113
+ - .document
114
+ - .flux
115
+ - .flux.local.sample
116
+ - .rspec
117
+ - Gemfile
118
+ - LICENSE.txt
119
+ - Rakefile
120
+ - VERSION
121
+ - bin/flux
122
+ - lib/flux.rb
123
+ - lib/flux/ext/pivotal-tracker.rb
124
+ - lib/flux/rcs/git.rb
125
+ - lib/flux/trackers/pivotal_tracker.rb
126
+ - lib/flux/util.rb
127
+ - lib/flux/util/table.rb
128
+ - lib/flux/workflows/mojotech.rb
129
+ - spec/flux/trackers/pivotal_tracker_spec.rb
130
+ - spec/flux/util/table_spec.rb
131
+ - spec/flux_spec.rb
132
+ - spec/spec_helper.rb
133
+ - spec/support/matchers/print_table.rb
134
+ - spec/support/rr.rb
135
+ has_rdoc: true
136
+ homepage: http://github.com/mojotech/flux
137
+ licenses:
138
+ - MIT
139
+ post_install_message:
140
+ rdoc_options: []
141
+
142
+ require_paths:
143
+ - lib
144
+ required_ruby_version: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ hash: 692717009
150
+ segments:
151
+ - 0
152
+ version: "0"
153
+ required_rubygems_version: !ruby/object:Gem::Requirement
154
+ none: false
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: "0"
159
+ requirements: []
160
+
161
+ rubyforge_project:
162
+ rubygems_version: 1.6.2
163
+ signing_key:
164
+ specification_version: 3
165
+ summary: Command line workflow manager.
166
+ test_files: []
167
+