pendaxes 0.0.1

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.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ fixtures/repo
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ -fd
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pendaxes.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # Pendaxes
2
+
3
+ Throw axes to pending makers!
4
+
5
+ Leaving a pending long time is really bad, shouldn't be happened. They'll make a trouble.
6
+ So, this gem sends notification to committer that added pending after a while from the commit.
7
+
8
+ Avoid the trouble due to pending examples :D
9
+
10
+ ## Installation
11
+
12
+ (1.9 required)
13
+
14
+ $ gem install pendaxes
15
+
16
+ ## Usage
17
+
18
+ $ pendaxes <config_file>
19
+
20
+ (writing in cron is recommended)
21
+
22
+ ### Configuration
23
+
24
+ #### Minimal
25
+
26
+ Clone `https://github.com/foo/bar.git` to `/path/to/be/cloned/repository`, then detect pendings using rspec detector (default).
27
+
28
+ Finally send notification to committers via email (from `no-reply@example.com`).
29
+
30
+ workspace:
31
+ path: /path/to/be/cloned/repository # where to clone?
32
+ repository: https://github.com/foo/bar.git # where clone from?
33
+ report:
34
+ use: haml
35
+ to: "report.html"
36
+ notifications:
37
+ - use: mail
38
+ from: no-reply@example.com
39
+ reporter:
40
+ use: haml
41
+
42
+
43
+ #### Full
44
+
45
+ detection:
46
+ use: rspec # use rspec detector for pending detection. (default)
47
+ # pattern: '*_spec.rb' # this will be passed after `git grep ... --`. Default is "*_spec.rb".
48
+ # allowed_for: 604800 # (second) = 1 week. Pendings will be marked "not allowed" if it elapsed more than this value
49
+
50
+ workspace:
51
+ path: /path/to/be/cloned/repository # where to clone?
52
+ repository: "https://github.com/user/repo.git" # where clone from?
53
+
54
+ report: # report configuration to save
55
+ use: haml # what reporter to use (haml and text is bundled in the gem)
56
+ to: "report.html" # where to save?
57
+ include_allowed: true # include "allowed" pendings in report. (default: true)
58
+ # VVV haml reporter specific configuration VVV
59
+ commit_url: "https://github.com/user/repo/commit/%commit%" # Used for link to commit. %commit% will be replaced to sha1. If not specified, will not be a link.
60
+ file_url: "https://github.com/user/repo/blob/HEAD/%file%#L%line%" # Used for link to file. %file% and %line% will be replaced to filepath and line no. If not specified, will not be a link.
61
+
62
+ notifications: # notifications. multiple values are accepted.
63
+ - use: terminal # use terminal notificator.
64
+ - use: mail # use mail notificator.
65
+ reporter: # reporter setting for this (mail) notification
66
+ use: haml
67
+ commit_url: "https://github.com/user/repo/commit/%commit%"
68
+ file_url: "https://github.com/user/repo/blob/HEAD/%file%#L%line%"
69
+
70
+ # VVV mail notificator specific configuration VVV
71
+ from: no-reply@example.com
72
+ to: foo@example.com # (optional) mail will be sent once to this mail address.
73
+ # without this, mail will be sent separated for each committer.
74
+ # (mails will include pendings added by its recipient only.)
75
+
76
+ delivery_method: sendmail # specify delivery_method. https://github.com/mikel/mail for more detail.
77
+ delivery_options: # (optional) used as option for delivery_method.
78
+ :location: /usr/sbin/sendmail
79
+
80
+ whitelist: # (optional) if whitelist set, mail won't be sent if not matched.
81
+ - foo@bar # complete match.
82
+ - /example\.com$/ # used as regexp.
83
+ blacklist: # (optional) mail won't be sent if matched. preferred than whitelist.
84
+ - black@example.com
85
+ - /^black/
86
+
87
+ alias: # (optional) Aliasing emails. if mail will be sent to <value> if git commit author is <key>.
88
+ "foo@gmail.com": "foo@company.com"
89
+
90
+
91
+ * Reporter: generates text or html by given pendings
92
+ * Notificator: get text or html by reporter, and notify it (via mail, to terminal, etc...)
93
+
94
+ ## Axes?
95
+
96
+ 斧... Axe in Japanese. Recently, Japanese engineer says a review comment as axe (斧).
97
+
98
+ Throwing axe means "comment my opnion."
99
+
100
+ This script throws axe to committer, about his/her uncontrolled pending tests.
101
+
102
+ ## Requirements
103
+
104
+ * Ruby 1.9+ (1.9.3 supported)
105
+ * git
106
+
107
+ we're using `git grep` and `git blame` to detect pendings, and supported test suite is currently `rspec` only.
108
+ this gem supports git managed repository, and using rspec.
109
+
110
+ Patches for other environment are welcomed. :-)
111
+
112
+ ## Writing notificator and reporter
113
+
114
+ TBD
115
+
116
+ ## License
117
+
118
+ MIT License:
119
+
120
+ (c) 2012 Shota Fukumori (sora_h)
121
+
122
+ Permission is hereby granted, free of charge, to any person obtaining a copy
123
+ of this software and associated documentation files (the "Software"), to deal
124
+ in the Software without restriction, including without limitation the rights
125
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
126
+ copies of the Software, and to permit persons to whom the Software is
127
+ furnished to do so, subject to the following conditions:
128
+
129
+ The above copyright notice and this permission notice shall be included in
130
+ all copies or substantial portions of the Software.
131
+
132
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
133
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
134
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
135
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
136
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
137
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
138
+ THE SOFTWARE.
139
+
140
+ ## Contributing
141
+
142
+ 1. Fork it
143
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
144
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
145
+ 4. Push to the branch (`git push origin my-new-feature`)
146
+ 5. Create new Pull Request
147
+
148
+
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+ RSpec::Core::RakeTask.new(:spec)
4
+
5
+ task :default => :spec
data/bin/pendaxes ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ begin
5
+ require 'pendaxes'
6
+ rescue LoadError
7
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
8
+ retry
9
+ end
10
+
11
+ exit Pendaxes.run(*ARGV)
Binary file
@@ -0,0 +1,56 @@
1
+ require_relative "./config"
2
+ require_relative "./workspace"
3
+ require_relative "./detector"
4
+ require_relative "./reporter"
5
+ require_relative "./notificator"
6
+
7
+ module Pendaxes
8
+ class CommandLine
9
+ def initialize(*args)
10
+ @args = args
11
+ @config = Config.new(YAML.load_file(args.first))
12
+ end
13
+
14
+ def run
15
+ puts "=> Update repository"
16
+ update
17
+
18
+ puts "=> Detect pendings"
19
+ detect
20
+
21
+ puts "=> Writing report"
22
+ report
23
+
24
+ puts "=> Send notifications"
25
+ notify
26
+
27
+ 0
28
+ end
29
+
30
+ def update
31
+ @workspace = Workspace.new(@config.workspace)
32
+ @workspace.update
33
+ end
34
+
35
+ def detect
36
+ @detector = Detector.find(@config.detection.use.to_sym).new(@workspace, {out: $stdout}.merge(@config.detection))
37
+ @pendings = @detector.detect
38
+ end
39
+
40
+ def report
41
+ @reporter = Reporter.find(@config.report.use.to_sym).new(@config.report)
42
+ @reporter.add @pendings
43
+ report = @reporter.report
44
+ open(@config.report.to, 'w') {|io| io.puts report }
45
+ end
46
+
47
+ def notify
48
+ @config.notifications.map{|x| Hashr.new(x) }.each do |notification|
49
+ puts " * #{notification.use}"
50
+ notificator = Notificator.find(notification.use.to_sym).new({out: $stdout}.merge(notification))
51
+ notificator.add @pendings
52
+ notificator.notify
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,10 @@
1
+ require 'hashr'
2
+
3
+ module Pendaxes
4
+ class Config < Hashr
5
+ define detection: {use: :rspec},
6
+ workspace: {},
7
+ report: {use: :text, to: "report.txt"},
8
+ notifications: [{use: :terminal}]
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ module Pendaxes
2
+ module Defaults
3
+ def defaults(h=nil)
4
+ default_defaults = self.superclass.respond_to?(:defaults) ? self.superclass.defaults : Hashr.new
5
+ @defaults ||= default_defaults
6
+ if h
7
+ @defaults = default_defaults.merge(h)
8
+ end
9
+ @defaults
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ require_relative 'pending_manager'
2
+ require_relative 'defaults'
3
+ require_relative 'finder'
4
+ require 'hashr'
5
+
6
+ module Pendaxes
7
+ class Detector
8
+ extend Defaults
9
+ extend Finder
10
+ find_in 'pendaxes/detectors'
11
+
12
+ defaults allowed_for: 604800
13
+
14
+ def initialize(workspace, config={})
15
+ @config = Hashr.new(self.class.defaults.merge(config))
16
+ @workspace = workspace
17
+ @pendings = []
18
+ end
19
+
20
+ def detect
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,78 @@
1
+ # coding: utf-8
2
+ require_relative '../detector'
3
+ require 'time'
4
+ require 'ripper'
5
+ require 'ripper/lexer'
6
+
7
+ module Pendaxes
8
+ class Detector
9
+ class RSpec < Detector
10
+ PENDERS = %w(xit xexample xspecify pending).freeze
11
+ GREP_CMD = ( %w(grep --name-only -e) \
12
+ + PENDERS.join(' --or -e ').split(/ /) \
13
+ + ['--']
14
+ ).freeze
15
+ PARENTS = %w(context describe it example specify xexample xspecify xit).freeze
16
+
17
+ defaults pattern: '*_spec.rb'
18
+
19
+ def detect
20
+ @workspace.dive do
21
+ pattern = @config.pattern.is_a?(Array) ? @config.pattern : [@config.pattern]
22
+ grep = @workspace.git(*GREP_CMD, *pattern).force_encoding("UTF-8")
23
+ return [] unless grep
24
+ files = grep.split(/\r?\n/).map(&:chomp)
25
+
26
+ files.inject([]) do |pendings, file|
27
+ @config.out.puts "* #{file}" if @config.out
28
+ file_content = File.read(file).force_encoding(@config.encoding || "UTF-8")
29
+ lines = file_content.split(/\r?\n/)
30
+ tokens = Ripper.lex(file_content, file)
31
+ _prev = nil
32
+
33
+ tokens.each_with_index do |token, i|
34
+ prev = _prev
35
+ _prev = token[1]
36
+ next if prev == :on_symbeg
37
+ next unless token[1] == :on_ident && PENDERS.include?(token[2])
38
+ pending = {}
39
+
40
+ line = token[0][0]
41
+
42
+ parent = (i-1).downto(0).inject {|_, j|
43
+ break lines[tokens[j][0][0]-1] if tokens[j][1] == :on_ident && (j.zero? || tokens[j-1][1] != :on_symbeg) && PARENTS.include?(tokens[j][2])
44
+ nil
45
+ }
46
+ parent.gsub!(/^[ \t]+/, '') if parent
47
+
48
+ pending[:example] = {
49
+ file: file, line: line,
50
+ message: lines[line-1].gsub(/^[ \t]+/, ''), parent: parent
51
+ }
52
+
53
+ pending[:commit] = blame(file, line)
54
+ pending[:allowed] = (Time.now - pending[:commit][:at]) <= @config.allowed_for
55
+
56
+ pendings << pending
57
+ end
58
+
59
+ pendings
60
+ end
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def blame(file, line)
67
+ @config.out.puts " * blaming #{file}:#{line}" if @config.out
68
+ blame = @workspace.git('blame', '-L', "#{line},#{line}", '-l', '-w', '-p', file).force_encoding("UTF-8").split(/\r?\n/).map{|l| l.split(/ /) }
69
+ commit = {
70
+ sha: blame[0].first, name: blame[1][1..-1].join(' '),
71
+ email: blame[2][1..-1].join(' ').gsub(/^</,'').gsub(/>$/,'')
72
+ }
73
+ commit[:at] = Time.parse(@workspace.git(*%w(log --pretty=%aD -n1), commit[:sha]).chomp)
74
+ commit
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,34 @@
1
+ module Pendaxes
2
+ module Finder
3
+ def find(name)
4
+ @finder_cache ||= {}
5
+ path = case name
6
+ when String
7
+ name
8
+ when Symbol
9
+ "#{@finder_prefix || ""}#{name}"
10
+ else
11
+ raise ArgumentError, "name should be a kind of String or Symbol"
12
+ end
13
+ return @finder_cache[name] if @finder_cache[name]
14
+ require path
15
+ announce name, @finder_latest_inherited
16
+ end
17
+
18
+ def inherited(klass)
19
+ if klass.name
20
+ announce klass.name.gsub(/^.*::/,'').gsub(/.[A-Z]/){|s| s[0]+"_"+s[1] }.downcase.to_sym, klass
21
+ end
22
+ @finder_latest_inherited = klass
23
+ end
24
+
25
+ def announce(name, klass)
26
+ (@finder_cache ||= {})[name] = klass
27
+ end
28
+
29
+ def find_in(prefix)
30
+ @finder_prefix = prefix + "/"
31
+ end
32
+ private :find_in
33
+ end
34
+ end
@@ -0,0 +1,32 @@
1
+ require_relative 'pending_manager'
2
+ require_relative 'defaults'
3
+ require_relative 'finder'
4
+ require 'hashr'
5
+
6
+ module Pendaxes
7
+ class Notificator
8
+ include PendingManager
9
+ extend Defaults
10
+ extend Finder
11
+ find_in 'pendaxes/notificators'
12
+ defaults reporter: {use: :text}
13
+
14
+ def initialize(config={})
15
+ @config = Hashr.new(self.class.defaults.merge(config))
16
+ @pendings = []
17
+ end
18
+
19
+ def notify
20
+ end
21
+
22
+ def reporter
23
+ Reporter.find(@config.reporter.use.to_sym)
24
+ end
25
+
26
+ def report_for(pendings)
27
+ r = reporter.new({include_allowed: true}.merge(@config.reporter))
28
+ r.add pendings
29
+ r.report
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,72 @@
1
+ require_relative '../notificator'
2
+ require 'mail'
3
+
4
+ module Pendaxes
5
+ class Notificator
6
+ class Mail < Notificator
7
+ defaults reporter: {use: :text}, blacklist: []
8
+
9
+ def notify
10
+ if @config.to
11
+ deliver(pendings, @config.to)
12
+ else
13
+ pendings.group_by {|pending| pending[:commit][:email] }.each do |email, pends|
14
+ deliver(pends, email)
15
+ end
16
+ end
17
+ end
18
+
19
+ def whitelist
20
+ if @config.whitelist
21
+ @whitelist ||= process_email_filter(@config.whitelist)
22
+ else
23
+ nil
24
+ end
25
+ end
26
+
27
+ def blacklist
28
+ @blacklist ||= process_email_filter(@config.blacklist)
29
+ end
30
+
31
+ private
32
+
33
+ def deliver(pends,email)
34
+ real_email = (@config.alias || {})[email] || email
35
+ return nil if blacklist.match?(real_email) || (whitelist && !whitelist.match?(real_email))
36
+
37
+ mail = ::Mail.new
38
+ mail.from = @config.from
39
+ mail.to = real_email
40
+ mail.subject = "[Pendaxes] Your #{pends.size} pending tests are waiting to be fixed"
41
+ mail.body = report_for(pends)
42
+ mail.content_type = 'text/html; charset=utf-8' if reporter.html?
43
+
44
+ if @config.delivery_method
45
+ mail.delivery_method @config.delivery_method.to_sym, @config.delivery_options || {}
46
+ end
47
+
48
+ @config.out.puts mail.inspect if @config.out
49
+
50
+ mail.deliver
51
+ end
52
+
53
+ def process_email_filter(list)
54
+ filter = list.map do |email|
55
+ if %r{^/(.*)/$} === email
56
+ Regexp.new($1)
57
+ else
58
+ email
59
+ end
60
+ end
61
+ class << filter
62
+ def match?(email)
63
+ self.any? do |condition|
64
+ condition.is_a?(Regexp) ? condition === email : condition == email
65
+ end
66
+ end
67
+ end
68
+ filter
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,19 @@
1
+ require_relative '../notificator'
2
+
3
+ module Pendaxes
4
+ class Notificator
5
+ class Terminal < Notificator
6
+ defaults to: $stdout, reporter: {use: :text}
7
+
8
+ def notify
9
+ io = @config.to
10
+ pendings.group_by{|x| "#{x[:commit][:name]} <#{x[:commit][:email]}>" }.each do |name, pends|
11
+ io.puts "#{name}:"
12
+ io.puts
13
+ io.puts report_for(pends)
14
+ io.puts
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ require 'hashr'
2
+
3
+ module Pendaxes
4
+ module PendingManager
5
+ def add(pending={})
6
+ case pending
7
+ when Array
8
+ @pendings.push *pending.map{|x| Hashr.new(x) }
9
+ when Hash
10
+ @pendings << Hashr.new(pending)
11
+ end
12
+ end
13
+
14
+ def pendings
15
+ if @config.include_allowed
16
+ all_pendings
17
+ else
18
+ @pendings.reject(&:allowed)
19
+ end
20
+ end
21
+
22
+ def all_pendings
23
+ @pendings
24
+ end
25
+
26
+ def reset
27
+ @pendings = []
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,27 @@
1
+ require_relative 'pending_manager'
2
+ require_relative 'defaults'
3
+ require_relative 'finder'
4
+ require 'hashr'
5
+
6
+ module Pendaxes
7
+ class Reporter
8
+ include PendingManager
9
+ extend Defaults
10
+ extend Finder
11
+ find_in 'pendaxes/reporters'
12
+
13
+ defaults include_allowed: true
14
+
15
+ def initialize(config={})
16
+ @config = Hashr.new(self.class.defaults.merge(config))
17
+ @pendings = []
18
+ end
19
+
20
+ def report
21
+ end
22
+
23
+ def self.html?
24
+ false
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ require_relative '../reporter'
2
+ require 'haml'
3
+ require 'digest/md5'
4
+ require 'cgi'
5
+
6
+ module Pendaxes
7
+ class Reporter
8
+ class Haml < Reporter
9
+ defaults commit_url: nil,
10
+ file_url: nil,
11
+ report_url: nil,
12
+ gravatar: true,
13
+ older_first: true,
14
+ template: "#{File.dirname(__FILE__)}/template.haml"
15
+
16
+ def report
17
+ haml = ::Haml::Engine.new(File.read(@config.template))
18
+ haml.render(binding, config: @config)
19
+ end
20
+
21
+ def self.html?; true; end
22
+
23
+ private
24
+
25
+ def relative_time(time, from = Time.now)
26
+ diff = from - time
27
+ return "in the future" if diff < 0
28
+ case diff
29
+ when 0..10
30
+ "Just now"
31
+ when 11..60
32
+ "in a minute"
33
+ when 61...3600
34
+ "%.1f minutes ago" % (diff/60)
35
+ when 3600...86400
36
+ "%.1f hours ago" % (diff/3600.0)
37
+ when 86400...604800
38
+ "%.1f days ago" % (diff/86400.0)
39
+ when 604800...2419200
40
+ "%.1f weeks ago" % (diff/604800.0)
41
+ when 2419200...31556926
42
+ "%.1f months ago" % (diff/2419200.0)
43
+ else
44
+ "%.1f years ago" % (diff/31556926.0)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end