suspect 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +104 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/suspect/file_tree/git/client.rb +27 -0
- data/lib/suspect/file_tree/git/snapshot.rb +45 -0
- data/lib/suspect/file_utils/flock_writer.rb +17 -0
- data/lib/suspect/file_utils/idempotent.rb +49 -0
- data/lib/suspect/gathering/rspec/listener.rb +70 -0
- data/lib/suspect/gathering/run_info.rb +32 -0
- data/lib/suspect/prediction/default.rb +24 -0
- data/lib/suspect/prediction/naive/all_found.rb +41 -0
- data/lib/suspect/rspec_listener.rb +45 -0
- data/lib/suspect/setup/collector_id_generator.rb +11 -0
- data/lib/suspect/setup/creator.rb +29 -0
- data/lib/suspect/setup/structure.rb +29 -0
- data/lib/suspect/storage/appender.rb +38 -0
- data/lib/suspect/storage/dir_path.rb +29 -0
- data/lib/suspect/storage/reader.rb +39 -0
- data/lib/suspect/version.rb +3 -0
- data/lib/suspect.rb +7 -0
- data/suspect.gemspec +33 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cf501a625399385a8be77bcc75418165958be083
|
4
|
+
data.tar.gz: 74d38bb2d72da37efcdc8ec72252fc12533d50fa
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d6ec146b0c9ecd46a48dd913cd6c898d6637643e5ef97b6e36fc91f7b23e82fd69cf5838231e2f38241dfb3a1218fd1f1e65448a3f24795bf32794d17c01acfb
|
7
|
+
data.tar.gz: f23a292ec133372e10806bac49bac4df157d9b577b4f22f4fa13870fe469fd7b37bdb186d882445df20808349228b448e22c5319d426b008f2f2bda84a4af524
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, and in the interest of
|
4
|
+
fostering an open and welcoming community, we pledge to respect all people who
|
5
|
+
contribute through reporting issues, posting feature requests, updating
|
6
|
+
documentation, submitting pull requests or patches, and other activities.
|
7
|
+
|
8
|
+
We are committed to making participation in this project a harassment-free
|
9
|
+
experience for everyone, regardless of level of experience, gender, gender
|
10
|
+
identity and expression, sexual orientation, disability, personal appearance,
|
11
|
+
body size, race, ethnicity, age, religion, or nationality.
|
12
|
+
|
13
|
+
Examples of unacceptable behavior by participants include:
|
14
|
+
|
15
|
+
* The use of sexualized language or imagery
|
16
|
+
* Personal attacks
|
17
|
+
* Trolling or insulting/derogatory comments
|
18
|
+
* Public or private harassment
|
19
|
+
* Publishing other's private information, such as physical or electronic
|
20
|
+
addresses, without explicit permission
|
21
|
+
* Other unethical or unprofessional conduct
|
22
|
+
|
23
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
24
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
25
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
26
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
27
|
+
threatening, offensive, or harmful.
|
28
|
+
|
29
|
+
By adopting this Code of Conduct, project maintainers commit themselves to
|
30
|
+
fairly and consistently applying these principles to every aspect of managing
|
31
|
+
this project. Project maintainers who do not follow or enforce the Code of
|
32
|
+
Conduct may be permanently removed from the project team.
|
33
|
+
|
34
|
+
This code of conduct applies both within project spaces and in public spaces
|
35
|
+
when an individual is representing the project or its community.
|
36
|
+
|
37
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
38
|
+
reported by contacting a project maintainer at mitin.pavel@gmail.com. All
|
39
|
+
complaints will be reviewed and investigated and will result in a response that
|
40
|
+
is deemed necessary and appropriate to the circumstances. Maintainers are
|
41
|
+
obligated to maintain confidentiality with regard to the reporter of an
|
42
|
+
incident.
|
43
|
+
|
44
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
45
|
+
version 1.3.0, available at
|
46
|
+
[http://contributor-covenant.org/version/1/3/0/][version]
|
47
|
+
|
48
|
+
[homepage]: http://contributor-covenant.org
|
49
|
+
[version]: http://contributor-covenant.org/version/1/3/0/
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Pavlo Mitin
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# Suspect
|
2
|
+
|
3
|
+
If you have **slow tests**, you need a troubleshooting strategy. It takes a lot of effort to get rid of this smell. In meantime it is good to have a palliative. `Suspect` provides such a band-aid. The gem collects test results along with VCS (Git) status and use harvested data to select a subset of test files to be run.
|
4
|
+
|
5
|
+
## Tags
|
6
|
+
|
7
|
+
* Test Smells
|
8
|
+
* Slow Tests
|
9
|
+
* RSpec
|
10
|
+
* TDD
|
11
|
+
* BDD
|
12
|
+
* Anti-patterns
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem 'suspect'
|
20
|
+
```
|
21
|
+
|
22
|
+
And then execute:
|
23
|
+
|
24
|
+
$ bundle
|
25
|
+
|
26
|
+
Or install it yourself as:
|
27
|
+
|
28
|
+
$ gem install suspect
|
29
|
+
|
30
|
+
Add to RSpec helper file:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
# spec/spec_helper.rb
|
34
|
+
require 'suspect/rspec_listener'
|
35
|
+
|
36
|
+
RSpec.configure do |config|
|
37
|
+
|
38
|
+
::Suspect::RSpecListener.setup_using config
|
39
|
+
|
40
|
+
```
|
41
|
+
|
42
|
+
Create a rake file:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
# lib/tasks/suspect.rake
|
46
|
+
|
47
|
+
namespace :suspect do
|
48
|
+
desc 'run suspect test files in parallel'
|
49
|
+
task :parallel_rspec do
|
50
|
+
paths = ::Suspect::Prediction::Default.paths
|
51
|
+
|
52
|
+
if paths.any?
|
53
|
+
puts "#{paths.size} test file(s) are going to be run..."
|
54
|
+
puts `bundle exec parallel_rspec #{paths.join(' ')} -n 7`
|
55
|
+
else
|
56
|
+
puts 'No test files found to be run'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
## Usage
|
63
|
+
|
64
|
+
`Suspect` is conceptually divided into two parts:
|
65
|
+
* data gathering
|
66
|
+
* failure prediction
|
67
|
+
|
68
|
+
### Gathering
|
69
|
+
|
70
|
+
As soon as the gem is added to Gemfile and `Suspect::RSpecListener.setup_using config` is invoked in `spec/spec_helper.rb`, the gathering part is up and running. Each time you run specs, results are stored for further usage. Harvested data is stored under `suspect/` folder in *.suspect files. Adding *.suspect files under source control allows data sharing between project members.
|
71
|
+
|
72
|
+
## Prediction
|
73
|
+
|
74
|
+
`Suspect::Prediction::Default.paths` returns a list of spec files which are more likely to fall. The installation section above contains a simple rake task for the prediction phase. Fill free to modify the task to better meet your requirements.
|
75
|
+
|
76
|
+
## Assumptions
|
77
|
+
|
78
|
+
* A project uses:
|
79
|
+
* Git
|
80
|
+
* RSpec
|
81
|
+
|
82
|
+
## TODO
|
83
|
+
|
84
|
+
* Basic error handling (especially for file operations)
|
85
|
+
* Enhanced Rake task examples in README:
|
86
|
+
* run rspec taking collected data into account
|
87
|
+
* run parallel_tests taking collected data into account
|
88
|
+
* More sophisticated strategies for finding test files to be run
|
89
|
+
|
90
|
+
## Development
|
91
|
+
|
92
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
93
|
+
|
94
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
95
|
+
|
96
|
+
## Contributing
|
97
|
+
|
98
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/suspect. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
99
|
+
|
100
|
+
|
101
|
+
## License
|
102
|
+
|
103
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
104
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "suspect"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Suspect
|
2
|
+
module FileTree
|
3
|
+
module Git
|
4
|
+
class Client
|
5
|
+
def branch
|
6
|
+
`git rev-parse --abbrev-ref HEAD`
|
7
|
+
end
|
8
|
+
|
9
|
+
def files
|
10
|
+
`git ls-files --full-name`
|
11
|
+
end
|
12
|
+
|
13
|
+
def modified_files
|
14
|
+
`git ls-files --full-name --modified`
|
15
|
+
end
|
16
|
+
|
17
|
+
def commit_hash
|
18
|
+
`git log -1 --format="%H"`
|
19
|
+
end
|
20
|
+
|
21
|
+
def diff
|
22
|
+
`git diff`
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require_relative './client'
|
2
|
+
|
3
|
+
module Suspect
|
4
|
+
module FileTree
|
5
|
+
module Git
|
6
|
+
class Snapshot
|
7
|
+
def initialize(client = ::Suspect::FileTree::Git::Client.new)
|
8
|
+
@client = client
|
9
|
+
end
|
10
|
+
|
11
|
+
def branch
|
12
|
+
without_new_line(client.branch)
|
13
|
+
end
|
14
|
+
|
15
|
+
def files
|
16
|
+
lines_to_files(client.files)
|
17
|
+
end
|
18
|
+
|
19
|
+
def modified_files
|
20
|
+
lines_to_files(client.modified_files)
|
21
|
+
end
|
22
|
+
|
23
|
+
def commit_hash
|
24
|
+
without_new_line(client.commit_hash)
|
25
|
+
end
|
26
|
+
|
27
|
+
def patch
|
28
|
+
client.diff
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :client
|
34
|
+
|
35
|
+
def without_new_line(str)
|
36
|
+
str.sub(/\n\z/, '')
|
37
|
+
end
|
38
|
+
|
39
|
+
def lines_to_files(multiline_string)
|
40
|
+
multiline_string.split(/\n/).map {|path| "./#{path}"}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Suspect
|
2
|
+
module FileUtils
|
3
|
+
class FlockWriter
|
4
|
+
|
5
|
+
PERMISSIONS = 0644
|
6
|
+
MODE = 'a'
|
7
|
+
|
8
|
+
def write(path, content)
|
9
|
+
File.open path, MODE, PERMISSIONS do |f|
|
10
|
+
f.flock File::LOCK_EX
|
11
|
+
f.write "#{content}\n"
|
12
|
+
f.flush
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'find'
|
4
|
+
|
5
|
+
module Suspect
|
6
|
+
module FileUtils
|
7
|
+
##
|
8
|
+
# A humble object class for file system manipulations
|
9
|
+
#
|
10
|
+
class Idempotent
|
11
|
+
def file_paths(base_path)
|
12
|
+
result = []
|
13
|
+
|
14
|
+
Find.find(base_path) do |path|
|
15
|
+
unless FileTest.directory?(path)
|
16
|
+
result << path
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
result
|
21
|
+
rescue Errno::ENOENT # No such file or directory.
|
22
|
+
[]
|
23
|
+
end
|
24
|
+
|
25
|
+
def read(path, &block)
|
26
|
+
if block_given?
|
27
|
+
::File.open(path).each &block
|
28
|
+
else
|
29
|
+
::File.open(path) { |f| f.readline }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def write(path, content)
|
34
|
+
return if path.exist?
|
35
|
+
|
36
|
+
temp_file = ::Tempfile.new
|
37
|
+
temp_file.write content
|
38
|
+
temp_file.close
|
39
|
+
|
40
|
+
# FileUtils.mv doesn't support --no-clobber option.
|
41
|
+
`mv --no-clobber #{temp_file.path} #{path.expand_path}`
|
42
|
+
end
|
43
|
+
|
44
|
+
def mkdir(path)
|
45
|
+
::FileUtils::mkdir_p path
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require_relative '../run_info'
|
2
|
+
|
3
|
+
module Suspect
|
4
|
+
module Gathering
|
5
|
+
module RSpec
|
6
|
+
##
|
7
|
+
# Depends on:
|
8
|
+
# * RSpec::Core::Reporter#examples
|
9
|
+
# * RSpec::Core::Reporter#failed_examples
|
10
|
+
# * RSpec::Core::Reporter#pending_examples
|
11
|
+
# * RSpec::Core::Example#file_path
|
12
|
+
#
|
13
|
+
class Listener
|
14
|
+
def initialize(file_tree, storage_appender, collector_id, clock)
|
15
|
+
@file_tree = file_tree
|
16
|
+
@storage_appender = storage_appender
|
17
|
+
@collector_id = collector_id
|
18
|
+
@clock = clock
|
19
|
+
end
|
20
|
+
|
21
|
+
def notification_names
|
22
|
+
%i(stop)
|
23
|
+
end
|
24
|
+
|
25
|
+
def stop(notification)
|
26
|
+
run_info = build_run_info(notification)
|
27
|
+
|
28
|
+
if run_info.failed_files.any?
|
29
|
+
storage_appender.append run_info
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_reader :file_tree, :storage_appender, :collector_id, :clock
|
36
|
+
|
37
|
+
def build_run_info(notification)
|
38
|
+
::Suspect::Gathering::RunInfo.new.tap do |info|
|
39
|
+
info.collector_id = collector_id
|
40
|
+
info.notified_at = clock.iso8601
|
41
|
+
info.failed_example_count = failed_example_count(notification)
|
42
|
+
info.pending_example_count = pending_example_count(notification)
|
43
|
+
info.successful_example_count = successful_example_count(notification)
|
44
|
+
info.failed_files = failed_files(notification)
|
45
|
+
info.branch = file_tree.branch
|
46
|
+
info.commit_hash = file_tree.commit_hash
|
47
|
+
info.modified_files = file_tree.modified_files
|
48
|
+
info.patch = file_tree.patch
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def failed_files(notification)
|
53
|
+
notification.failed_examples.map(&:file_path).sort.uniq
|
54
|
+
end
|
55
|
+
|
56
|
+
def failed_example_count(notification)
|
57
|
+
notification.failed_examples.size
|
58
|
+
end
|
59
|
+
|
60
|
+
def pending_example_count(notification)
|
61
|
+
notification.pending_examples.size
|
62
|
+
end
|
63
|
+
|
64
|
+
def successful_example_count(notification)
|
65
|
+
notification.examples.size - (failed_example_count(notification) + pending_example_count(notification))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Suspect
|
2
|
+
module Gathering
|
3
|
+
##
|
4
|
+
# Contains results of a spec run.
|
5
|
+
#
|
6
|
+
class RunInfo < Struct.new(:collector_id,
|
7
|
+
:notified_at, # String representation in ISO 8601 format.
|
8
|
+
|
9
|
+
:failed_example_count,
|
10
|
+
:successful_example_count,
|
11
|
+
:pending_example_count,
|
12
|
+
|
13
|
+
:failed_files, # An array of files contained failed test examples.
|
14
|
+
:modified_files, # An array of modified files.
|
15
|
+
|
16
|
+
:branch, # The current Git branch.
|
17
|
+
:commit_hash, # A hash of the current Git commit.
|
18
|
+
:patch, # A version control system patch for the current file tree state.
|
19
|
+
:version) # Should be changed each time
|
20
|
+
# backward compatibility with already stored data
|
21
|
+
# is broken.
|
22
|
+
# Is intended to use during deserialization.
|
23
|
+
|
24
|
+
VERSION = '1'
|
25
|
+
|
26
|
+
def initialize(*params)
|
27
|
+
super(*params)
|
28
|
+
self.version ||= VERSION
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
require 'suspect/file_tree/git/snapshot'
|
4
|
+
require 'suspect/storage/reader'
|
5
|
+
require 'suspect/prediction/naive/all_found'
|
6
|
+
|
7
|
+
module Suspect
|
8
|
+
module Prediction
|
9
|
+
class Default
|
10
|
+
class << self
|
11
|
+
def paths
|
12
|
+
root_path = ::Pathname.new('.')
|
13
|
+
structure = ::Suspect::Setup::Structure.new(root_path)
|
14
|
+
storage_path = structure.storage_path
|
15
|
+
file_helper = ::Suspect::FileUtils::Idempotent.new
|
16
|
+
reader = ::Suspect::Storage::Reader.new(storage_path, file_helper)
|
17
|
+
file_tree = ::Suspect::FileTree::Git::Snapshot.new
|
18
|
+
|
19
|
+
Naive::AllFound.new(reader, file_tree).paths
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Suspect
|
2
|
+
module Prediction
|
3
|
+
module Naive
|
4
|
+
class AllFound
|
5
|
+
def initialize(run_infos, file_tree)
|
6
|
+
@run_infos = run_infos
|
7
|
+
@file_tree = file_tree
|
8
|
+
end
|
9
|
+
|
10
|
+
def paths
|
11
|
+
result = []
|
12
|
+
return result if modified_files.empty?
|
13
|
+
|
14
|
+
run_infos.foreach do |run_info|
|
15
|
+
if match?(modified_files, run_info.modified_files)
|
16
|
+
result.push *run_info.failed_files
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
select_existing_files(result.uniq)
|
21
|
+
end
|
22
|
+
|
23
|
+
def match?(first_array, second_array)
|
24
|
+
(first_array & second_array).any?
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :run_infos, :file_tree
|
30
|
+
|
31
|
+
def modified_files
|
32
|
+
@modified_files ||= file_tree.modified_files
|
33
|
+
end
|
34
|
+
|
35
|
+
def select_existing_files(files)
|
36
|
+
files & file_tree.files
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
require_relative './file_utils/idempotent'
|
5
|
+
require_relative './setup/collector_id_generator'
|
6
|
+
require_relative './setup/structure'
|
7
|
+
require_relative './setup/creator'
|
8
|
+
require_relative './gathering/rspec/listener'
|
9
|
+
require_relative './file_tree/git/snapshot'
|
10
|
+
require_relative './storage/appender'
|
11
|
+
require_relative './storage/dir_path'
|
12
|
+
|
13
|
+
module Suspect
|
14
|
+
##
|
15
|
+
# A facade enabling easy setup:
|
16
|
+
#
|
17
|
+
# require 'suspect/rspec_listener'
|
18
|
+
#
|
19
|
+
# RSpec.configure do |config|
|
20
|
+
# ::Suspect::RSpecListener.setup_using config
|
21
|
+
#
|
22
|
+
class RSpecListener
|
23
|
+
class << self
|
24
|
+
def setup_using(rspec_config)
|
25
|
+
new.register_listener rspec_config.reporter
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def register_listener(reporter)
|
30
|
+
root_path = ::Pathname.new('.')
|
31
|
+
file_helper = ::Suspect::FileUtils::Idempotent.new
|
32
|
+
structure = ::Suspect::Setup::Structure.new(root_path)
|
33
|
+
collector_id_generator = ::Suspect::Setup::CollectorIdGenerator.new
|
34
|
+
::Suspect::Setup::Creator.new(structure, collector_id_generator, file_helper).build
|
35
|
+
|
36
|
+
storage_path = ::Suspect::Storage::DirPath.new(structure.storage_path, Time.now.utc)
|
37
|
+
collector_id = file_helper.read(structure.collector_id_path)
|
38
|
+
storage = ::Suspect::Storage::Appender.new(dir_path: storage_path, dir_helper: file_helper, collector_id: collector_id)
|
39
|
+
file_tree = ::Suspect::FileTree::Git::Snapshot.new
|
40
|
+
listener = ::Suspect::Gathering::RSpec::Listener.new(file_tree, storage, collector_id, ::Time.now.utc)
|
41
|
+
|
42
|
+
reporter.register_listener listener, *listener.notification_names
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Suspect
|
2
|
+
module Setup
|
3
|
+
class Creator
|
4
|
+
def initialize(structure, collector_id_generator, file_helper)
|
5
|
+
@structure = structure
|
6
|
+
@collector_id_generator = collector_id_generator
|
7
|
+
@file_helper = file_helper
|
8
|
+
end
|
9
|
+
|
10
|
+
def build
|
11
|
+
create_storage_dir
|
12
|
+
create_collector_id_file
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_reader :structure, :collector_id_generator, :file_helper
|
18
|
+
|
19
|
+
def create_collector_id_file
|
20
|
+
file_helper.write structure.collector_id_path,
|
21
|
+
collector_id_generator.next
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_storage_dir
|
25
|
+
file_helper.mkdir structure.storage_path
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Suspect
|
2
|
+
module Setup
|
3
|
+
class Structure
|
4
|
+
MAIN_DIR_NAME = 'suspect'
|
5
|
+
STORAGE_DIR_NAME = 'storage'
|
6
|
+
COLLECTOR_ID_NAME = 'collector_id'
|
7
|
+
|
8
|
+
def initialize(root_path)
|
9
|
+
@root_path = root_path
|
10
|
+
end
|
11
|
+
|
12
|
+
def storage_path
|
13
|
+
main_dir_path + STORAGE_DIR_NAME
|
14
|
+
end
|
15
|
+
|
16
|
+
def collector_id_path
|
17
|
+
main_dir_path + COLLECTOR_ID_NAME
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :root_path
|
23
|
+
|
24
|
+
def main_dir_path
|
25
|
+
root_path + MAIN_DIR_NAME
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
require_relative './../file_utils/flock_writer'
|
5
|
+
|
6
|
+
module Suspect
|
7
|
+
module Storage
|
8
|
+
class Appender
|
9
|
+
VERSION = '1'
|
10
|
+
|
11
|
+
def initialize(opts)
|
12
|
+
@writer = opts[:writer] || ::Suspect::FileUtils::FlockWriter.new
|
13
|
+
@dir_helper = opts[:dir_helper] ||fail(ArgumentError, 'No dir_helper found')
|
14
|
+
@dir_path = opts[:dir_path] || fail(ArgumentError, 'No dir_path found')
|
15
|
+
@collector_id = opts[:collector_id] || fail(ArgumentError, 'No collector_id found')
|
16
|
+
end
|
17
|
+
|
18
|
+
def append(run_info)
|
19
|
+
dir_helper.mkdir dir_path.expand_path
|
20
|
+
|
21
|
+
writer.write filename,
|
22
|
+
serialize(run_info)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :writer, :dir_helper, :dir_path, :collector_id
|
28
|
+
|
29
|
+
def filename
|
30
|
+
"#{dir_path.expand_path}/#{collector_id}-#{VERSION}.suspect"
|
31
|
+
end
|
32
|
+
|
33
|
+
def serialize(run_info)
|
34
|
+
::JSON.generate(run_info.to_h)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Suspect
|
2
|
+
module Storage
|
3
|
+
class DirPath
|
4
|
+
def initialize(base_path, clock)
|
5
|
+
@base_path = base_path
|
6
|
+
@clock = clock
|
7
|
+
end
|
8
|
+
|
9
|
+
def expand_path
|
10
|
+
File.join(base_path,
|
11
|
+
format(clock.year),
|
12
|
+
format(clock.month),
|
13
|
+
format(clock.day))
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
expand_path
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :base_path, :clock
|
23
|
+
|
24
|
+
def format(number)
|
25
|
+
number.to_s.rjust(2, '0')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'suspect/gathering/run_info'
|
2
|
+
|
3
|
+
module Suspect
|
4
|
+
module Storage
|
5
|
+
class Reader
|
6
|
+
def initialize(base_path, file_helper)
|
7
|
+
@base_path = base_path
|
8
|
+
@file_helper = file_helper
|
9
|
+
end
|
10
|
+
|
11
|
+
def foreach
|
12
|
+
run_info_paths.each do |path|
|
13
|
+
file_helper.read(path) do |line|
|
14
|
+
yield run_info_from(line)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :base_path, :file_helper
|
22
|
+
|
23
|
+
def run_info_paths
|
24
|
+
file_helper.file_paths(base_path).select { |p| p.end_with?('.suspect') }
|
25
|
+
end
|
26
|
+
|
27
|
+
def run_info_from(line)
|
28
|
+
data = ::JSON.parse(line)
|
29
|
+
run_info = ::Suspect::Gathering::RunInfo.new
|
30
|
+
|
31
|
+
data.each_key do |k|
|
32
|
+
run_info.public_send "#{k}=", data[k]
|
33
|
+
end
|
34
|
+
|
35
|
+
run_info
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/suspect.rb
ADDED
data/suspect.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'suspect/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "suspect"
|
8
|
+
spec.version = Suspect::VERSION
|
9
|
+
spec.authors = ["Pavlo Mitin"]
|
10
|
+
spec.email = ["mitin.pavel@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Selects a subset of spec files to be run based on previous failures.}
|
13
|
+
spec.description = %q{Gathers spec failures revealing relationship between files in a project. Selects a subset of spec files to be run taking previous failures into account.}
|
14
|
+
spec.homepage = "https://github.com/MitinPavel/suspect"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
18
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
23
|
+
end
|
24
|
+
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_development_dependency "bundler", "~> 1.12"
|
31
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
32
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: suspect
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Pavlo Mitin
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-05-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.12'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.12'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
description: Gathers spec failures revealing relationship between files in a project.
|
56
|
+
Selects a subset of spec files to be run taking previous failures into account.
|
57
|
+
email:
|
58
|
+
- mitin.pavel@gmail.com
|
59
|
+
executables: []
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- ".gitignore"
|
64
|
+
- ".rspec"
|
65
|
+
- ".travis.yml"
|
66
|
+
- CODE_OF_CONDUCT.md
|
67
|
+
- Gemfile
|
68
|
+
- LICENSE.txt
|
69
|
+
- README.md
|
70
|
+
- Rakefile
|
71
|
+
- bin/console
|
72
|
+
- bin/setup
|
73
|
+
- lib/suspect.rb
|
74
|
+
- lib/suspect/file_tree/git/client.rb
|
75
|
+
- lib/suspect/file_tree/git/snapshot.rb
|
76
|
+
- lib/suspect/file_utils/flock_writer.rb
|
77
|
+
- lib/suspect/file_utils/idempotent.rb
|
78
|
+
- lib/suspect/gathering/rspec/listener.rb
|
79
|
+
- lib/suspect/gathering/run_info.rb
|
80
|
+
- lib/suspect/prediction/default.rb
|
81
|
+
- lib/suspect/prediction/naive/all_found.rb
|
82
|
+
- lib/suspect/rspec_listener.rb
|
83
|
+
- lib/suspect/setup/collector_id_generator.rb
|
84
|
+
- lib/suspect/setup/creator.rb
|
85
|
+
- lib/suspect/setup/structure.rb
|
86
|
+
- lib/suspect/storage/appender.rb
|
87
|
+
- lib/suspect/storage/dir_path.rb
|
88
|
+
- lib/suspect/storage/reader.rb
|
89
|
+
- lib/suspect/version.rb
|
90
|
+
- suspect.gemspec
|
91
|
+
homepage: https://github.com/MitinPavel/suspect
|
92
|
+
licenses:
|
93
|
+
- MIT
|
94
|
+
metadata:
|
95
|
+
allowed_push_host: https://rubygems.org
|
96
|
+
post_install_message:
|
97
|
+
rdoc_options: []
|
98
|
+
require_paths:
|
99
|
+
- lib
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
requirements: []
|
111
|
+
rubyforge_project:
|
112
|
+
rubygems_version: 2.5.1
|
113
|
+
signing_key:
|
114
|
+
specification_version: 4
|
115
|
+
summary: Selects a subset of spec files to be run based on previous failures.
|
116
|
+
test_files: []
|