cutting_edge 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,46 @@
1
+ require 'rubygems'
2
+
3
+ module VersionRequirementComparator
4
+
5
+ # Operators that require the checked version to be higher than the required version
6
+ HIGHER_THAN = ['>', '>=']
7
+
8
+ def version_requirement_diff(requirement, latest_version)
9
+ segments = latest_version.canonical_segments
10
+
11
+ # Since we know `latest_version` is higher than the requirement, we can and must ignore cases where the constraint fails because `check_version` is lower
12
+ # Such cases would be caused by the fact that we are generating `check_version` below by chopping off the version decimals after `position`
13
+ # For example: `latest_version` is 3.5. On the first pass of this loop, we will thus be checking version 3.0.
14
+ # This would fail a ~> 3.2 requirement on the first pass, falsely yielding an `:outdated_major`.
15
+ # Therefore, filter out the HIGHER_THAN operators
16
+ constraints = requirement.requirements.map do |comparator, version|
17
+ if comparator == '~>'
18
+ Gem::Requirement.new("< #{version.bump.to_s}") # Use only the upper bound requirement of a ~> comparison
19
+ elsif !HIGHER_THAN.include?(comparator)
20
+ Gem::Requirement.new("#{comparator} #{version.to_s}")
21
+ end
22
+ end
23
+ constraints.compact!
24
+
25
+ segments.each_with_index do |_v, position|
26
+ check_version = Gem::Version.new(segments[0..position].join('.'))
27
+ constraints.each do |constraint|
28
+ unless constraint.satisfied_by?(check_version)
29
+ return version_difference_type(position)
30
+ end
31
+ end
32
+ end
33
+ return :unknown # This can occur if latest_version is actually lower than the requirement (e.g., when a newer version of a gem has been yanked)
34
+ end
35
+
36
+ def version_difference_type(position)
37
+ case position
38
+ when 0
39
+ :outdated_major
40
+ when 1
41
+ :outdated_minor
42
+ else
43
+ :outdated_patch
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,22 @@
1
+ require File.expand_path('../../badge.rb', __FILE__)
2
+ require File.expand_path('../helpers.rb', __FILE__)
3
+
4
+ class BadgeWorker < GenericWorker
5
+
6
+ def perform(identifier)
7
+ log_info 'Running Worker!'
8
+ dependencies = get_from_store(identifier)
9
+ if dependencies
10
+ status = generate_status(dependencies[:outdated])
11
+ add_to_store("svg-#{identifier}", Badge.build_badge(status, dependencies[:outdated_total]))
12
+ end
13
+ GC.start
14
+ end
15
+
16
+ private
17
+
18
+ def generate_status(status)
19
+ # status is more specific than Badge.build_badge currently expects, so make it a bit less so
20
+ status == :up_to_date ? status : :out_of_date
21
+ end
22
+ end
@@ -0,0 +1,100 @@
1
+ require 'rubygems'
2
+ require 'http'
3
+ require 'moneta'
4
+ require File.expand_path('../../versions.rb', __FILE__)
5
+ require File.expand_path('../../langs.rb', __FILE__)
6
+ require File.expand_path('../helpers.rb', __FILE__)
7
+
8
+ class DependencyWorker < GenericWorker
9
+ include VersionRequirementComparator
10
+
11
+ # Order is significant for purposes of calculating results[:outdated]
12
+ STATUS_TYPES = [:outdated_major, :outdated_minor, :outdated_patch, :ok, :no_requirement, :unknown]
13
+ OUTDATED_TYPES = STATUS_TYPES[0..-3]
14
+
15
+ def perform(identifier, lang, locations, dependency_types)
16
+ log_info 'Running Worker!'
17
+ dependencies = {}
18
+ locations.each do |name, url|
19
+ contents = http_get(url)
20
+ dependencies[name] = get_results(get_lang(lang).parse_file(name, contents), dependency_types)
21
+ end
22
+ dependencies.merge!(generate_stats(dependencies))
23
+ add_to_store(identifier, dependencies)
24
+ GC.start
25
+ end
26
+
27
+ private
28
+
29
+ def get_results(dependencies, dependency_types)
30
+ results = {}
31
+ STATUS_TYPES.each {|type| results[type] = []}
32
+ if dependencies
33
+ dependencies.select! {|dep| dependency_types.include?(dep.first.type)}
34
+ dependencies.each do |dep, latest_version|
35
+ dependency_hash = dependency(dep.name, dep.requirement.to_s, latest_version.to_s, dep.type)
36
+ if dependency_hash[:required] == 'unknown'
37
+ results[:no_requirement] << dependency_hash
38
+ elsif latest_version.nil?
39
+ results[:unknown] << dependency_hash
40
+ elsif is_outdated?(dep, latest_version)
41
+ outdated = version_requirement_diff(dep.requirement, latest_version.respond_to?(:last) ? latest_version.last : latest_version)
42
+ results[outdated] << dependency_hash
43
+ else
44
+ results[:ok] << dependency_hash
45
+ end
46
+ end
47
+ end
48
+ results
49
+ end
50
+
51
+ def generate_stats(locations)
52
+ results = {}
53
+ STATUS_TYPES.each do |type|
54
+ num = stats(type, locations)
55
+ results[type] = num
56
+ if OUTDATED_TYPES.include?(type)
57
+ results[:outdated_total] = results[:outdated_total].to_i + num
58
+ results[:outdated] = type unless results[:outdated] || num == 0
59
+ end
60
+ end
61
+ results[:outdated] = :unknown if results[:outdated_total] == 0 && results[:ok] == 0
62
+ results[:outdated] = :up_to_date unless results[:outdated]
63
+ results
64
+ end
65
+
66
+ def stats(stat, locations)
67
+ sum = 0
68
+ locations.each do |name, dependencies|
69
+ sum = sum + dependencies[stat].length
70
+ end
71
+ sum
72
+ end
73
+
74
+ def dependency(name, requirement, latest, type)
75
+ {
76
+ :name => name,
77
+ :required => requirement,
78
+ :latest => latest,
79
+ :type => type
80
+ }
81
+ end
82
+
83
+ def get_lang(lang)
84
+ Object.const_get("#{lang.capitalize}Lang")
85
+ end
86
+
87
+ def http_get(url)
88
+ begin
89
+ response = HTTP.get(url)
90
+ response.status == 200 ? response.to_s : nil
91
+ rescue HTTP::TimeoutError => e
92
+ log_info("Encountered error when fetching latest version of #{name}: #{e.class} #{e.message}")
93
+ end
94
+ end
95
+
96
+ def is_outdated?(dependency, latest_version)
97
+ !dependency.requirement.satisfied_by?(latest_version)
98
+ end
99
+
100
+ end
@@ -0,0 +1,24 @@
1
+ require 'sinatra/logger'
2
+ require 'sucker_punch'
3
+
4
+ module WorkerHelpers
5
+
6
+ def log_info(message)
7
+ logger.info(message) if ::CuttingEdge::App.enable_logging
8
+ end
9
+
10
+ def add_to_store(identifier, dependencies)
11
+ ::CuttingEdge::App.store[identifier] = dependencies
12
+ end
13
+
14
+ def get_from_store(identifier)
15
+ ::CuttingEdge::App.store[identifier]
16
+ end
17
+
18
+ end
19
+
20
+ class GenericWorker
21
+ include ::SuckerPunch::Job
22
+ include ::WorkerHelpers
23
+ include ::SemanticLogger::Loggable
24
+ end
@@ -0,0 +1,89 @@
1
+ REQUIREMENT_TXT = <<EOF
2
+ requests>=2.0
3
+ oauthlib
4
+ requests-oauthlib>=1.0.0 [PDF]
5
+ -e svn+http://myrepo/svn/MyApp#egg=MyApp
6
+ Flask>=0.7
7
+ urlobject==1.0
8
+ six
9
+ EOF
10
+
11
+ # See https://www.python.org/dev/peps/pep-0440/#version-exclusion
12
+ # And https://github.com/pypa/pipfile
13
+ PIPFILE = <<EOF
14
+ [[source]]
15
+ url = 'https://pypi.python.org/simple'
16
+ verify_ssl = true
17
+ name = 'pypi'
18
+
19
+ [requires]
20
+ python_version = '2.7'
21
+
22
+ [packages]
23
+ requests = { extras = ['socks'] }
24
+ records = '>0.5.0'
25
+ foo = '!= 0.5.*'
26
+ bar = '~=0.2'
27
+ baz = '=0.3.*'
28
+ django = { git = 'https://github.com/django/django.git', ref = '1.11.4', editable = true }
29
+ "e682b37" = {file = "https://github.com/divio/django-cms/archive/release/3.4.x.zip"}
30
+ "e1839a8" = {path = ".", editable = true}
31
+ pywinusb = { version = "*", os_name = "=='nt'", index="pypi"}
32
+
33
+ [dev-packages]
34
+ nose = '*'
35
+ unittest2 = {version = ">=1.0,<3.0", markers="python_version < '2.7.9' or (python_version >= '3.0' and python_version < '3.4')"}
36
+ EOF
37
+
38
+ describe PythonLang do
39
+ context 'requirements.txt' do
40
+ it 'expects the default dependency files to be requirements.txt and Pipfile' do
41
+ expect(PythonLang.locations).to eq ['requirements.txt', 'Pipfile']
42
+ end
43
+
44
+ it 'parses requirements.txt' do
45
+ result = PythonLang.parse_file('requirements.txt', REQUIREMENT_TXT)
46
+ expect(result).to be_a Array
47
+ expect(result.length).to eq 6
48
+ result.each do |dep, version|
49
+ expect(dep).to be_a Gem::Dependency
50
+ expect(dep.type).to eq :runtime
51
+ expect(version.version).to match /\d\.\d.*/
52
+ end
53
+ end
54
+ end
55
+
56
+ context 'pipfile' do
57
+ it 'fails softly on invalid Pipfile' do
58
+ expect(PythonLang.parse_file('Pipfile', 'waa')).to eq []
59
+ end
60
+
61
+ it 'parses Pipefile' do
62
+ result = PythonLang.parse_file('Pipfile', PIPFILE)
63
+ expect(result).to be_a Array
64
+
65
+ dev = result.select {|r| r.first.type == :development}
66
+ run = result.select {|r| r.first.type == :runtime}
67
+ expect(dev.length).to eq 2
68
+ expect(run.length).to eq 9
69
+
70
+ expect(run.first.first).to be_a OpenStruct
71
+ expect(run.first.first.requirement).to eq 'unknown'
72
+ expect(run.first.last).to be_nil
73
+ expect(dev.first.first.requirement.to_s).to eq '>= 0'
74
+ expect(dev.first.last.version).to match /\d\.\d.*/
75
+
76
+ foo = result.find {|r| r.first.name == 'foo'}
77
+ bar = result.find {|r| r.first.name == 'bar'}
78
+ baz = result.find {|r| r.first.name == 'baz'}
79
+
80
+ foo_req = foo.first.requirement.to_s.split(',')
81
+ foo_req.map! {|r| r.strip}
82
+ expect(foo_req).to include('< 0.5.0')
83
+ expect(foo_req).to include('>= 0.6')
84
+
85
+ expect(bar.first.requirement.to_s).to eq '~> 0.2'
86
+ expect(baz.first.requirement.to_s).to eq '~> 0.3.0'
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,81 @@
1
+ CARGO = <<EOF
2
+ [dependencies]
3
+ log = { version = "0.4.*", features = ["std"] }
4
+ regex = { version = "1.0.3", optional = true }
5
+ termcolor = { version = "^1.0.2", optional = true }
6
+ humantime = { version = "~1.3", optional = true }
7
+ atty = { version = "0.2.5", optional = true }
8
+ my-library = { git = 'https://example.com/git/my-library' }
9
+ uuid = "1.0"
10
+
11
+ [dev-dependencies]
12
+ tempdir = "0.3"
13
+
14
+ [build-dependencies]
15
+ cc = "1.0.3"
16
+ EOF
17
+
18
+ def translate_req(str)
19
+ RustLang.send(:translate_requirement, str)
20
+ end
21
+
22
+ describe RustLang do
23
+ it 'expects the default dependency files to be Cargo.toml' do
24
+ expect(RustLang.locations).to eq ['Cargo.toml']
25
+ end
26
+
27
+ it 'parses Cargo.toml' do
28
+ results = RustLang.parse_file('Cargo.toml', CARGO)
29
+ expect(results.length).to eq 9
30
+
31
+ dev = results.select {|r| r.first.type == :development}
32
+ run = results.select {|r| r.first.type == :runtime}
33
+ build = results.select {|r| r.first.type == :build}
34
+
35
+ expect(dev.length).to eq 1
36
+ expect(run.length).to eq 7
37
+ expect(build.length).to eq 1
38
+
39
+ expect(run[5].first).to be_a OpenStruct
40
+ expect(run[5].first.requirement).to eq 'unknown'
41
+ expect(run[5].last).to be_nil
42
+ expect(dev.first.first.requirement.to_s).to match />= 0\.3/
43
+ expect(dev.first.first.requirement.to_s).to match /< 0\.4/
44
+ expect(dev.first.last.version).to match /\d\.\d.*/
45
+ end
46
+
47
+ context 'translates Rust version requirements to Gem:: compatible requirements' do
48
+ # See https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
49
+ it 'for the caret operator' do
50
+ # ^1.2.3 := >=1.2.3, <2.0.0
51
+ # ^1.2 := >=1.2.0, <2.0.0
52
+ # ^1 := >=1.0.0, <2.0.0
53
+ # ^0.2.3 := >=0.2.3, <0.3.0
54
+ # ^0.2 := >=0.2.0, <0.3.0
55
+ # ^0.0.3 := >=0.0.3, <0.0.4
56
+ # ^0.0 := >=0.0.0, <0.1.0
57
+ # ^0 := >=0.0.0, <1.0.0
58
+ expect(translate_req('^1.2.3')).to eq ['>= 1.2.3', '< 2']
59
+ expect(translate_req('^1.2')).to eq ['>= 1.2', '< 2']
60
+ expect(translate_req('^1')).to eq ['>= 1', '< 2']
61
+ expect(translate_req('^0.2.3')).to eq ['>= 0.2.3', '< 0.3']
62
+ expect(translate_req('^0.2')).to eq ['>= 0.2', '< 0.3']
63
+ expect(translate_req('^0.0.3')).to eq ['>= 0.0.3', '< 0.0.4']
64
+ expect(translate_req('^0.0')).to eq ['>= 0.0', '< 0.1']
65
+ expect(translate_req('^0')).to eq ['>= 0', '< 1']
66
+ end
67
+
68
+ it 'for the ~ operator' do
69
+ expect(translate_req('~1.2.3')). to eq '~>1.2.3'
70
+ end
71
+
72
+ it 'for the * operator' do
73
+ # * := >=0.0.0 := >= 0
74
+ # 1.* := >=1.0.0, <2.0.0 := ~> 1.0
75
+ # 1.2.* := >=1.2.0, <1.3.0 := ~> 1.2.0
76
+ expect(translate_req('*')).to eq '>= 0'
77
+ expect(translate_req('1.*')).to eq '~> 1.0'
78
+ expect(translate_req('1.2.*')).to eq '~> 1.2.0'
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,111 @@
1
+ require 'simplecov'
2
+
3
+ if ENV['TRAVIS']
4
+ require 'coveralls'
5
+ Coveralls.wear!
6
+ end
7
+
8
+ require File.expand_path('../../lib/cutting_edge/app.rb', __FILE__)
9
+ CuttingEdge::App.set(:enable_logging, false)
10
+ ::SemanticLogger.add_appender(io: STDOUT)
11
+
12
+ # This file was generated by the `rspec --init` command. Conventionally, all
13
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
14
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
15
+ # this file to always be loaded, without a need to explicitly require it in any
16
+ # files.
17
+ #
18
+ # Given that it is always loaded, you are encouraged to keep this file as
19
+ # light-weight as possible. Requiring heavyweight dependencies from this file
20
+ # will add to the boot time of your test suite on EVERY test run, even for an
21
+ # individual file that may not need all of that loaded. Instead, consider making
22
+ # a separate helper file that requires the additional dependencies and performs
23
+ # the additional setup, and require it from the spec files that actually need
24
+ # it.
25
+ #
26
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
27
+ RSpec.configure do |config|
28
+ # rspec-expectations config goes here. You can use an alternate
29
+ # assertion/expectation library such as wrong or the stdlib/minitest
30
+ # assertions if you prefer.
31
+ config.expect_with :rspec do |expectations|
32
+ # This option will default to `true` in RSpec 4. It makes the `description`
33
+ # and `failure_message` of custom matchers include text for helper methods
34
+ # defined using `chain`, e.g.:
35
+ # be_bigger_than(2).and_smaller_than(4).description
36
+ # # => "be bigger than 2 and smaller than 4"
37
+ # ...rather than:
38
+ # # => "be bigger than 2"
39
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
40
+ end
41
+
42
+ # rspec-mocks config goes here. You can use an alternate test double
43
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
44
+ config.mock_with :rspec do |mocks|
45
+ # Prevents you from mocking or stubbing a method that does not exist on
46
+ # a real object. This is generally recommended, and will default to
47
+ # `true` in RSpec 4.
48
+ mocks.verify_partial_doubles = true
49
+ end
50
+
51
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
52
+ # have no way to turn it off -- the option exists only for backwards
53
+ # compatibility in RSpec 3). It causes shared context metadata to be
54
+ # inherited by the metadata hash of host groups and examples, rather than
55
+ # triggering implicit auto-inclusion in groups with matching metadata.
56
+ config.shared_context_metadata_behavior = :apply_to_host_groups
57
+
58
+ # The settings below are suggested to provide a good initial experience
59
+ # with RSpec, but feel free to customize to your heart's content.
60
+ =begin
61
+ # This allows you to limit a spec run to individual examples or groups
62
+ # you care about by tagging them with `:focus` metadata. When nothing
63
+ # is tagged with `:focus`, all examples get run. RSpec also provides
64
+ # aliases for `it`, `describe`, and `context` that include `:focus`
65
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
66
+ config.filter_run_when_matching :focus
67
+
68
+ # Allows RSpec to persist some state between runs in order to support
69
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
70
+ # you configure your source control system to ignore this file.
71
+ config.example_status_persistence_file_path = "spec/examples.txt"
72
+
73
+ # Limits the available syntax to the non-monkey patched syntax that is
74
+ # recommended. For more details, see:
75
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
76
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
77
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
78
+ config.disable_monkey_patching!
79
+
80
+ # This setting enables warnings. It's recommended, but in some cases may
81
+ # be too noisy due to issues in dependencies.
82
+ config.warnings = true
83
+
84
+ # Many RSpec users commonly either run the entire suite or an individual
85
+ # file, and it's useful to allow more verbose output when running an
86
+ # individual spec file.
87
+ if config.files_to_run.one?
88
+ # Use the documentation formatter for detailed output,
89
+ # unless a formatter has already been configured
90
+ # (e.g. via a command-line flag).
91
+ config.default_formatter = "doc"
92
+ end
93
+
94
+ # Print the 10 slowest examples and example groups at the
95
+ # end of the spec run, to help surface which specs are running
96
+ # particularly slow.
97
+ config.profile_examples = 10
98
+
99
+ # Run specs in random order to surface order dependencies. If you find an
100
+ # order dependency and want to debug it, you can fix the order by providing
101
+ # the seed, which is printed after each run.
102
+ # --seed 1234
103
+ config.order = :random
104
+
105
+ # Seed global randomization in this process using the `--seed` CLI option.
106
+ # Setting this allows you to use `--seed` to deterministically reproduce
107
+ # test failures related to randomization by passing the same `--seed` value
108
+ # as the one that triggered the failure.
109
+ Kernel.srand config.seed
110
+ =end
111
+ end