pendaxes 0.0.1

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