pivo_flow 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +48 -0
- data/Rakefile +2 -0
- data/bin/pf +8 -0
- data/bin/pf-prepare-commit-msg +15 -0
- data/lib/pivo_flow/base.rb +87 -0
- data/lib/pivo_flow/cli.rb +55 -0
- data/lib/pivo_flow/pivotal.rb +129 -0
- data/lib/pivo_flow/version.rb +3 -0
- data/lib/pivo_flow.rb +10 -0
- data/pivo_flow.gemspec +23 -0
- metadata +125 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Adam Nowak
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# PivoFlow
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'pivo_flow'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install pivo_flow
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
All the required information is gathered on demand, but it's a good idea to prepare:
|
22
|
+
|
23
|
+
* project's Pivotal Tracker ID
|
24
|
+
* your Pivotal Tracker API token
|
25
|
+
|
26
|
+
Get list of current stories
|
27
|
+
|
28
|
+
pf stories
|
29
|
+
|
30
|
+
Start story with given ID
|
31
|
+
|
32
|
+
pf start STORY_ID
|
33
|
+
|
34
|
+
Finish current story [or given story ID]
|
35
|
+
|
36
|
+
pf finish [STORY_ID]
|
37
|
+
|
38
|
+
Clear current story without notifying Pivotal
|
39
|
+
|
40
|
+
pf clear
|
41
|
+
|
42
|
+
## Contributing
|
43
|
+
|
44
|
+
1. Fork it
|
45
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
46
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
47
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
48
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/bin/pf
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
story_path = 'tmp/.pivotal_story_id'
|
3
|
+
if File.exists?(story_path)
|
4
|
+
story_id = File.read(story_path).strip
|
5
|
+
if story_id =~ /(\d{7,})/
|
6
|
+
puts IO.read(ARGV[0])
|
7
|
+
commit_msg = IO.read(ARGV[0])
|
8
|
+
unless commit_msg.include?($1)
|
9
|
+
File.open(ARGV[0], 'w') do |file|
|
10
|
+
file.print commit_msg
|
11
|
+
file.print "[##{$1}]"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module PivoFlow
|
2
|
+
class Base
|
3
|
+
GIT_DIR = '.git'
|
4
|
+
KEYS_TO_CHECK = ["test-pivotal.project-id", "test-pivotal.api-token"]
|
5
|
+
|
6
|
+
def initialize(*args)
|
7
|
+
@options = {}
|
8
|
+
@current_dir = Dir.pwd
|
9
|
+
return "no GIT (#{GIT_DIR}) directory found" unless File.directory?(File.join(@current_dir, GIT_DIR))
|
10
|
+
|
11
|
+
# paths
|
12
|
+
@git_dir = File.join(@current_dir, GIT_DIR)
|
13
|
+
@git_hook_path = File.join(@git_dir, 'hooks', 'prepare-commit-msg')
|
14
|
+
@pf_git_hook_name = 'pf-prepare-commit-msg'
|
15
|
+
@pf_git_hook_path = File.join(@git_dir, 'hooks', @pf_git_hook_name)
|
16
|
+
|
17
|
+
@options[:repository] = Grit::Repo.new(@git_dir)
|
18
|
+
install_git_hook if git_hook_needed?
|
19
|
+
git_config_ok? ? parse_git_config : add_git_config
|
20
|
+
run
|
21
|
+
end
|
22
|
+
|
23
|
+
def git_hook_needed?
|
24
|
+
!File.executable?(@git_hook_path) || !File.read(@git_hook_path).match(/\.\/#{@pf_git_hook_name}/)
|
25
|
+
end
|
26
|
+
|
27
|
+
def install_git_hook
|
28
|
+
puts "Installing prepare-commit-msg hook..."
|
29
|
+
hook_path = File.join(File.dirname(__FILE__), '..', '..', 'bin', @pf_git_hook_name)
|
30
|
+
FileUtils.cp(hook_path, @pf_git_hook_path, preserve: true)
|
31
|
+
puts "File copied..."
|
32
|
+
unless File.read(@git_hook_path).match(/\.\/#{@pf_git_hook_name}/)
|
33
|
+
File.open(@git_hook_path, "a") { |f| f.puts("./#{@pf_git_hook_name}") }
|
34
|
+
puts "Reference to pf-prepare-commit-msg added to prepare-commit-msg..."
|
35
|
+
end
|
36
|
+
unless File.executable?(@git_hook_path)
|
37
|
+
FileUtils.chmod 0755, @git_hook_path unless
|
38
|
+
puts "Chmod on #{@git_hook_path} set to 755"
|
39
|
+
end
|
40
|
+
|
41
|
+
puts "Success!\n"
|
42
|
+
end
|
43
|
+
|
44
|
+
def run
|
45
|
+
raise "you should define run!"
|
46
|
+
end
|
47
|
+
|
48
|
+
def user_name
|
49
|
+
@options[:user_name] ||= @options[:repository].config['pivotal.full-name'] || @options[:repository].config['user.name']
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def git_config_ok?
|
55
|
+
!KEYS_TO_CHECK.any? { |key| @options[:repository].config[key].nil? }
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_git_config
|
59
|
+
ask_question_and_update_config "Pivotal: what is your project's ID?", "test-pivotal.project-id"
|
60
|
+
ask_question_and_update_config "Pivotal: what is your pivotal tracker api-token?", "test-pivotal.api-token"
|
61
|
+
parse_git_config
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_git_config
|
65
|
+
KEYS_TO_CHECK.each do |key|
|
66
|
+
new_key = key.split(".").last
|
67
|
+
@options[new_key] = @options[:repository].config[key]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def ask_question_and_update_config question, variable
|
72
|
+
@options[:repository].config[variable] ||= ask_question(question)
|
73
|
+
end
|
74
|
+
|
75
|
+
def ask_question question, first_answer = nil
|
76
|
+
h = HighLine.new
|
77
|
+
h.ask("#{question}\t") do |q|
|
78
|
+
q.responses[:ask_on_error] = :question
|
79
|
+
q.responses[:not_valid] = "It can't be empty, sorry"
|
80
|
+
q.validate = ->(id) { !id.empty? }
|
81
|
+
q.first_answer = first_answer
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module PivoFlow
|
2
|
+
class Cli
|
3
|
+
|
4
|
+
def initialize *args
|
5
|
+
if args.length.zero?
|
6
|
+
puts "You forgot method name"
|
7
|
+
exit 1
|
8
|
+
end
|
9
|
+
parse_argv(*args)
|
10
|
+
end
|
11
|
+
|
12
|
+
def stories
|
13
|
+
PivoFlow::Pivotal.new.show_stories
|
14
|
+
end
|
15
|
+
|
16
|
+
def start story_id
|
17
|
+
PivoFlow::Pivotal.new.pick_up_story(story_id)
|
18
|
+
end
|
19
|
+
|
20
|
+
def finish story_id=nil
|
21
|
+
file_story_path = File.join(Dir.pwd, "/tmp/.pivotal_story_id")
|
22
|
+
if File.exists? file_story_path
|
23
|
+
story_id = File.open(file_story_path).read.strip
|
24
|
+
end
|
25
|
+
PivoFlow::Pivotal.new.finish_story(story_id)
|
26
|
+
end
|
27
|
+
|
28
|
+
def clear
|
29
|
+
file_story_path = File.join(Dir.pwd, "/tmp/.pivotal_story_id")
|
30
|
+
if File.exists? file_story_path
|
31
|
+
FileUtils.remove_file(file_story_path)
|
32
|
+
end
|
33
|
+
puts "Current pivotal story id cleared."
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def valid_method? method_name
|
39
|
+
self.methods.include? method_name.to_sym
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse_argv(*args)
|
43
|
+
command = args.first.split.first
|
44
|
+
args = args.slice(1..-1)
|
45
|
+
|
46
|
+
unless valid_method?(command)
|
47
|
+
puts "Ups, no such method..."
|
48
|
+
exit 1
|
49
|
+
end
|
50
|
+
send(command, *args)
|
51
|
+
exit 0
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
module PivoFlow
|
3
|
+
class Pivotal < Base
|
4
|
+
|
5
|
+
def run
|
6
|
+
@story_id_file_name = ".pivotal_story_id"
|
7
|
+
@story_id_tmp_path = File.join(@current_dir, "/tmp")
|
8
|
+
@story_id_file_path = File.join(@story_id_tmp_path, @story_id_file_name)
|
9
|
+
|
10
|
+
return 1 unless @options["api-token"] && @options["project-id"]
|
11
|
+
PivotalTracker::Client.token = @options["api-token"]
|
12
|
+
PivotalTracker::Client.use_ssl = true
|
13
|
+
@options[:project] ||= PivotalTracker::Project.find(@options["project-id"])
|
14
|
+
end
|
15
|
+
|
16
|
+
def user_stories
|
17
|
+
project_stories.select{ |story| story.owned_by == user_name }
|
18
|
+
end
|
19
|
+
|
20
|
+
def project_stories
|
21
|
+
@options[:stories] ||= fetch_stories
|
22
|
+
end
|
23
|
+
|
24
|
+
def unasigned_stories
|
25
|
+
project_stories.select{ |story| story.owned_by == nil }
|
26
|
+
end
|
27
|
+
|
28
|
+
def current_story force = false
|
29
|
+
if (@options[:current_story] && !force)
|
30
|
+
@options[:current_story]
|
31
|
+
else
|
32
|
+
@options[:current_story] = user_stories.count.zero? ? nil : user_stories.first
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def list_stories_to_output stories
|
37
|
+
if current_story
|
38
|
+
puts "You've got some started stories, it may be a good idea to finish them in the first place"
|
39
|
+
puts "[##{current_story.id}] #{current_story.name} - #{current_story.description}"
|
40
|
+
end
|
41
|
+
|
42
|
+
HighLine.new.choose do |menu|
|
43
|
+
menu.header = "--- STORIES FROM PIVOTAL TRACKER ---\nWhich one would you like to start? "
|
44
|
+
menu.prompt = "story no.? "
|
45
|
+
menu.select_by = :index
|
46
|
+
stories.each do |story|
|
47
|
+
vars = {
|
48
|
+
story_id: story.id,
|
49
|
+
requested_by: story.requested_by,
|
50
|
+
name: story.name,
|
51
|
+
story_type: story_type_icon(story),
|
52
|
+
estimate: estimate_points(story)
|
53
|
+
}
|
54
|
+
story_text = "[#%{story_id}] %{story_type} [%{estimate} pts.] (requested by: %{requested_by}) %{name}" % vars
|
55
|
+
story_text += "\n Description: #{story.description}" unless story.description
|
56
|
+
menu.choice(story_text) { |answer| pick_up_story(answer.match(/\[#(?<id>\d+)\]/)[:id])}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def story_type_icon story
|
62
|
+
case story.story_type
|
63
|
+
when "feature" then "☆"
|
64
|
+
when "bug" then "☠"
|
65
|
+
when "chore" then "✙"
|
66
|
+
else "☺"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def estimate_points story
|
71
|
+
unless story.estimate.nil?
|
72
|
+
story.estimate < 0 ? "?" : story.estimate
|
73
|
+
else
|
74
|
+
"no"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def pick_up_story story_id
|
79
|
+
save_story_id_to_file(story_id) if start_story(story_id)
|
80
|
+
end
|
81
|
+
|
82
|
+
def update_story story_id, state
|
83
|
+
story = @options[:project].stories.find(story_id)
|
84
|
+
if story.nil?
|
85
|
+
puts "Story not found, sorry."
|
86
|
+
end
|
87
|
+
if story.update(owned_by: user_name, current_state: state).errors.count.zero?
|
88
|
+
puts "Story updated in Pivotal Tracker"
|
89
|
+
true
|
90
|
+
else
|
91
|
+
error_message = "ERROR"
|
92
|
+
error_message += ": #{story.errors.first}"
|
93
|
+
puts error_message
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def start_story story_id
|
98
|
+
update_story(story_id, :started)
|
99
|
+
end
|
100
|
+
|
101
|
+
def finish_story story_id
|
102
|
+
remove_story_id_file if story_id.nil? or update_story(story_id, :finished)
|
103
|
+
end
|
104
|
+
|
105
|
+
def remove_story_id_file
|
106
|
+
FileUtils.remove_file(@story_id_file_path)
|
107
|
+
end
|
108
|
+
|
109
|
+
def save_story_id_to_file story_id
|
110
|
+
FileUtils.mkdir_p(@story_id_tmp_path)
|
111
|
+
File.open(@story_id_file_path, 'w') { |f| f.write(story_id) }
|
112
|
+
end
|
113
|
+
|
114
|
+
def show_stories
|
115
|
+
stories = user_stories + unasigned_stories
|
116
|
+
if stories.count.zero?
|
117
|
+
puts "hmm... there is no story assigned to you! I'll better check for unasigned stories!"
|
118
|
+
stories = unasigned_stories
|
119
|
+
end
|
120
|
+
list_stories_to_output stories.first(10)
|
121
|
+
end
|
122
|
+
|
123
|
+
def fetch_stories(count = 100, state = "unstarted,unscheduled")
|
124
|
+
conditions = { current_state: state, limit: count }
|
125
|
+
@options[:stories] = @options[:project].stories.all(conditions)
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
end
|
data/lib/pivo_flow.rb
ADDED
data/pivo_flow.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/pivo_flow/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Adam Nowak"]
|
6
|
+
gem.email = ["lubieniebieski@gmail.com"]
|
7
|
+
gem.description = %q{Automated querying for pivotal stories, adding story id to commit message, etc.}
|
8
|
+
gem.summary = %q{Simple pivotal tracker integration for day to day work with git}
|
9
|
+
gem.homepage = "https://github.com/lubieniebieski/pivo_flow"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "pivo_flow"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = PivoFlow::VERSION
|
17
|
+
|
18
|
+
gem.add_runtime_dependency "pivotal-tracker"
|
19
|
+
gem.add_runtime_dependency "grit"
|
20
|
+
gem.add_runtime_dependency "highline"
|
21
|
+
gem.add_development_dependency "rspec"
|
22
|
+
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pivo_flow
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Adam Nowak
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-06 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: pivotal-tracker
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: grit
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: highline
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rspec
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
description: Automated querying for pivotal stories, adding story id to commit message,
|
79
|
+
etc.
|
80
|
+
email:
|
81
|
+
- lubieniebieski@gmail.com
|
82
|
+
executables:
|
83
|
+
- pf
|
84
|
+
- pf-prepare-commit-msg
|
85
|
+
extensions: []
|
86
|
+
extra_rdoc_files: []
|
87
|
+
files:
|
88
|
+
- .gitignore
|
89
|
+
- Gemfile
|
90
|
+
- LICENSE
|
91
|
+
- README.md
|
92
|
+
- Rakefile
|
93
|
+
- bin/pf
|
94
|
+
- bin/pf-prepare-commit-msg
|
95
|
+
- lib/pivo_flow.rb
|
96
|
+
- lib/pivo_flow/base.rb
|
97
|
+
- lib/pivo_flow/cli.rb
|
98
|
+
- lib/pivo_flow/pivotal.rb
|
99
|
+
- lib/pivo_flow/version.rb
|
100
|
+
- pivo_flow.gemspec
|
101
|
+
homepage: https://github.com/lubieniebieski/pivo_flow
|
102
|
+
licenses: []
|
103
|
+
post_install_message:
|
104
|
+
rdoc_options: []
|
105
|
+
require_paths:
|
106
|
+
- lib
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ! '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
requirements: []
|
120
|
+
rubyforge_project:
|
121
|
+
rubygems_version: 1.8.24
|
122
|
+
signing_key:
|
123
|
+
specification_version: 3
|
124
|
+
summary: Simple pivotal tracker integration for day to day work with git
|
125
|
+
test_files: []
|