agile_notifier 1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e8d6da02fafc819c407ab3abf1b53f40ab5c7fcd
4
+ data.tar.gz: 8248b6c72b97d94581e2142d174fe37fc65b7769
5
+ SHA512:
6
+ metadata.gz: 401c943e96a5b85e53e95b615bd5128c996f7d026c99561f17e05948cca16baf3af427b48da755d3b94f6bff177ee53c720e5b4cd63efc43af8a0ba7b49f3c1b
7
+ data.tar.gz: f468cb62a61bc49b88f47f9d46e22540c929732e1fb0c7a0de4cd802bc1282bc128a251c944f4dd1daf8f0620bc905db725c36edb781ea2cbaefeb11bd1aa6f2
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+ #gemspec
3
+
4
+ gem 'json'
5
+ gem 'httparty'
6
+
7
+ group :test do
8
+ gem 'mocha'
9
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,20 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ httparty (0.11.0)
5
+ multi_json (~> 1.0)
6
+ multi_xml (>= 0.5.2)
7
+ json (1.8.0)
8
+ metaclass (0.0.1)
9
+ mocha (0.14.0)
10
+ metaclass (~> 0.0.1)
11
+ multi_json (1.8.0)
12
+ multi_xml (0.5.5)
13
+
14
+ PLATFORMS
15
+ ruby
16
+
17
+ DEPENDENCIES
18
+ httparty
19
+ json
20
+ mocha
data/README.md ADDED
@@ -0,0 +1,43 @@
1
+ AgileNotifier
2
+ =============
3
+
4
+ Agile Notifier - an easy way of monitoring Agile SW Engineering by auralization change. It supports most of the popular tools including **CI** (Continuous Integration), **SCM** (Source Control Management), and **ITS** (Issue Tracking System). Of course you can integrate more tools as you want. And the simple beautiful DSL syntax gives you a rocket start.
5
+
6
+ In a modern agile team, members would like to be notified immediately once their Continuous Integration job fails (but building process usually takes quite a while). That's the initial idea of this tool.
7
+
8
+ The joy of this tool is that, whenever a build fails, it can blame whoever submitted the commit (or praises the committer who fixed it), in whatever language you want, and even the sentenses can be randomly chosen each time :)
9
+
10
+ Have fun with it!
11
+
12
+ ## Examples of Usage:
13
+ ```ruby
14
+ AgileNotifier::Configuration.set do
15
+ ci_url 'http://x.x.x.x:8080'
16
+ ci_job 'your-project-continuous-build'
17
+ ci_get 'Jenkins'
18
+
19
+ scm_url 'https://github.xyzcompany.com'
20
+ scm_repo user: 'your_user_name', repo: 'your_repository_name'
21
+ scm_get 'Github', enterprise: true
22
+
23
+ # for non-enterprise version
24
+ # scm_url 'https://api.github.com'
25
+ # scm_repo user: 'your_user_name', repo: 'your_repository_name'
26
+ # scm_get 'Github'
27
+
28
+ speak 'en'
29
+ play 'Boing' # Mac OSX TTS voice name(optional field), unnecessary for other OS
30
+
31
+ alert_on_fail
32
+ alert_on_fix
33
+ end
34
+ ```
35
+
36
+ ## Notes:
37
+ * TTS (Text To Speech) on Linux used here has two dependencies:
38
+
39
+ * TTS service benefits from online MARY TTS Web Client: http://mary.dfki.de:59125/ While it has limited languages support, please check before use.
40
+ * play command comes from package sox which is not pre-installed, you have to install manually beforehand by:
41
+ * ```
42
+ sudo apt-get install sox libsox-fmt-all
43
+ ```
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), *%w[lib])))
2
+ require 'agile_notifier'
3
+
4
+ task :default => :test
5
+
6
+ require 'rake/testtask'
7
+ desc 'Run Unit Test'
8
+ Rake::TestTask.new do |test|
9
+ test.libs << 'test'
10
+ test.test_files = FileList['test/test*.rb']
11
+ test.verbose = true
12
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ $: << File.expand_path('../lib', __FILE__)
4
+ require "agile_notifier"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'agile_notifier'
8
+ s.version = AgileNotifier::VERSION
9
+ s.license = 'MIT'
10
+ s.date = '2013-05-22'
11
+ s.summary = %q{agile_notifier alerts you via making wonderful noises}
12
+ s.description = %q{agile_notifier alerts you via making wonderful noises, make software development more fun}
13
+ s.authors = ['Jing Li']
14
+ s.email = ['thyrlian@gmail.com']
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.homepage = 'https://github.com/thyrlian/AgileNotifier'
18
+
19
+ s.add_runtime_dependency('json', '~> 1.8.0')
20
+ s.add_runtime_dependency('httparty', '~> 0.11.0')
21
+ s.add_development_dependency('mocha', '~> 0.14.0')
22
+ end
@@ -0,0 +1,89 @@
1
+ require_relative 'servable'
2
+ require_relative 'trackable'
3
+
4
+ module AgileNotifier
5
+ class CI
6
+ include Servable
7
+ alias_method :original_is_available?, :is_available?
8
+
9
+ attr_accessor :jobs
10
+
11
+ def initialize(url, *jobs)
12
+ @url = url
13
+ jobs.empty? ? @jobs = get_all_jobs : @jobs = jobs
14
+ end
15
+
16
+ def is_available?
17
+ original_is_available?(@url)
18
+ end
19
+
20
+ def get_all_jobs
21
+ raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
22
+ end
23
+
24
+ def job
25
+ if @jobs.size == 1
26
+ return @jobs.first
27
+ else
28
+ raise('There are more than one job, please use method [jobs] instead of [job]')
29
+ end
30
+ end
31
+
32
+ class Job
33
+ attr_accessor :name, :url
34
+ attr_reader :last_build, :penultimate_build
35
+
36
+ def initialize(name, url)
37
+ @name = name
38
+ @url = url
39
+ @last_build = get_last_build
40
+ @penultimate_build = get_penultimate_build
41
+ end
42
+
43
+ def get_last_build
44
+ raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
45
+ end
46
+
47
+ def get_penultimate_build
48
+ raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
49
+ end
50
+
51
+ def update_last_build
52
+ @last_build = get_last_build
53
+ end
54
+
55
+ def update_penultimate_build
56
+ @penultimate_build = get_penultimate_build
57
+ end
58
+
59
+ class Build
60
+ include Trackable
61
+
62
+ attr_accessor :number, :url, :result, :revision
63
+
64
+ def initialize(number, url)
65
+ @number = number
66
+ @url = url
67
+ @result = get_result
68
+ @revision = get_revision
69
+ end
70
+
71
+ def get_result
72
+ raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
73
+ end
74
+
75
+ def get_revision
76
+ raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
77
+ end
78
+
79
+ def get_previous_build
80
+ raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
81
+ end
82
+
83
+ def get_previous_result
84
+ raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,91 @@
1
+ # encoding: utf-8
2
+
3
+ module AgileNotifier
4
+ class Composer
5
+ SENTENCES_BLAME = {
6
+ de: [
7
+ "%{committer_name} hat den Build kaputt gemacht.",
8
+ "Schießt %{committer_name} mit der Nerf Gun ab!",
9
+ "%{committer_name} hat Scheiße gebaut.",
10
+ "%Hilfe! Hilfe! %{committer_name} versucht mich zu töten!"
11
+ ],
12
+ en: [
13
+ "%{committer_name} has broken the build.",
14
+ "%{committer_name} fucked up the build.",
15
+ "What the fucking code has %{committer_name} pushed!"
16
+ ],
17
+ es: [
18
+ "%{committer_name} ha destruido la compilacion",
19
+ "que clase de codigo esta escribiendo %{committer_name} ? ",
20
+ "tal vez es mejor que %{committer_name} se dedique a otra cosa..."
21
+ ],
22
+ zh: [
23
+ "%{committer_name}在搞毛啊, 构建失败了!",
24
+ "%{committer_name}提交的什么烂代码啊?",
25
+ "请注意, %{committer_name}在搞破坏."
26
+ ]
27
+ }
28
+
29
+ SENTENCES_PRAISE = {
30
+ de: [
31
+ "%{committer_name} hat den Build gefixt!",
32
+ "%{committer_name} ist ein Genie!",
33
+ "%{committer_name} hat die Welt gerettet!"
34
+ ],
35
+ en: [
36
+ "%{committer_name} has fixed the build.",
37
+ "%{committer_name} is super brilliant!",
38
+ "%{committer_name} saved the world.",
39
+ "%{committer_name} roundhouse kicked chuck norris' butt"
40
+ ],
41
+ es: [
42
+ "%{committer_name} ha reparado la compilacion",
43
+ "%{committer_name} es un genio!",
44
+ "%{committer_name} es el mejor programador de la historia!"
45
+ ],
46
+ zh: [
47
+ "%{committer_name}很厉害啊, 修复了构建.",
48
+ "%{committer_name}是个好同志, 该涨工资了.",
49
+ "%{committer_name}是当代活雷锋啊!"
50
+ ]
51
+ }
52
+
53
+ class << self
54
+ def blame_committer_of_a_commit(args)
55
+ committer_name = get_committer_name_of_a_commit(args)
56
+ blame_committer(committer_name, args[:language])
57
+ end
58
+
59
+ def praise_committer_of_a_commit(args)
60
+ committer_name = get_committer_name_of_a_commit(args)
61
+ praise_committer(committer_name, args[:language])
62
+ end
63
+
64
+ def get_committer_name_of_a_commit(args)
65
+ repo = args[:repo]
66
+ revision = args[:revision] || args[:build].revision
67
+ repo.get_committer_name_of_a_commit(revision)
68
+ end
69
+
70
+ def blame_committer(committer_name, language)
71
+ mention_committer(committer_name, SENTENCES_BLAME[language])
72
+ end
73
+
74
+ def praise_committer(committer_name, language)
75
+ mention_committer(committer_name, SENTENCES_PRAISE[language])
76
+ end
77
+
78
+ def mention_committer(committer_name, sentences)
79
+ sentence = random_picker(sentences)
80
+ sentence.gsub(/%\{committer_name\}/, committer_name)
81
+ end
82
+
83
+ def random_picker(list)
84
+ random_number = rand(list.size)
85
+ list[random_number]
86
+ end
87
+ end
88
+
89
+ private_class_method :get_committer_name_of_a_commit, :blame_committer, :praise_committer, :mention_committer, :random_picker
90
+ end
91
+ end
@@ -0,0 +1,82 @@
1
+ require_relative '../agile_notifier'
2
+
3
+ module AgileNotifier
4
+ class Configuration
5
+ def initialize(&blk)
6
+ @current_module = Object.const_get(self.class.to_s.split('::').first)
7
+ instance_eval(&blk)
8
+ end
9
+
10
+ class << self
11
+ def set(&blk)
12
+ new(&blk)
13
+ end
14
+ end
15
+
16
+ def ci_url(url)
17
+ @ci_url = url
18
+ end
19
+
20
+ def ci_job(job)
21
+ @ci_jobs ||= []
22
+ @ci_jobs.push(job)
23
+ end
24
+
25
+ def ci_get(ci_type)
26
+ @ci = @current_module.const_get(ci_type).new(@ci_url, *@ci_jobs)
27
+ end
28
+
29
+ def scm_url(url)
30
+ @scm_url = url
31
+ end
32
+
33
+ def scm_repo(repo)
34
+ @scm_repos ||= []
35
+ @scm_repos.push(repo)
36
+ end
37
+
38
+ def scm_get(scm_type, args = {})
39
+ enterprise = args.fetch(:enterprise, false)
40
+ if enterprise
41
+ @scm = @current_module.const_get(scm_type).new_enterprise_version(@scm_url)
42
+ else
43
+ @scm = @current_module.const_get(scm_type).new(@scm_url)
44
+ end
45
+ @scm_repos.each do |repo|
46
+ @scm.add_repository(repo)
47
+ end
48
+ return @scm
49
+ end
50
+
51
+ def speak(language)
52
+ @language = language.to_s.downcase.intern
53
+ end
54
+
55
+ def play(voice)
56
+ @voice = voice
57
+ end
58
+
59
+ def alert_on_fail
60
+ alert(:blame, :fail)
61
+ end
62
+
63
+ def alert_on_fix
64
+ alert(:praise, :fix)
65
+ end
66
+
67
+ def alert(composer_type, judger_type)
68
+ args = Hash.new
69
+ args[:language] = @language
70
+ args[:voice] = @voice if @voice
71
+ composer_type = composer_type.to_s.downcase
72
+ judger_type = judger_type.to_s.downcase
73
+ composer_method = "#{composer_type}_committer_of_a_commit".intern
74
+ judger_method = "on_#{judger_type}".intern
75
+ text = Composer.send(composer_method, repo: @scm.repository, revision: @ci.job.last_build.revision, language: @language)
76
+ Judger.send(judger_method, @ci.job.last_build, text, args)
77
+ end
78
+
79
+ private_class_method :new
80
+ private :alert
81
+ end
82
+ end
@@ -0,0 +1,38 @@
1
+ require_relative 'scm'
2
+
3
+ module AgileNotifier
4
+ class Git < SCM
5
+ attr_accessor :repo
6
+
7
+ def initialize(dir)
8
+ @repo = Repository.new(dir)
9
+ end
10
+
11
+ class << self
12
+ def combine_commands(*commands)
13
+ separator = ' && '
14
+ combined_commands = commands.inject('') do |cmds, cmd|
15
+ cmds += "#{cmd}#{separator}"
16
+ end
17
+ combined_commands.gsub!(/#{separator}$/, '')
18
+ end
19
+
20
+ def run_command(command)
21
+ `#{command}`
22
+ end
23
+ end
24
+
25
+ class Repository < SCM::Repository
26
+ def initialize(repo)
27
+ @repo = repo
28
+ end
29
+
30
+ def get_committer_of_a_commit(revision)
31
+ go_to_repo = "cd #{@repo}"
32
+ show_author_name = "git show #{revision} --pretty=format:%an | head -1"
33
+ cmd = Git.combine_commands(go_to_repo, show_author_name)
34
+ Git.run_command(cmd).chomp
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,57 @@
1
+ require 'json'
2
+ require 'httparty'
3
+ require_relative 'scm'
4
+ require_relative 'response_helper'
5
+
6
+ module AgileNotifier
7
+ class Github < SCM
8
+ extend ResponseHelper
9
+
10
+ ENTERPRISE_API = '/api/v3'
11
+ USERAGENT = 'AgileNotifier'
12
+
13
+ def initialize(url)
14
+ super
15
+ if url.include?(ENTERPRISE_API)
16
+ status_url = url + '/zen'
17
+ begin
18
+ status = HTTParty.get(status_url).code
19
+ availability = ( status == 200 )
20
+ rescue => e
21
+ puts e.message
22
+ availability = false
23
+ end
24
+ else
25
+ status_url = url.gsub(/:\/\/api\./, '://status.') + '/api/status.json'
26
+ status = self.class.get_value('status', status_url)
27
+ availability = ( status == 'good' )
28
+ end
29
+ raise('Github is not available.') unless availability
30
+ end
31
+
32
+ class << self
33
+ def new_enterprise_version(url)
34
+ new(url + ENTERPRISE_API)
35
+ end
36
+
37
+ def get_value(key, url)
38
+ get_value_of_key(key, url, :headers => {'User-Agent' => USERAGENT})
39
+ end
40
+ end
41
+
42
+ def add_repository(args)
43
+ user = args[:user]
44
+ repo = args[:repo]
45
+ repository = Repository.new(user: user, repo: repo, url: @url)
46
+ @repositories.push(repository)
47
+ end
48
+
49
+ class Repository < SCM::Repository
50
+ def get_committer_name_of_a_commit(revision)
51
+ url = @url + "/repos/#{@user}/#{@repo}/git/commits/#{revision}"
52
+ committer = Github.get_value('committer', url)
53
+ committer['name']
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,19 @@
1
+ module AgileNotifier
2
+ class ITS
3
+ def initialize(url)
4
+ @url = url
5
+ end
6
+
7
+ class Project
8
+ def initialize(name)
9
+ @name = name
10
+ end
11
+ end
12
+
13
+ class Issue
14
+ def initialize(id)
15
+ @id = id
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,99 @@
1
+ require 'uri'
2
+ require 'json'
3
+ require 'httparty'
4
+ require_relative 'ci'
5
+ require_relative 'response_helper'
6
+
7
+ module AgileNotifier
8
+ class Jenkins < CI
9
+ extend ResponseHelper
10
+
11
+ JSON_API = '/api/json'
12
+
13
+ def self.get_value(key, url)
14
+ get_value_of_key(key, url.gsub(/\/$/, '') + JSON_API)
15
+ end
16
+
17
+ def initialize(url, *names)
18
+ @url = url
19
+ if names.empty?
20
+ @jobs = get_all_jobs
21
+ else
22
+ @jobs = []
23
+ names.each do |name|
24
+ job_url = URI.encode("#{@url}/job/#{name}/")
25
+ @jobs.push(Job.new(name, job_url))
26
+ end
27
+ end
28
+ end
29
+
30
+ def get_all_jobs
31
+ jobs = self.class.get_value('jobs', @url)
32
+ if jobs.nil?
33
+ return nil
34
+ else
35
+ jobs.inject([]) do |all_jobs, job|
36
+ all_jobs.push(Job.new(job['name'], job['url']))
37
+ end
38
+ end
39
+ end
40
+
41
+ class Job < CI::Job
42
+ def get_last_build
43
+ last_build = Jenkins.get_value('lastBuild', @url)
44
+ last_build.nil? ? nil : Build.new(last_build['number'], last_build['url'])
45
+ end
46
+
47
+ def get_penultimate_build
48
+ last_build.get_previous_build
49
+ end
50
+
51
+ class Build < CI::Job::Build
52
+ include Servable
53
+
54
+ def get_result
55
+ result = Jenkins.get_value('result', @url)
56
+ result.nil? ? nil : result
57
+ end
58
+
59
+ def get_revision
60
+ revision = Jenkins.get_value('lastBuiltRevision', @url)
61
+ revision.nil? ? nil : revision['SHA1']
62
+ end
63
+
64
+ def get_previous_build
65
+ previous_number = @number - 1
66
+ while previous_number > 0
67
+ previous_url = @url.gsub(/\/#{@number}\//, "/#{previous_number}/")
68
+ if is_available?(previous_url)
69
+ return Build.new(previous_number, previous_url)
70
+ else
71
+ previous_number -= 1
72
+ end
73
+ end
74
+ return nil
75
+ end
76
+
77
+ def get_previous_result
78
+ get_previous_build.get_result
79
+ end
80
+
81
+ def passed?
82
+ @result == 'SUCCESS'
83
+ end
84
+
85
+ def failed?
86
+ @result == 'FAILURE'
87
+ end
88
+
89
+ def fixed?
90
+ if get_previous_result == 'FAILURE'
91
+ return passed?
92
+ else
93
+ return nil # if previous result is SUCCESS, doesn't make sense, then return nil
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,7 @@
1
+ require_relative 'its'
2
+
3
+ module AgileNotifier
4
+ class Jira < ITS
5
+
6
+ end
7
+ end
@@ -0,0 +1,30 @@
1
+ require_relative 'tts'
2
+
3
+ module AgileNotifier
4
+ class Judger
5
+ class << self
6
+ def on_fail(trackable, text, args)
7
+ on_condition(trackable.failed?, text, args)
8
+ end
9
+
10
+ def on_pass(trackable, text, args)
11
+ on_condition(trackable.passed?, text, args)
12
+ end
13
+
14
+ def on_fix(trackable, text, args)
15
+ on_condition(trackable.fixed?, text, args)
16
+ end
17
+
18
+ def on_condition(condition, text, args)
19
+ if condition
20
+ TTS.speak(text, args)
21
+ true
22
+ else
23
+ false
24
+ end
25
+ end
26
+ end
27
+
28
+ private_class_method :on_condition
29
+ end
30
+ end
@@ -0,0 +1,37 @@
1
+ module AgileNotifier
2
+ class OperatingSystem
3
+ TYPE = {
4
+ lin: 'linux',
5
+ mac: 'osx',
6
+ win: 'windows',
7
+ unknown: 'UNKNOWN'
8
+ }
9
+
10
+ class << self
11
+ def is_linux?
12
+ match_os(/linux/)
13
+ end
14
+
15
+ def is_mac?
16
+ match_os(/darwin/)
17
+ end
18
+
19
+ def is_windows?
20
+ match_os(/mswin|mingw|cygwin|bccwin|wince|emx/)
21
+ end
22
+
23
+ def match_os(regex)
24
+ !!RUBY_PLATFORM.match(regex)
25
+ end
26
+
27
+ def what
28
+ return TYPE[:lin] if is_linux?
29
+ return TYPE[:mac] if is_mac?
30
+ return TYPE[:win] if is_windows?
31
+ return TYPE[:unknown]
32
+ end
33
+ end
34
+
35
+ private_class_method :new, :match_os
36
+ end
37
+ end
@@ -0,0 +1,20 @@
1
+ module AgileNotifier
2
+ class Player
3
+ class << self
4
+ def play_on_linux(file)
5
+ # play command comes from sox package, not pre-installed
6
+ system("play #{file}")
7
+ end
8
+
9
+ def play_on_osx(file)
10
+ system("afplay #{file}")
11
+ end
12
+
13
+ def play_on_windows(file)
14
+ raise(NotImplementedError, "Method [#{__method__}] is empty, please implement", caller)
15
+ end
16
+ end
17
+
18
+ private_class_method :new
19
+ end
20
+ end
@@ -0,0 +1,41 @@
1
+ require 'json'
2
+ require 'httparty'
3
+
4
+ module AgileNotifier
5
+ class ResponseError < StandardError
6
+ end
7
+
8
+ module ResponseHelper
9
+ def get_value_of_key(key, *args)
10
+ pm = PrivateMethods.new
11
+ pm.get_value_of_key_from_json(key, pm.get_json_response(*args))
12
+ end
13
+
14
+ class PrivateMethods
15
+ def get_json_response(*args)
16
+ response = HTTParty.get(*args)
17
+ if response.code == 200
18
+ return JSON.parse(response.body)
19
+ else
20
+ raise(ResponseError, "HTTP Status Code: #{response.code} - #{response.parsed_response}", caller)
21
+ end
22
+ end
23
+
24
+ def get_value_of_key_from_json(key, collection) # only returns the first match if duplicated keys exist
25
+ if collection.respond_to?(:each_pair) # behaves like Hash
26
+ return collection[key] if collection.has_key?(key)
27
+ collection.keys.each do |k|
28
+ value = send(__method__, key, collection[k]) # recursive call
29
+ return value if !value.nil?
30
+ end
31
+ elsif collection.respond_to?(:each_index) # behaves like Array
32
+ collection.each do |x|
33
+ value = send(__method__, key, x) # recursive call
34
+ return value if !value.nil?
35
+ end
36
+ end
37
+ return nil
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,40 @@
1
+ module AgileNotifier
2
+ class SCM
3
+ attr_accessor :url, :repositories
4
+
5
+ def initialize(url)
6
+ @url = url
7
+ @repositories = []
8
+ end
9
+
10
+ def add_repository(repository)
11
+ @repositories.push(repository)
12
+ end
13
+
14
+ def repository
15
+ if @repositories.size == 1
16
+ return @repositories.first
17
+ else
18
+ raise('There are more than one repository, please use method [repositories] instead of [repository]')
19
+ end
20
+ end
21
+
22
+ class Repository
23
+ attr_accessor :user, :repo, :url
24
+
25
+ def initialize(args)
26
+ @user = args[:user]
27
+ @repo = args[:repo]
28
+ @url = args[:url]
29
+ end
30
+
31
+ def get_commit(revision)
32
+ raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
33
+ end
34
+
35
+ def get_committer_of_a_commit(revision)
36
+ raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,55 @@
1
+ require 'net/http'
2
+
3
+ module AgileNotifier
4
+ module Servable
5
+ class PrivateMethods
6
+ def is_https_request?(url)
7
+ regex = /https:\/\//
8
+ regex.match(url) ? true : false
9
+ end
10
+
11
+ def is_http_server_available?(url)
12
+ uri = URI(url)
13
+ begin
14
+ res = Net::HTTP.get_response(uri)
15
+ rescue Errno::ECONNREFUSED # server shutdown
16
+ return false
17
+ end
18
+ return is_response_ok?(res)
19
+ end
20
+
21
+ def is_https_server_available?(url)
22
+ uri = URI(url)
23
+ http = Net::HTTP.new(uri.host, uri.port)
24
+ http.use_ssl = true
25
+ req = Net::HTTP::Get.new(uri.request_uri)
26
+ begin
27
+ res = http.request(req)
28
+ rescue
29
+ return false
30
+ end
31
+ return is_response_ok?(res)
32
+ end
33
+
34
+ def is_response_ok?(res)
35
+ status_code = res.code.to_i
36
+ if status_code == 200
37
+ return true
38
+ else # e.g. 503 (Service Unavailable)
39
+ return false
40
+ end
41
+ end
42
+ end
43
+
44
+ def is_available?(url)
45
+ pm = PrivateMethods.new
46
+ pm.instance_eval do
47
+ if is_https_request?(url)
48
+ return is_https_server_available?(url)
49
+ else
50
+ return is_http_server_available?(url)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,15 @@
1
+ module AgileNotifier
2
+ module Trackable
3
+ def failed?
4
+ raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
5
+ end
6
+
7
+ def passed?
8
+ raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
9
+ end
10
+
11
+ def fixed?
12
+ raise(NotImplementedError, "Abstract method [#{__method__}] is called, please implement", caller)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,112 @@
1
+ require 'uri'
2
+ require 'open-uri'
3
+ require_relative 'operating_system'
4
+ require_relative 'player'
5
+
6
+ module AgileNotifier
7
+ class TTS
8
+ class << self
9
+ def speak(text, args)
10
+ os_type = get_os_type
11
+ service = "speak_on_#{os_type}"
12
+ language = args[:language]
13
+ voice = args[:voice]
14
+ Service.send(service.intern, text, language, voice)
15
+ end
16
+
17
+ def get_os_type
18
+ OperatingSystem.what
19
+ end
20
+ end
21
+
22
+ class Service
23
+ @@temp_file = File.dirname(__FILE__) + '/temp_tts'
24
+
25
+ class << self
26
+ def download_file(url, file = @@temp_file)
27
+ open(url) do |remote_file|
28
+ data = remote_file.read
29
+ File.open(file, 'wb') do |local_file|
30
+ local_file.write(data)
31
+ end
32
+ end
33
+ end
34
+
35
+ def remove_file(file = @@temp_file)
36
+ File.delete(file)
37
+ end
38
+
39
+ def play(url)
40
+ download_file(url)
41
+ os_type = get_os_type
42
+ player = "play_on_#{os_type}"
43
+ Player.send(player.intern, @@temp_file)
44
+ remove_file
45
+ end
46
+
47
+ def replace_punctuation_by_space(text)
48
+ text.gsub(/[[:punct:]](\s+)*/, ' ').gsub(/\s$/, '')
49
+ end
50
+
51
+ def encode_www_form(text)
52
+ URI.encode_www_form('' => text)[1..-1]
53
+ end
54
+
55
+ def encode_www_form_and_remove_punctuation(text)
56
+ encode_www_form(replace_punctuation_by_space(text))
57
+ end
58
+
59
+ def speak_on_linux(text, language, voice = nil)
60
+ # TODO: should check service availability first, if not raise exception
61
+ mary_tts(text, language)
62
+ end
63
+
64
+ def speak_on_osx(text, language, voice)
65
+ if voice
66
+ unless osx_speech(voice, text)
67
+ raise("No available voice found, please check if you have downloaded voice [#{voice}] in System Preferences -> Speech")
68
+ end
69
+ else
70
+ list_of_available_voices = `say -v '?'`.split("\n")
71
+ voices = list_of_available_voices.inject({}) do |collection, record|
72
+ matched_results = record.match(/^(.*[^\s])\s+([a-z]{2})_[A-Z]{2}\s+/)
73
+ available_language = matched_results[2].downcase.intern
74
+ available_voice = matched_results[1]
75
+ if collection.has_key?(available_language)
76
+ collection[available_language] << available_voice
77
+ else
78
+ collection[available_language] = [available_voice]
79
+ end
80
+ collection
81
+ end
82
+ osx_speech(voices[language].first, text)
83
+ end
84
+ end
85
+
86
+ def speak_on_windows(text, language, voice = nil)
87
+ raise(NotImplementedError, "Method [#{__method__}] is empty, please implement", caller)
88
+ end
89
+
90
+ def osx_speech(voice, text)
91
+ system("say -v #{voice} #{text} > /dev/null 2>&1")
92
+ end
93
+
94
+ def tts_api(text)
95
+ url = 'http://tts-api.com/tts.mp3?q='
96
+ play(url + encode_www_form_and_remove_punctuation(text))
97
+ end
98
+
99
+ def mary_tts(text, language)
100
+ url = 'http://mary.dfki.de:59125/'
101
+ text = encode_www_form_and_remove_punctuation(text)
102
+ audio_format = 'WAVE_FILE'
103
+ play(url + "process?INPUT_TEXT=#{text}&INPUT_TYPE=TEXT&OUTPUT_TYPE=AUDIO&LOCALE=#{language}&AUDIO=#{audio_format}")
104
+ end
105
+ end
106
+
107
+ private_class_method :download_file, :remove_file, :play, :replace_punctuation_by_space, :encode_www_form, :encode_www_form_and_remove_punctuation, :osx_speech
108
+ end
109
+
110
+ private_class_method :new, :get_os_type
111
+ end
112
+ end
@@ -0,0 +1,5 @@
1
+ Dir[(File.expand_path(File.dirname(__FILE__)) + "/agile_notifier/*.rb")].each { |file| require file }
2
+
3
+ module AgileNotifier
4
+ VERSION = '1.0'
5
+ end
@@ -0,0 +1,22 @@
1
+ class Module
2
+ def backup_const(name)
3
+ redef_const(:TEMP_BACKUP, const_get(name))
4
+ end
5
+
6
+ def restore_const(name)
7
+ redef_const(name, const_get(:TEMP_BACKUP))
8
+ remove_const(:TEMP_BACKUP)
9
+ end
10
+
11
+ def redef_const(name, value)
12
+ remove_const(name) if const_defined?(name)
13
+ const_set(name, value)
14
+ end
15
+
16
+ def stub_const_and_test(const_name, const_value, &test_blk)
17
+ backup_const(const_name)
18
+ redef_const(const_name, const_value)
19
+ test_blk.call
20
+ restore_const(const_name)
21
+ end
22
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,6 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), *%w[.. lib agile_notifier])))
2
+ require 'agile_notifier'
3
+ require 'test/unit'
4
+ require 'mocha/setup'
5
+
6
+ include AgileNotifier
@@ -0,0 +1,16 @@
1
+ require_relative 'helper'
2
+ require_relative 'ext/module'
3
+
4
+ class TestOperatingSystem < Test::Unit::TestCase
5
+ def test_is_mac?
6
+ Object.stub_const_and_test(:RUBY_PLATFORM, 'x86_64-darwin11.2.0') { assert_equal(true, OperatingSystem.is_mac?) }
7
+ end
8
+
9
+ def test_is_linux?
10
+ Object.stub_const_and_test(:RUBY_PLATFORM, 'x86_64-linux') { assert_equal(true, OperatingSystem.is_linux?) }
11
+ end
12
+
13
+ def test_is_windows?
14
+ Object.stub_const_and_test(:RUBY_PLATFORM, 'i386-mingw32') { assert_equal(true, OperatingSystem.is_windows?) }
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ require_relative 'helper'
2
+
3
+ class TestResponseHelper < Test::Unit::TestCase
4
+ def setup
5
+ @pm = ResponseHelper::PrivateMethods.new
6
+ end
7
+
8
+ def test_get_value_of_key_from_json
9
+ json = {'actions' => [{}, {}, {'buildsByBranchName' => {'origin/master' => {'buildNumber' => 3, 'revision' => {'SHA1' => '08a2be82ba83c1e89e01f698a30203fb0284aa33', 'branch' => [{'SHA1' => '4eac3c2c5b8f2daddeaa37926e7c1f55cd756f1f', 'name' => 'origin/master'}, {'SHA1' => '08a2be82ba83c1e89e01f698a30203fb0284aa33', 'name' => 'origin/HEAD'}]}}}}], 'description' => 'just a test', 'displayName' => 'Test Project', 'url' => 'http://localhost:8080/job/Test%20Project/', 'builds' => [{'number' => 3, 'url' => 'http://localhost:8080/job/Test%20Project/3/'}, {'number' => 2, 'url' => 'http://localhost:8080/job/Test%20Project/2/'}, {'number' => 1, 'url' => 'http://localhost:8080/job/Test%20Project/1/'}], 'lastFailedBuild' => {'number' => 2, 'url' => 'http://localhost:8080/job/Test%20Project/2/'}, 'lastSuccessfulBuild' => {'number' => 3, 'url' => 'http://localhost:8080/job/Test%20Project/3/'}}
10
+ assert_equal('origin/master', @pm.get_value_of_key_from_json('name', json)) # find first occurrence
11
+ assert_equal('08a2be82ba83c1e89e01f698a30203fb0284aa33', @pm.get_value_of_key_from_json('SHA1', json)) # find first occurrence
12
+ assert_equal(3, @pm.get_value_of_key_from_json('number', json)) # find first occurrence
13
+ assert_equal('Test Project', @pm.get_value_of_key_from_json('displayName', json))
14
+ assert_equal('http://localhost:8080/job/Test%20Project/3/', @pm.get_value_of_key_from_json('lastSuccessfulBuild', json)['url'])
15
+ assert_equal(nil, @pm.get_value_of_key_from_json('sexyGirl', json)) # nonexistent key should return nil
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'helper'
2
+
3
+ class TestServable < Test::Unit::TestCase
4
+ def setup
5
+ @obj = Servable::PrivateMethods.new
6
+ end
7
+
8
+ def test_is_https_request
9
+ assert_equal(true, @obj.is_https_request?('https://example.com/'))
10
+ end
11
+
12
+ def test_is_not_https_request
13
+ assert_equal(false, @obj.is_https_request?('http://example.com/'))
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: agile_notifier
3
+ version: !ruby/object:Gem::Version
4
+ version: '1.0'
5
+ platform: ruby
6
+ authors:
7
+ - Jing Li
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-05-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 1.8.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 1.8.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: httparty
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 0.11.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 0.11.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: mocha
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 0.14.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.14.0
55
+ description: agile_notifier alerts you via making wonderful noises, make software
56
+ development more fun
57
+ email:
58
+ - thyrlian@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - Gemfile
64
+ - Gemfile.lock
65
+ - README.md
66
+ - Rakefile
67
+ - agile_notifier.gemspec
68
+ - lib/agile_notifier.rb
69
+ - lib/agile_notifier/ci.rb
70
+ - lib/agile_notifier/composer.rb
71
+ - lib/agile_notifier/configuration.rb
72
+ - lib/agile_notifier/git.rb
73
+ - lib/agile_notifier/github.rb
74
+ - lib/agile_notifier/its.rb
75
+ - lib/agile_notifier/jenkins.rb
76
+ - lib/agile_notifier/jira.rb
77
+ - lib/agile_notifier/judger.rb
78
+ - lib/agile_notifier/operating_system.rb
79
+ - lib/agile_notifier/player.rb
80
+ - lib/agile_notifier/response_helper.rb
81
+ - lib/agile_notifier/scm.rb
82
+ - lib/agile_notifier/servable.rb
83
+ - lib/agile_notifier/trackable.rb
84
+ - lib/agile_notifier/tts.rb
85
+ - test/ext/module.rb
86
+ - test/helper.rb
87
+ - test/test_operating_system.rb
88
+ - test/test_response_helper.rb
89
+ - test/test_servable.rb
90
+ homepage: https://github.com/thyrlian/AgileNotifier
91
+ licenses:
92
+ - MIT
93
+ metadata: {}
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ! '>='
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubyforge_project:
110
+ rubygems_version: 2.1.4
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: agile_notifier alerts you via making wonderful noises
114
+ test_files:
115
+ - test/ext/module.rb
116
+ - test/helper.rb
117
+ - test/test_operating_system.rb
118
+ - test/test_response_helper.rb
119
+ - test/test_servable.rb