hot_catch 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +27 -0
- data/Rakefile +34 -0
- data/lib/generators/hot_catch/install_generator.rb +57 -0
- data/lib/generators/hot_catch/uninstall_generator.rb +28 -0
- data/lib/hot_catch.rb +3 -0
- data/lib/hot_catch/custom_log_subscribers.rb +67 -0
- data/lib/hot_catch/hot_catch_log.rb +60 -0
- data/lib/hot_catch/hot_catch_logger.rb +114 -0
- data/lib/hot_catch/version.rb +3 -0
- data/lib/tasks/hot_catch_tasks.rake +4 -0
- metadata +55 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ce4c89223725b3c189db6f67ccca14d5d3f2f92a
|
4
|
+
data.tar.gz: 6cb563238d8ee4e61b38900ada6e32909328f81a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a821681cac3f21fd3b43f1486a523e9adbe368af0b4b2abc6b0e7c0de6460d8ac269ad8cdc63e319fd9027d400102a0f644c82ae1ff2cae5e453a9b5aa51f62e
|
7
|
+
data.tar.gz: 2172336cfc52925ce9b3d1171d0d47024051350471d3fb5c4bd187df44226d205089177dd0e38eabefe012b89dee501addf7ba3b91409faedf2f20368c9873c5
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2017 Davhot
|
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/README.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# HotCatch
|
2
|
+
Данный гем позволяет изменить логирование в базу данных.
|
3
|
+
В дальнейшем будет отправлять логи главному приложению.
|
4
|
+
|
5
|
+
## Использование
|
6
|
+
Логи можно посмотреть в консоли:
|
7
|
+
```ruby
|
8
|
+
> HotCatchLog.all
|
9
|
+
> HotCatchLog.first.log_data
|
10
|
+
```
|
11
|
+
В данной реализации логи разделены по статусам запросов (поле `status HotCatchLog`).
|
12
|
+
Кроме того одинаковые ошибки собираются в одну, при этом увеличивается счётчик в моделе (поле `count_log HotCatchLog`).
|
13
|
+
|
14
|
+
## Установка
|
15
|
+
1. Добавить гем в `Gemfile`
|
16
|
+
```ruby
|
17
|
+
gem 'hot_catch'
|
18
|
+
```
|
19
|
+
2. `$ bundle install`
|
20
|
+
3. Запустить генератор установки: `$ rails generate hot_catch:install`
|
21
|
+
Для удаления: `$ rails generate hot_catch:uninstall`
|
22
|
+
4. `$ rails db:migrate`
|
23
|
+
|
24
|
+
Это всё! Теперь все логи формируются в базе данных.
|
25
|
+
|
26
|
+
## License
|
27
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'HotCatch'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'lib'
|
28
|
+
t.libs << 'test'
|
29
|
+
t.pattern = 'test/**/*_test.rb'
|
30
|
+
t.verbose = false
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
task default: :test
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# rails generate testgem:install
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
|
5
|
+
module HotCatch
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
7
|
+
# Включение всех файлов из указанной папки относительно __FILE__ (то есть текущего файла)
|
8
|
+
source_root File.expand_path('../../../hot_catch', __FILE__)
|
9
|
+
# include Rails::Generators::Migration
|
10
|
+
|
11
|
+
def add_model_hot_catch
|
12
|
+
create_file "db/migrate/20170729154223_create_hot_catch_log.rb" do
|
13
|
+
<<RUBY
|
14
|
+
class CreateHotCatchLog < ActiveRecord::Migration[5.0]
|
15
|
+
def change
|
16
|
+
create_table :hot_catch_logs do |t|
|
17
|
+
t.string :log_data, null: false
|
18
|
+
t.integer :count_log, default: 1, null: false
|
19
|
+
t.string :status, null: false
|
20
|
+
|
21
|
+
t.timestamps
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
RUBY
|
26
|
+
end
|
27
|
+
|
28
|
+
create_file "app/models/application_record.rb" do
|
29
|
+
"class ApplicationRecord < ActiveRecord::Base
|
30
|
+
self.abstract_class = true
|
31
|
+
end"
|
32
|
+
end unless File.exist? "app/models/application_record.rb"
|
33
|
+
|
34
|
+
|
35
|
+
copy_file "hot_catch_log.rb", "app/models/hot_catch_log.rb"
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def add_rack_logger
|
40
|
+
copy_file "hot_catch_logger.rb", "config/hot_catch_logger.rb"
|
41
|
+
end
|
42
|
+
|
43
|
+
def change_files
|
44
|
+
insert_into_file "config/application.rb", "\nrequire_relative 'hot_catch_logger'\n", :before => "module ExampleApp"
|
45
|
+
|
46
|
+
application "config.middleware.insert_before Rails::Rack::Logger, Rails::Rack::HotCatchLogger\n
|
47
|
+
config.middleware.delete Rails::Rack::Logger"
|
48
|
+
|
49
|
+
insert_string = "\n require 'hot_catch/custom_log_subscribers.rb'
|
50
|
+
config.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new('hot_catch_buf_file'))\n"
|
51
|
+
|
52
|
+
insert_into_file "config/environments/development.rb", insert_string, :after => "Rails.application.configure do"
|
53
|
+
insert_into_file "config/environments/production.rb", insert_string, :after => "Rails.application.configure do"
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# rails generate testgem:install
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
|
5
|
+
module HotCatch
|
6
|
+
class UninstallGenerator < Rails::Generators::Base
|
7
|
+
# include Rails::Generators::Migration
|
8
|
+
|
9
|
+
def remove_model_hot_catch
|
10
|
+
remove_file "db/migrate/20170729154223_create_hot_catch_log.rb"
|
11
|
+
remove_file "app/models/hot_catch_log.rb"
|
12
|
+
end
|
13
|
+
|
14
|
+
def remove_rack_logger
|
15
|
+
remove_file "config/hot_catch_logger.rb"
|
16
|
+
end
|
17
|
+
|
18
|
+
def unchange_files
|
19
|
+
gsub_file "config/application.rb", /require_relative 'hot_catch_logger'/, ""
|
20
|
+
gsub_file "config/application.rb", /config.*Logger/, ""
|
21
|
+
|
22
|
+
gsub_file "config/environments/development.rb", /.*\s.*hot_catch_buf_file'\)\)/, ""
|
23
|
+
gsub_file "config/environments/production.rb", /.*\s.*hot_catch_buf_file'\)\)/, ""
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
data/lib/hot_catch.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'active_record/log_subscriber'
|
2
|
+
|
3
|
+
class CustomActiveRecordLogSubscriber < ActiveRecord::LogSubscriber
|
4
|
+
# Убрана подсветка и не логируется таблица hot_catch_buf_file
|
5
|
+
def sql(event)
|
6
|
+
self.class.runtime += event.duration
|
7
|
+
return unless logger.debug?
|
8
|
+
|
9
|
+
payload = event.payload
|
10
|
+
|
11
|
+
return if IGNORE_PAYLOAD_NAMES.include?(payload[:name])
|
12
|
+
|
13
|
+
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
|
14
|
+
sql = payload[:sql]
|
15
|
+
binds = nil
|
16
|
+
|
17
|
+
unless (payload[:binds] || []).empty?
|
18
|
+
casted_params = type_casted_binds(payload[:binds], payload[:type_casted_binds])
|
19
|
+
binds = " " + payload[:binds].zip(casted_params).map { |attr, value|
|
20
|
+
render_bind(attr, value)
|
21
|
+
}.inspect
|
22
|
+
end
|
23
|
+
|
24
|
+
# name = colorize_payload_name(name, payload[:name])
|
25
|
+
# sql = color(sql, sql_color(sql), true)
|
26
|
+
|
27
|
+
debug " #{name} #{sql}#{binds}" unless payload[:sql].to_s =~ /hot_catch_logs|COMMIT|BEGIN/
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Отписка от уведомлений ActiveRecord::LogSubscriber
|
32
|
+
notifier = ActiveSupport::Notifications.notifier
|
33
|
+
subscribers = notifier.listeners_for("sql.active_record")
|
34
|
+
subscribers.each {|s| ActiveSupport::Notifications.unsubscribe s }
|
35
|
+
|
36
|
+
# Подписка на уведомления CustomActiveRecordLogSubscriber
|
37
|
+
CustomActiveRecordLogSubscriber.attach_to :active_record
|
38
|
+
# ====================================================================================
|
39
|
+
require 'action_controller/log_subscriber'
|
40
|
+
|
41
|
+
class CustomActionControllerLogSubscriber < ActionController::LogSubscriber
|
42
|
+
# Статус записывается в лог файл hot_catch_buf_file
|
43
|
+
def process_action(event)
|
44
|
+
payload = event.payload
|
45
|
+
additions = ActionController::Base.log_process_action(payload)
|
46
|
+
|
47
|
+
status = payload[:status]
|
48
|
+
if status.nil? && payload[:exception].present?
|
49
|
+
exception_class_name = payload[:exception].first
|
50
|
+
status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
|
51
|
+
end
|
52
|
+
|
53
|
+
File.open('hot_catch_buf_file', 'a'){ |file| file.puts "!!!#{status}!!!" }
|
54
|
+
|
55
|
+
info do
|
56
|
+
|
57
|
+
message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
|
58
|
+
message << " (#{additions.join(" | ")})" unless additions.blank?
|
59
|
+
message
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
subscribers = notifier.listeners_for("process_action.action_controller")
|
65
|
+
subscribers.each {|s| ActiveSupport::Notifications.unsubscribe s }
|
66
|
+
|
67
|
+
CustomActionControllerLogSubscriber.attach_to :action_controller
|
@@ -0,0 +1,60 @@
|
|
1
|
+
class HotCatchLog < ApplicationRecord
|
2
|
+
|
3
|
+
validates :log_data, :status, presence: true
|
4
|
+
validates :count_log, presence: true, numericality: {only_integer: true, greater_than: 0}
|
5
|
+
|
6
|
+
STATUSES = ["STATUS", "SUCCESS", "REDIRECTION", "CLIENT_ERROR", "SERVER_ERROR", "WARNING"]
|
7
|
+
|
8
|
+
# Проверка лога на уникальность
|
9
|
+
def self.process_data_log(str)
|
10
|
+
str, status = get_status(str)
|
11
|
+
|
12
|
+
# Если статус не найден, то возможна ошибка в контроллере
|
13
|
+
status = STATUSES[3] if !status && str =~ /ActionController/
|
14
|
+
|
15
|
+
str2 = strip_str(str)
|
16
|
+
opt = true
|
17
|
+
|
18
|
+
HotCatchLog.where(status: status).each do |cur_log|
|
19
|
+
if strip_str(cur_log.log_data) == str2
|
20
|
+
cur_log.count_log += 1
|
21
|
+
cur_log.save
|
22
|
+
opt = false
|
23
|
+
return
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
if opt && str.strip.present? && status.present?
|
28
|
+
HotCatchLog.create(log_data: format_log(str), status: status)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Метод для сравнения логов
|
36
|
+
def self.strip_str(str)
|
37
|
+
str = str.dup
|
38
|
+
str.gsub!(/\d+(\.\d+)?ms/, "") # За сколько выполнилось действие
|
39
|
+
str.gsub!(/\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\s\+\d{4}/, "") # дата
|
40
|
+
str.gsub!(/#<.*:0x[\d|\w]+>/, "") # Объект (например #<StaticPagesController:0x007f37791686d8>)
|
41
|
+
format_log(str)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Помимо сравнения логов, нужен для форматирования лога и записи в бд
|
45
|
+
def self.format_log(str)
|
46
|
+
str = str.split("\n").select{|x| x.present?}.join("\n") # убирает повторяющиеся переводы строки
|
47
|
+
str.strip! # лишние символы пропусков в начале и конце
|
48
|
+
str
|
49
|
+
end
|
50
|
+
|
51
|
+
# Возвращает статус и лог без статуса
|
52
|
+
def self.get_status(str)
|
53
|
+
status = str.match(/!!!(\d{3})!!!/)
|
54
|
+
status = status[1] if status
|
55
|
+
str = str.gsub(/!!!(\d{3})!!!\s/, "")
|
56
|
+
status = STATUSES[status[0].to_i - 1] if status.present?
|
57
|
+
[str, status]
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'active_support/core_ext/time/conversions'
|
2
|
+
require 'active_support/core_ext/object/blank'
|
3
|
+
require 'active_support/log_subscriber'
|
4
|
+
require 'action_dispatch/http/request'
|
5
|
+
require 'rack/body_proxy'
|
6
|
+
# require 'rack/utils'
|
7
|
+
|
8
|
+
require_relative '../app/models/application_record.rb'
|
9
|
+
require_relative '../app/models/hot_catch_log.rb'
|
10
|
+
|
11
|
+
module Rails
|
12
|
+
module Rack
|
13
|
+
# Sets log tags, logs the request, calls the app, and flushes the logs.
|
14
|
+
#
|
15
|
+
# Log tags (+taggers+) can be an Array containing: methods that the +request+
|
16
|
+
# object responds to, objects that respond to +to_s+ or Proc objects that accept
|
17
|
+
# an instance of the +request+ object.
|
18
|
+
class HotCatchLogger < ActiveSupport::LogSubscriber
|
19
|
+
def initialize(app, taggers = nil)
|
20
|
+
@app = app
|
21
|
+
@taggers = taggers || []
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(env)
|
25
|
+
request = ActionDispatch::Request.new(env)
|
26
|
+
|
27
|
+
if logger.respond_to?(:tagged)
|
28
|
+
logger.tagged(compute_tags(request)) { call_app(request, env) }
|
29
|
+
else
|
30
|
+
call_app(request, env)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def call_app(request, env)
|
37
|
+
instrumenter = ActiveSupport::Notifications.instrumenter
|
38
|
+
instrumenter.start 'request.action_dispatch', request: request
|
39
|
+
|
40
|
+
# Флаг начала запроса
|
41
|
+
logger << "\nBEGIN\n"
|
42
|
+
|
43
|
+
logger.info { started_request_message(request) }
|
44
|
+
resp = @app.call(env)
|
45
|
+
resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) }
|
46
|
+
resp
|
47
|
+
rescue Exception
|
48
|
+
finish(request)
|
49
|
+
raise
|
50
|
+
ensure
|
51
|
+
ActiveSupport::LogSubscriber.flush_all!
|
52
|
+
end
|
53
|
+
|
54
|
+
# Started GET "/session/new" for 127.0.0.1 at 2012-09-26 14:51:42 -0700
|
55
|
+
def started_request_message(request)
|
56
|
+
'Started %s "%s" for %s at %s' % [
|
57
|
+
request.request_method,
|
58
|
+
request.filtered_path,
|
59
|
+
request.ip,
|
60
|
+
Time.now.to_default_s ]
|
61
|
+
end
|
62
|
+
|
63
|
+
def compute_tags(request)
|
64
|
+
@taggers.collect do |tag|
|
65
|
+
case tag
|
66
|
+
when Proc
|
67
|
+
tag.call(request)
|
68
|
+
when Symbol
|
69
|
+
request.send(tag)
|
70
|
+
else
|
71
|
+
tag
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def finish(request)
|
79
|
+
# Если быстро обновлять страницу, то конец лога не успевает записаться.
|
80
|
+
# Для этого записываем последний лог в новый файл и с новым запросом считываем
|
81
|
+
# его и записанный конец лога из изначального лог-файла.
|
82
|
+
|
83
|
+
# Считали данные из основного лог-файла
|
84
|
+
logs = ""
|
85
|
+
File.open('hot_catch_buf_file'){ |file| logs = file.read}
|
86
|
+
File.open('hot_catch_buf_file', 'w'){ |file| file.print "" }
|
87
|
+
logs = logs.split("BEGIN").delete_if{|x| !x.present? }
|
88
|
+
|
89
|
+
# Считали данные из дополнительного лог-файла и записали начало последнего лога
|
90
|
+
add_log = ""
|
91
|
+
File.open('hot_catch_buf_file2'){ |file| add_log = file.read} if File.exist?('hot_catch_buf_file2')
|
92
|
+
File.open('hot_catch_buf_file2', 'w'){ |file| file.print logs[-1] }
|
93
|
+
logs.delete_at(-1)
|
94
|
+
|
95
|
+
# Собираем начало лога и конец
|
96
|
+
add_log = add_log + (logs[0]).to_s
|
97
|
+
HotCatchLog.process_data_log(add_log) unless add_log.strip =~ /assets/
|
98
|
+
|
99
|
+
logs.delete_at(0)
|
100
|
+
# Обрабатываем остальные логи, если есть
|
101
|
+
logs.each {|log| HotCatchLog.process_data_log(log) unless log.strip =~ /assets/}
|
102
|
+
|
103
|
+
|
104
|
+
|
105
|
+
instrumenter = ActiveSupport::Notifications.instrumenter
|
106
|
+
instrumenter.finish 'request.action_dispatch', request: request
|
107
|
+
end
|
108
|
+
|
109
|
+
def logger
|
110
|
+
Rails.logger
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hot_catch
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Davhot
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-08-09 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Description of Testgem.
|
14
|
+
email:
|
15
|
+
- david.home@mail.ru
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- MIT-LICENSE
|
21
|
+
- README.md
|
22
|
+
- Rakefile
|
23
|
+
- lib/generators/hot_catch/install_generator.rb
|
24
|
+
- lib/generators/hot_catch/uninstall_generator.rb
|
25
|
+
- lib/hot_catch.rb
|
26
|
+
- lib/hot_catch/custom_log_subscribers.rb
|
27
|
+
- lib/hot_catch/hot_catch_log.rb
|
28
|
+
- lib/hot_catch/hot_catch_logger.rb
|
29
|
+
- lib/hot_catch/version.rb
|
30
|
+
- lib/tasks/hot_catch_tasks.rake
|
31
|
+
homepage: https://myhomepage.com
|
32
|
+
licenses:
|
33
|
+
- MIT
|
34
|
+
metadata: {}
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
requirements: []
|
50
|
+
rubyforge_project:
|
51
|
+
rubygems_version: 2.5.1
|
52
|
+
signing_key:
|
53
|
+
specification_version: 4
|
54
|
+
summary: Summary of Testgem.
|
55
|
+
test_files: []
|