policial 0.0.1

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: f9ea96661ceb59f07846827daf7ccf1d9f1ed2c6
4
+ data.tar.gz: 26d33c00ee9d4dfa3e65196f3faa6d8c4bfad416
5
+ SHA512:
6
+ metadata.gz: 59ea217f976c38a00fb0a80bac2fd23c73f592622efcef1e8689db454ced0e54365da3e9a459f0b0a1a26d6e34ae5c4e5a4105d748f90ccb16e98a20b7e1bf8e
7
+ data.tar.gz: 58be1670722255f6886aae39685cf7357406e4b1e74d97b752752faa682574b8d5914bd233ad9c32f8683197e33e79e38220062faf4144deb34a85ccd729ddd0
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Volmer Soares
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,79 @@
1
+ # Policial
2
+
3
+ [![Build Status](https://travis-ci.org/volmer/policial.svg)](https://travis-ci.org/volmer/policial)
4
+
5
+ *Policial* is a gem that investigates pull requests and accuses style guide
6
+ violations. It is based on thoughtbot's
7
+ [Hound project](https://github.com/thoughtbot/hound).
8
+ Currently it only investigates ruby code. You can setup your ruby code style
9
+ rules by defining a `.rubocop.yml` file in you repo. Please see
10
+ [RuboCop's README](https://github.com/bbatsov/rubocop).
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'policial'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install policial
27
+
28
+ ## Usage
29
+
30
+ First, you need to set your GitHub credentials. For more information on
31
+ this, please check [Octokit README](https://github.com/octokit/octokit.rb).
32
+
33
+ ```ruby
34
+ Octokit.configure do |c|
35
+ c.access_tokein = 'mygithubtoken666'
36
+ end
37
+ ```
38
+
39
+ You start with a pull request which Policial will run an investigation
40
+ against. You can setup a pull request manually:
41
+
42
+ ```ruby
43
+ pull_request = Policial::PullRequest.new(
44
+ repo: 'volmer/my_repo',
45
+ number: 3,
46
+ head_sha: 'headsha'
47
+ )
48
+ ```
49
+
50
+ Or you can extract a pull request from a
51
+ [GitHub `pull_request` webhook](https://developer.github.com/webhooks):
52
+
53
+ ```ruby
54
+ event = Policial::PullRequestEvent.new(webhook_payload)
55
+ pull_request = event.pull_request
56
+ ```
57
+
58
+ Now you can start an investigation:
59
+
60
+ ```ruby
61
+ investigation = Policial::Investigation.new(pull_request)
62
+
63
+ # Let's investigate this pull request...
64
+ investigation.run
65
+
66
+ # Want to know the violations found?
67
+ investigation.violations
68
+
69
+ # Hurry, post comments about those violations on the pull request!
70
+ investigation.accuse
71
+ ```
72
+
73
+ ## Contributing
74
+
75
+ 1. Fork it ( https://github.com/volmer/policial/fork )
76
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
77
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
78
+ 4. Push to the branch (`git push origin my-new-feature`)
79
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'rubocop/rake_task'
4
+
5
+ RuboCop::RakeTask.new
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: [:rubocop, :spec]
data/lib/policial.rb ADDED
@@ -0,0 +1,28 @@
1
+ require 'octokit'
2
+
3
+ require 'policial/accusation_policy'
4
+ require 'policial/commenter'
5
+ require 'policial/commit'
6
+ require 'policial/commit_file'
7
+ require 'policial/investigation'
8
+ require 'policial/line'
9
+ require 'policial/patch'
10
+ require 'policial/pull_request'
11
+ require 'policial/pull_request_event'
12
+ require 'policial/repo_config'
13
+ require 'policial/style_checker'
14
+ require 'policial/style_guides/base'
15
+ require 'policial/style_guides/ruby'
16
+ require 'policial/style_guides/unsupported'
17
+ require 'policial/unchanged_line'
18
+ require 'policial/version'
19
+ require 'policial/violation'
20
+
21
+ # Public: The global gem module. It exposes some module attribute accessors
22
+ # so you can configure GitHub credentials, enable/disable style guides
23
+ # and more.
24
+ module Policial
25
+ STYLE_GUIDES = [Policial::StyleGuides::Ruby]
26
+
27
+ Octokit.auto_paginate = true
28
+ end
@@ -0,0 +1,34 @@
1
+ module Policial
2
+ # Public: Checks if a given pull request should be investigated or not.
3
+ class AccusationPolicy
4
+ def initialize(pull_request)
5
+ @pull_request = pull_request
6
+ end
7
+
8
+ def allowed_for?(violation)
9
+ unreported_violation_messages(violation).any?
10
+ end
11
+
12
+ private
13
+
14
+ def unreported_violation_messages(violation)
15
+ violation.messages - existing_violation_messages(violation)
16
+ end
17
+
18
+ def existing_violation_messages(violation)
19
+ previous_comments_on_line(violation).map(&:body)
20
+ .flat_map { |body| body.split('<br>') }
21
+ end
22
+
23
+ def previous_comments_on_line(violation)
24
+ existing_comments.select do |comment|
25
+ comment.path == violation.filename &&
26
+ comment.original_position == violation.patch_position
27
+ end
28
+ end
29
+
30
+ def existing_comments
31
+ @existing_comments ||= @pull_request.comments
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,25 @@
1
+ module Policial
2
+ # Public: Comment violations on pull request.
3
+ class Commenter
4
+ def initialize(pull_request)
5
+ @pull_request = pull_request
6
+ end
7
+
8
+ def comment_violation(violation)
9
+ Octokit.create_pull_request_comment(
10
+ @pull_request.repo,
11
+ @pull_request.number,
12
+ comment_body(violation),
13
+ @pull_request.head_commit.sha,
14
+ violation.filename,
15
+ violation.line_number
16
+ )
17
+ end
18
+
19
+ private
20
+
21
+ def comment_body(violation)
22
+ violation.messages.join('<br/>')
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ module Policial
2
+ # Public: A Commit in a GitHub repo.
3
+ class Commit
4
+ attr_reader :repo, :sha
5
+
6
+ def initialize(repo, sha)
7
+ @repo = repo
8
+ @sha = sha
9
+ end
10
+
11
+ def file_content(filename)
12
+ contents = Octokit.contents(@repo, path: filename, ref: @sha)
13
+
14
+ if contents && contents.content
15
+ Base64.decode64(contents.content).force_encoding('UTF-8')
16
+ else
17
+ ''
18
+ end
19
+ rescue Octokit::NotFound
20
+ ''
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,40 @@
1
+ module Policial
2
+ # Public: A file in a commit.
3
+ class CommitFile
4
+ attr_reader :commit
5
+
6
+ def initialize(file, commit)
7
+ @file = file
8
+ @commit = commit
9
+ end
10
+
11
+ def filename
12
+ @file.filename
13
+ end
14
+
15
+ def content
16
+ @content ||= begin
17
+ @commit.file_content(filename) unless removed?
18
+ end
19
+ end
20
+
21
+ def removed?
22
+ @file.status == 'removed'
23
+ end
24
+
25
+ def line_at(line_number)
26
+ changed_lines.detect { |line| line.number == line_number } ||
27
+ UnchangedLine.new
28
+ end
29
+
30
+ private
31
+
32
+ def changed_lines
33
+ @changed_lines ||= patch.changed_lines
34
+ end
35
+
36
+ def patch
37
+ Patch.new(@file.patch)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,35 @@
1
+ module Policial
2
+ # Public: Starting with unparsed data coming from a pull request event,
3
+ # it checks all changes introduced looking for style guide violations. It
4
+ # also accuse all present violations through comments on all relevant lines
5
+ # in the pull request.
6
+ class Investigation
7
+ attr_accessor :violations, :pull_request
8
+
9
+ def initialize(pull_request)
10
+ @pull_request = pull_request
11
+ end
12
+
13
+ def run
14
+ @violations ||= StyleChecker.new(@pull_request).violations
15
+ end
16
+
17
+ def accuse
18
+ return if @violations.blank?
19
+
20
+ commenter = Commenter.new(@pull_request)
21
+
22
+ @violations.each do |violation|
23
+ if accusation_policy.allowed_for?(violation)
24
+ commenter.comment_violation(violation)
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def accusation_policy
32
+ @accusation_policy ||= AccusationPolicy.new(@pull_request)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,16 @@
1
+ module Policial
2
+ # Public: a changed line in a commit file.
3
+ class Line
4
+ attr_reader :number, :patch_position
5
+
6
+ def initialize(number, content, patch_position)
7
+ @number = number
8
+ @content = content
9
+ @patch_position = patch_position
10
+ end
11
+
12
+ def changed?
13
+ true
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,35 @@
1
+ module Policial
2
+ # Public: a chunk of changed code in a commit file.
3
+ class Patch
4
+ RANGE_INFORMATION_LINE = /^@@ .+\+(?<line_number>\d+),/
5
+ MODIFIED_LINE = /^\+(?!\+|\+)/
6
+ NOT_REMOVED_LINE = /^[^-]/
7
+
8
+ def initialize(body)
9
+ @body = body || ''
10
+ end
11
+
12
+ def changed_lines
13
+ line_number = 0
14
+
15
+ @body.lines.each_with_index.with_object([]) do |(line, patch_pos), lines|
16
+ line_number =
17
+ parse_line(line, line_number, patch_pos, lines) || line_number
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def parse_line(line_content, line_number, patch_position, lines)
24
+ case line_content
25
+ when RANGE_INFORMATION_LINE
26
+ Regexp.last_match[:line_number].to_i
27
+ when MODIFIED_LINE
28
+ lines << Line.new(line_number, line_content, patch_position)
29
+ line_number + 1
30
+ when NOT_REMOVED_LINE
31
+ line_number + 1
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,60 @@
1
+ module Policial
2
+ # Public: A GibHub Pull Request.
3
+ class PullRequest
4
+ attr_reader :repo, :number, :user
5
+
6
+ def initialize(repo:, number:, head_sha:, user: nil)
7
+ @repo = repo
8
+ @number = number
9
+ @head_sha = head_sha
10
+ @user = user
11
+ end
12
+
13
+ def comments
14
+ @comments ||= fetch_comments
15
+ end
16
+
17
+ def files
18
+ @files ||= Octokit.pull_request_files(@repo, @number).map do |file|
19
+ build_commit_file(file)
20
+ end
21
+ end
22
+
23
+ def head_commit
24
+ @head_commit ||= Commit.new(@repo, @head_sha)
25
+ end
26
+
27
+ private
28
+
29
+ def build_commit_file(file)
30
+ CommitFile.new(file, head_commit)
31
+ end
32
+
33
+ def fetch_comments
34
+ paginate do |page|
35
+ Octokit.pull_request_comments(
36
+ @repo,
37
+ @number,
38
+ page: page
39
+ )
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def paginate
46
+ page, results, all_pages_fetched = 1, [], false
47
+
48
+ until all_pages_fetched
49
+ if (page_results = yield(page)).empty?
50
+ all_pages_fetched = true
51
+ else
52
+ results += page_results
53
+ page += 1
54
+ end
55
+ end
56
+
57
+ results
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,29 @@
1
+ require 'json'
2
+
3
+ module Policial
4
+ # Public: Parses a pull request event payload.
5
+ class PullRequestEvent
6
+ attr_reader :payload
7
+
8
+ def initialize(payload)
9
+ @payload = payload
10
+ end
11
+
12
+ def pull_request
13
+ @pull_request ||= PullRequest.new(
14
+ repo: @payload['repository']['full_name'],
15
+ number: @payload['number'],
16
+ head_sha: @payload['pull_request']['head']['sha'],
17
+ user: @payload['pull_request']['user']['login']
18
+ )
19
+ rescue NoMethodError
20
+ nil
21
+ end
22
+
23
+ def should_investigate?
24
+ !pull_request.nil? && (
25
+ @payload['action'] == 'opened' || @payload['action'] == 'synchronize'
26
+ )
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,36 @@
1
+ module Policial
2
+ # Public: Load and parse config files from GitHub repo.
3
+ class RepoConfig
4
+ def initialize(commit)
5
+ @commit = commit
6
+ end
7
+
8
+ def enabled_for?(style_guide_class)
9
+ Policial::STYLE_GUIDES.include?(style_guide_class)
10
+ end
11
+
12
+ def for(style_guide_class)
13
+ config_file = style_guide_class.config_file
14
+
15
+ if config_file
16
+ load_file(config_file)
17
+ else
18
+ {}
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def load_file(file)
25
+ config_file_content = @commit.file_content(file)
26
+
27
+ parse_yaml(config_file_content) || {}
28
+ end
29
+
30
+ def parse_yaml(content)
31
+ YAML.load(content)
32
+ rescue Psych::SyntaxError
33
+ {}
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,48 @@
1
+ module Policial
2
+ # Public: Filters files to reviewable subset, builds style guide based on file
3
+ # extension and delegates to style guide for line violations.
4
+ class StyleChecker
5
+ def initialize(pull_request)
6
+ @pull_request = pull_request
7
+ @style_guides = {}
8
+ end
9
+
10
+ def violations
11
+ @violations ||= violations_in_checked_files.select(&:on_changed_line?)
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :pull_request, :style_guides
17
+
18
+ def violations_in_checked_files
19
+ files_to_check.flat_map do |file|
20
+ style_guide(file.filename).violations_in_file(file)
21
+ end
22
+ end
23
+
24
+ def files_to_check
25
+ pull_request.files.reject(&:removed?).select do |file|
26
+ style_guide(file.filename).enabled?
27
+ end
28
+ end
29
+
30
+ def style_guide(filename)
31
+ style_guide_class = style_guide_class(filename)
32
+ style_guides[style_guide_class] ||= style_guide_class.new(config)
33
+ end
34
+
35
+ def style_guide_class(filename)
36
+ case filename
37
+ when /.+\.rb\z/
38
+ StyleGuides::Ruby
39
+ else
40
+ StyleGuides::Unsupported
41
+ end
42
+ end
43
+
44
+ def config
45
+ @config ||= RepoConfig.new(pull_request.head_commit)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,18 @@
1
+ module Policial
2
+ module StyleGuides
3
+ # Public: Base to contain common style guide logic.
4
+ class Base
5
+ def initialize(repo_config)
6
+ @repo_config = repo_config
7
+ end
8
+
9
+ def enabled?
10
+ @repo_config.enabled_for?(self.class)
11
+ end
12
+
13
+ def violations_in_file(_file)
14
+ fail NotImplementedError, "must implement ##{__method__}"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,67 @@
1
+ require 'rubocop'
2
+
3
+ module Policial
4
+ module StyleGuides
5
+ # Public: Determine Ruby style guide violations per-line.
6
+ class Ruby < Base
7
+ class << self
8
+ attr_writer :config_file
9
+
10
+ def config_file
11
+ @config_file || RuboCop::ConfigLoader::DOTFILE
12
+ end
13
+ end
14
+
15
+ def violations_in_file(file)
16
+ if config.file_to_exclude?(file.filename)
17
+ []
18
+ else
19
+ offenses = team.inspect_file(parsed_source(file))
20
+ build_violations(offenses, file)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def team
27
+ RuboCop::Cop::Team.new(RuboCop::Cop::Cop.all, config, rubocop_options)
28
+ end
29
+
30
+ def parsed_source(file)
31
+ RuboCop::ProcessedSource.new(file.content, file.filename)
32
+ end
33
+
34
+ def config
35
+ @config ||= RuboCop::ConfigLoader.merge_with_default(custom_config, '')
36
+ end
37
+
38
+ def custom_config
39
+ custom = @repo_config.for(self.class)
40
+
41
+ if custom.is_a?(Hash)
42
+ RuboCop::Config.new(custom, '').tap do |config|
43
+ config.add_missing_namespaces
44
+ config.make_excludes_absolute
45
+ end
46
+ else
47
+ RuboCop::Config.new
48
+ end
49
+ end
50
+
51
+ def rubocop_options
52
+ { debug: true } if config['ShowCopNames']
53
+ end
54
+
55
+ def build_violations(offenses, file)
56
+ offenses.each_with_object({}) do |offense, violations|
57
+ if violations[offense.line]
58
+ violations[offense.line].add_messages([offense.message])
59
+ else
60
+ violations[offense.line] =
61
+ Violation.new(file, offense.line, offense.message)
62
+ end
63
+ end.values
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,10 @@
1
+ module Policial
2
+ module StyleGuides
3
+ # Public: Returns empty set of violations.
4
+ class Unsupported < Base
5
+ def violations_in_file(_)
6
+ []
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ module Policial
2
+ # Public: An unchanged line in a commit file.
3
+ class UnchangedLine
4
+ def initialize(*)
5
+ end
6
+
7
+ def patch_position
8
+ -1
9
+ end
10
+
11
+ def changed?
12
+ false
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ # Public: The gem version.
2
+ module Policial
3
+ VERSION = '0.0.1'
4
+ end
@@ -0,0 +1,30 @@
1
+ module Policial
2
+ # Public: Hold file, line, and violation message values. Built by style
3
+ # guides.
4
+ class Violation
5
+ attr_reader :line_number, :filename
6
+
7
+ def initialize(file, line_number, message)
8
+ @filename = file.filename
9
+ @line = file.line_at(line_number)
10
+ @line_number = line_number
11
+ @messages = [message]
12
+ end
13
+
14
+ def add_messages(messages)
15
+ @messages += messages
16
+ end
17
+
18
+ def messages
19
+ @messages.uniq
20
+ end
21
+
22
+ def patch_position
23
+ @line.patch_position
24
+ end
25
+
26
+ def on_changed_line?
27
+ @line.changed?
28
+ end
29
+ end
30
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: policial
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Volmer Soares
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-12-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: octokit
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.7'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.2'
41
+ description: Review pull requests for style guide violations.
42
+ email:
43
+ - volmerius@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - LICENSE.txt
49
+ - README.md
50
+ - Rakefile
51
+ - lib/policial.rb
52
+ - lib/policial/accusation_policy.rb
53
+ - lib/policial/commenter.rb
54
+ - lib/policial/commit.rb
55
+ - lib/policial/commit_file.rb
56
+ - lib/policial/investigation.rb
57
+ - lib/policial/line.rb
58
+ - lib/policial/patch.rb
59
+ - lib/policial/pull_request.rb
60
+ - lib/policial/pull_request_event.rb
61
+ - lib/policial/repo_config.rb
62
+ - lib/policial/style_checker.rb
63
+ - lib/policial/style_guides/base.rb
64
+ - lib/policial/style_guides/ruby.rb
65
+ - lib/policial/style_guides/unsupported.rb
66
+ - lib/policial/unchanged_line.rb
67
+ - lib/policial/version.rb
68
+ - lib/policial/violation.rb
69
+ homepage: https://github.com/volmer/policial
70
+ licenses:
71
+ - MIT
72
+ metadata: {}
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 2.4.5
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: Review pull requests for style guide violations
93
+ test_files: []