httparty 0.20.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -3,6 +3,7 @@ gemspec
3
3
 
4
4
  gem 'rake'
5
5
  gem 'mongrel', '1.2.0.pre2'
6
+ gem 'json'
6
7
 
7
8
  group :development do
8
9
  gem 'guard'
@@ -11,6 +12,7 @@ group :development do
11
12
  end
12
13
 
13
14
  group :test do
15
+ gem 'rexml'
14
16
  gem 'rspec', '~> 3.4'
15
17
  gem 'simplecov', require: false
16
18
  gem 'aruba'
data/Guardfile CHANGED
@@ -1,7 +1,8 @@
1
1
  rspec_options = {
2
- version: 1,
3
2
  all_after_pass: false,
4
- all_on_start: false
3
+ all_on_start: false,
4
+ failed_mode: :keep,
5
+ cmd: 'bundle exec rspec',
5
6
  }
6
7
 
7
8
  guard 'rspec', rspec_options do
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # httparty
2
2
 
3
- [![Build Status](https://travis-ci.org/jnunemaker/httparty.svg?branch=master)](https://travis-ci.org/jnunemaker/httparty)
3
+ [![CI](https://github.com/jnunemaker/httparty/actions/workflows/ci.yml/badge.svg)](https://github.com/jnunemaker/httparty/actions/workflows/ci.yml)
4
4
 
5
5
  Makes http fun again! Ain't no party like a httparty, because a httparty don't stop.
6
6
 
data/docs/README.md CHANGED
@@ -14,6 +14,20 @@ response = HTTParty.get('http://example.com', format: :plain)
14
14
  JSON.parse response, symbolize_names: true
15
15
  ```
16
16
 
17
+ ## Posting JSON
18
+ When using Content Type `application/json` with `POST`, `PUT` or `PATCH` requests, the body should be a string of valid JSON:
19
+
20
+ ```ruby
21
+ # With written JSON
22
+ HTTParty.post('http://example.com', body: "{\"foo\":\"bar\"}", headers: { 'Content-Type' => 'application/json' })
23
+
24
+ # Using JSON.generate
25
+ HTTParty.post('http://example.com', body: JSON.generate({ foo: 'bar' }), headers: { 'Content-Type' => 'application/json' })
26
+
27
+ # Using object.to_json
28
+ HTTParty.post('http://example.com', body: { foo: 'bar' }.to_json, headers: { 'Content-Type' => 'application/json' })
29
+ ```
30
+
17
31
  ## Working with SSL
18
32
 
19
33
  You can use this guide to work with SSL certificates.
@@ -123,11 +137,16 @@ If you explicitly set `Accept-Encoding`, there be dragons:
123
137
  `Net::HTTP` will automatically decompress it, and will omit `Content-Encoding`
124
138
  from your `HTTParty::Response` headers.
125
139
 
126
- * For encodings `br` (Brotli) or `compress` (LZW), HTTParty will automatically
127
- decompress if you include the `brotli` or `ruby-lzws` gems respectively into your project.
140
+ * For the following encodings, HTTParty will automatically decompress them if you include
141
+ the required gem into your project. Similar to above, if decompression succeeds,
142
+ `Content-Encoding` will be omitted from your `HTTParty::Response` headers.
128
143
  **Warning:** Support for these encodings is experimental and not fully battle-tested.
129
- Similar to above, if decompression succeeds, `Content-Encoding` will be omitted
130
- from your `HTTParty::Response` headers.
144
+
145
+ | Content-Encoding | Required Gem |
146
+ | --- | --- |
147
+ | `br` (Brotli) | [brotli](https://rubygems.org/gems/brotli) |
148
+ | `compress` (LZW) | [ruby-lzws](https://rubygems.org/gems/ruby-lzws) |
149
+ | `zstd` (Zstandard) | [zstd-ruby](https://rubygems.org/gems/zstd-ruby) |
131
150
 
132
151
  * For other encodings, `HTTParty::Response#body` will return the raw uncompressed byte string,
133
152
  and you'll need to inspect the `Content-Encoding` response header and decompress it yourself.
@@ -147,7 +166,8 @@ JSON.parse(res.body) # safe
147
166
 
148
167
  require 'brotli'
149
168
  require 'lzws'
150
- res = HTTParty.get('https://example.com/test.json', headers: { 'Accept-Encoding' => 'br,compress' })
169
+ require 'zstd-ruby'
170
+ res = HTTParty.get('https://example.com/test.json', headers: { 'Accept-Encoding' => 'br,compress,zstd' })
151
171
  JSON.parse(res.body)
152
172
 
153
173
 
data/httparty.gemspec CHANGED
@@ -13,10 +13,11 @@ Gem::Specification.new do |s|
13
13
  s.summary = 'Makes http fun! Also, makes consuming restful web services dead easy.'
14
14
  s.description = 'Makes http fun! Also, makes consuming restful web services dead easy.'
15
15
 
16
- s.required_ruby_version = '>= 2.3.0'
16
+ s.required_ruby_version = '>= 2.7.0'
17
17
 
18
+ s.add_dependency 'csv'
18
19
  s.add_dependency 'multi_xml', ">= 0.5.2"
19
- s.add_dependency('mime-types', "~> 3.0")
20
+ s.add_dependency 'mini_mime', ">= 1.0.0"
20
21
 
21
22
  # If this line is removed, all hard partying will cease.
22
23
  s.post_install_message = "When you HTTParty, you must party hard!"
@@ -119,10 +119,7 @@ module HTTParty
119
119
  if add_timeout?(options[:timeout])
120
120
  http.open_timeout = options[:timeout]
121
121
  http.read_timeout = options[:timeout]
122
-
123
- from_ruby_version('2.6.0', option: :write_timeout, warn: false) do
124
- http.write_timeout = options[:timeout]
125
- end
122
+ http.write_timeout = options[:timeout]
126
123
  end
127
124
 
128
125
  if add_timeout?(options[:read_timeout])
@@ -134,15 +131,11 @@ module HTTParty
134
131
  end
135
132
 
136
133
  if add_timeout?(options[:write_timeout])
137
- from_ruby_version('2.6.0', option: :write_timeout) do
138
- http.write_timeout = options[:write_timeout]
139
- end
134
+ http.write_timeout = options[:write_timeout]
140
135
  end
141
136
 
142
137
  if add_max_retries?(options[:max_retries])
143
- from_ruby_version('2.5.0', option: :max_retries) do
144
- http.max_retries = options[:max_retries]
145
- end
138
+ http.max_retries = options[:max_retries]
146
139
  end
147
140
 
148
141
  if options[:debug_output]
@@ -157,15 +150,11 @@ module HTTParty
157
150
  #
158
151
  # @see https://bugs.ruby-lang.org/issues/6617
159
152
  if options[:local_host]
160
- from_ruby_version('2.0.0', option: :local_host) do
161
- http.local_host = options[:local_host]
162
- end
153
+ http.local_host = options[:local_host]
163
154
  end
164
155
 
165
156
  if options[:local_port]
166
- from_ruby_version('2.0.0', option: :local_port) do
167
- http.local_port = options[:local_port]
168
- end
157
+ http.local_port = options[:local_port]
169
158
  end
170
159
 
171
160
  http
@@ -173,14 +162,6 @@ module HTTParty
173
162
 
174
163
  private
175
164
 
176
- def from_ruby_version(ruby_version, option: nil, warn: true)
177
- if RUBY_VERSION >= ruby_version
178
- yield
179
- elsif warn
180
- Kernel.warn("Warning: option #{ option } requires Ruby version #{ ruby_version } or later")
181
- end
182
- end
183
-
184
165
  def add_timeout?(timeout)
185
166
  timeout && (timeout.is_a?(Integer) || timeout.is_a?(Float))
186
167
  end
@@ -17,7 +17,8 @@ module HTTParty
17
17
  'none' => :none,
18
18
  'identity' => :none,
19
19
  'br' => :brotli,
20
- 'compress' => :lzw
20
+ 'compress' => :lzw,
21
+ 'zstd' => :zstd
21
22
  }.freeze
22
23
 
23
24
  # The response body of the request
@@ -88,5 +89,14 @@ module HTTParty
88
89
  nil
89
90
  end
90
91
  end
92
+
93
+ def zstd
94
+ return nil unless defined?(::Zstd)
95
+ begin
96
+ ::Zstd.decompress(body)
97
+ rescue StandardError
98
+ nil
99
+ end
100
+ end
91
101
  end
92
102
  end
@@ -22,7 +22,7 @@ module HTTParty
22
22
  log_request
23
23
  log_response
24
24
 
25
- logger.public_send level, messages.join('\n')
25
+ logger.public_send level, messages.join("\n")
26
26
  end
27
27
 
28
28
  private
@@ -24,6 +24,7 @@ module HTTParty
24
24
  attr_reader :request, :response
25
25
 
26
26
  def logstash_message
27
+ require 'json'
27
28
  {
28
29
  '@timestamp' => current_time,
29
30
  '@version' => 1,
@@ -29,7 +29,7 @@ module HTTParty
29
29
  @mattr_inheritable_attrs += args
30
30
 
31
31
  args.each do |arg|
32
- module_eval %(class << self; attr_accessor :#{arg} end)
32
+ singleton_class.attr_accessor(arg)
33
33
  end
34
34
 
35
35
  @mattr_inheritable_attrs
@@ -42,14 +42,12 @@ module HTTParty
42
42
  subclass.instance_variable_set(ivar, instance_variable_get(ivar).clone)
43
43
 
44
44
  if instance_variable_get(ivar).respond_to?(:merge)
45
- method = <<-EOM
45
+ subclass.class_eval <<~RUBY, __FILE__, __LINE__ + 1
46
46
  def self.#{inheritable_attribute}
47
47
  duplicate = ModuleInheritableAttributes.hash_deep_dup(#{ivar})
48
48
  #{ivar} = superclass.#{inheritable_attribute}.merge(duplicate)
49
49
  end
50
- EOM
51
-
52
- subclass.class_eval method
50
+ RUBY
53
51
  end
54
52
  end
55
53
  end
@@ -118,16 +118,19 @@ module HTTParty
118
118
  protected
119
119
 
120
120
  def xml
121
+ require 'multi_xml'
121
122
  MultiXml.parse(body)
122
123
  end
123
124
 
124
125
  UTF8_BOM = "\xEF\xBB\xBF"
125
126
 
126
127
  def json
128
+ require 'json'
127
129
  JSON.parse(body, :quirks_mode => true, :allow_nan => true)
128
130
  end
129
131
 
130
132
  def csv
133
+ require 'csv'
131
134
  CSV.parse(body)
132
135
  end
133
136
 
@@ -32,6 +32,13 @@ module HTTParty
32
32
 
33
33
  private
34
34
 
35
+ # https://html.spec.whatwg.org/#multipart-form-data
36
+ MULTIPART_FORM_DATA_REPLACEMENT_TABLE = {
37
+ '"' => '%22',
38
+ "\r" => '%0D',
39
+ "\n" => '%0A'
40
+ }.freeze
41
+
35
42
  def generate_multipart
36
43
  normalized_params = params.flat_map { |key, value| HashConversions.normalize_keys(key, value) }
37
44
 
@@ -40,7 +47,7 @@ module HTTParty
40
47
  memo << %(Content-Disposition: form-data; name="#{key}")
41
48
  # value.path is used to support ActionDispatch::Http::UploadedFile
42
49
  # https://github.com/jnunemaker/httparty/pull/585
43
- memo << %(; filename="#{file_name(value)}") if file?(value)
50
+ memo << %(; filename="#{file_name(value).gsub(/["\r\n]/, MULTIPART_FORM_DATA_REPLACEMENT_TABLE)}") if file?(value)
44
51
  memo << NEWLINE
45
52
  memo << "Content-Type: #{content_type(value)}#{NEWLINE}" if file?(value)
46
53
  memo << NEWLINE
@@ -84,8 +91,9 @@ module HTTParty
84
91
 
85
92
  def content_type(object)
86
93
  return object.content_type if object.respond_to?(:content_type)
87
- mime = MIME::Types.type_for(object.path)
88
- mime.empty? ? 'application/octet-stream' : mime[0].content_type
94
+ require 'mini_mime'
95
+ mime = MiniMime.lookup_by_filename(object.path)
96
+ mime ? mime.content_type : 'application/octet-stream'
89
97
  end
90
98
 
91
99
  def file_name(object)
@@ -47,8 +47,12 @@ module HTTParty
47
47
  end
48
48
 
49
49
  def self._load(data)
50
- http_method, path, options = Marshal.load(data)
51
- new(http_method, path, options)
50
+ http_method, path, options, last_response, last_uri, raw_request = Marshal.load(data)
51
+ instance = new(http_method, path, options)
52
+ instance.last_response = last_response
53
+ instance.last_uri = last_uri
54
+ instance.instance_variable_set("@raw_request", raw_request)
55
+ instance
52
56
  end
53
57
 
54
58
  attr_accessor :http_method, :options, :last_response, :redirect, :last_uri
@@ -184,7 +188,7 @@ module HTTParty
184
188
  opts = options.dup
185
189
  opts.delete(:logger)
186
190
  opts.delete(:parser) if opts[:parser] && opts[:parser].is_a?(Proc)
187
- Marshal.dump([http_method, path, opts])
191
+ Marshal.dump([http_method, path, opts, last_response, @last_uri, @raw_request])
188
192
  end
189
193
 
190
194
  private
@@ -291,24 +295,7 @@ module HTTParty
291
295
 
292
296
  def handle_response(raw_body, &block)
293
297
  if response_redirects?
294
- options[:limit] -= 1
295
- if options[:logger]
296
- logger = HTTParty::Logger.build(options[:logger], options[:log_level], options[:log_format])
297
- logger.format(self, last_response)
298
- end
299
- self.path = last_response['location']
300
- self.redirect = true
301
- if last_response.class == Net::HTTPSeeOther
302
- unless options[:maintain_method_across_redirects] && options[:resend_on_redirect]
303
- self.http_method = Net::HTTP::Get
304
- end
305
- elsif last_response.code != '307' && last_response.code != '308'
306
- unless options[:maintain_method_across_redirects]
307
- self.http_method = Net::HTTP::Get
308
- end
309
- end
310
- capture_cookies(last_response)
311
- perform(&block)
298
+ handle_redirection(&block)
312
299
  else
313
300
  raw_body ||= last_response.body
314
301
 
@@ -327,10 +314,34 @@ module HTTParty
327
314
  end
328
315
  end
329
316
 
317
+ def handle_redirection(&block)
318
+ options[:limit] -= 1
319
+ if options[:logger]
320
+ logger = HTTParty::Logger.build(options[:logger], options[:log_level], options[:log_format])
321
+ logger.format(self, last_response)
322
+ end
323
+ self.path = last_response['location']
324
+ self.redirect = true
325
+ if last_response.class == Net::HTTPSeeOther
326
+ unless options[:maintain_method_across_redirects] && options[:resend_on_redirect]
327
+ self.http_method = Net::HTTP::Get
328
+ end
329
+ elsif last_response.code != '307' && last_response.code != '308'
330
+ unless options[:maintain_method_across_redirects]
331
+ self.http_method = Net::HTTP::Get
332
+ end
333
+ end
334
+ if http_method == Net::HTTP::Get
335
+ clear_body
336
+ end
337
+ capture_cookies(last_response)
338
+ perform(&block)
339
+ end
340
+
330
341
  def handle_host_redirection
331
342
  check_duplicate_location_header
332
343
  redirect_path = options[:uri_adapter].parse(last_response['location']).normalize
333
- return if redirect_path.relative? || path.host == redirect_path.host
344
+ return if redirect_path.relative? || path.host == redirect_path.host || uri.host == redirect_path.host
334
345
  @changed_hosts = true
335
346
  end
336
347
 
@@ -358,6 +369,14 @@ module HTTParty
358
369
  parser.call(body, format)
359
370
  end
360
371
 
372
+ # Some Web Application Firewalls reject incoming GET requests that have a body
373
+ # if we redirect, and the resulting verb is GET then we will clear the body that
374
+ # may be left behind from the initiating request
375
+ def clear_body
376
+ options[:body] = nil
377
+ @raw_request.body = nil
378
+ end
379
+
361
380
  def capture_cookies(response)
362
381
  return unless response['Set-Cookie']
363
382
  cookies_hash = HTTParty::CookieHash.new
@@ -67,12 +67,12 @@ module HTTParty
67
67
  end
68
68
 
69
69
  # Support old multiple_choice? method from pre 2.0.0 era.
70
- if ::RUBY_VERSION >= '2.0.0' && ::RUBY_PLATFORM != 'java'
70
+ if ::RUBY_PLATFORM != 'java'
71
71
  alias_method :multiple_choice?, :multiple_choices?
72
72
  end
73
73
 
74
74
  # Support old status codes method from pre 2.6.0 era.
75
- if ::RUBY_VERSION >= '2.6.0' && ::RUBY_PLATFORM != 'java'
75
+ if ::RUBY_PLATFORM != 'java'
76
76
  alias_method :gateway_time_out?, :gateway_timeout?
77
77
  alias_method :request_entity_too_large?, :payload_too_large?
78
78
  alias_method :request_time_out?, :request_timeout?
@@ -133,7 +133,7 @@ module HTTParty
133
133
  end
134
134
 
135
135
  def throw_exception
136
- if @request.options[:raise_on] && @request.options[:raise_on].include?(code)
136
+ if @request.options[:raise_on].to_a.detect { |c| code.to_s.match(/#{c.to_s}/) }
137
137
  ::Kernel.raise ::HTTParty::ResponseError.new(@response), "Code #{code} - #{body}"
138
138
  end
139
139
  end
@@ -5,7 +5,7 @@ module HTTParty
5
5
  attr_reader :text, :content_type, :assume_utf16_is_big_endian
6
6
 
7
7
  def initialize(text, assume_utf16_is_big_endian: true, content_type: nil)
8
- @text = text.dup
8
+ @text = +text
9
9
  @content_type = content_type
10
10
  @assume_utf16_is_big_endian = assume_utf16_is_big_endian
11
11
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTParty
4
- VERSION = '0.20.0'
4
+ VERSION = '0.22.0'
5
5
  end
data/lib/httparty.rb CHANGED
@@ -2,13 +2,7 @@
2
2
 
3
3
  require 'pathname'
4
4
  require 'net/http'
5
- require 'net/https'
6
5
  require 'uri'
7
- require 'zlib'
8
- require 'multi_xml'
9
- require 'mime/types'
10
- require 'json'
11
- require 'csv'
12
6
 
13
7
  require 'httparty/module_inheritable_attributes'
14
8
  require 'httparty/cookie_hash'
@@ -84,7 +78,7 @@ module HTTParty
84
78
  #
85
79
  # class Foo
86
80
  # include HTTParty
87
- # raise_on [404, 500]
81
+ # raise_on [404, 500, '5[0-9]*']
88
82
  # end
89
83
  def raise_on(codes = [])
90
84
  default_options[:raise_on] = *codes
@@ -592,6 +586,13 @@ module HTTParty
592
586
  perform_request Net::HTTP::Unlock, path, options, &block
593
587
  end
594
588
 
589
+ def build_request(http_method, path, options = {})
590
+ options = ModuleInheritableAttributes.hash_deep_dup(default_options).merge(options)
591
+ HeadersProcessor.new(headers, options).call
592
+ process_cookies(options)
593
+ Request.new(http_method, path, options)
594
+ end
595
+
595
596
  attr_reader :default_options
596
597
 
597
598
  private
@@ -607,10 +608,7 @@ module HTTParty
607
608
  end
608
609
 
609
610
  def perform_request(http_method, path, options, &block) #:nodoc:
610
- options = ModuleInheritableAttributes.hash_deep_dup(default_options).merge(options)
611
- HeadersProcessor.new(headers, options).call
612
- process_cookies(options)
613
- Request.new(http_method, path, options).perform(&block)
611
+ build_request(http_method, path, options).perform(&block)
614
612
  end
615
613
 
616
614
  def process_cookies(options) #:nodoc:
@@ -677,6 +675,10 @@ module HTTParty
677
675
  def self.options(*args, &block)
678
676
  Basement.options(*args, &block)
679
677
  end
678
+
679
+ def self.build_request(*args, &block)
680
+ Basement.build_request(*args, &block)
681
+ end
680
682
  end
681
683
 
682
684
  require 'httparty/hash_conversions'
data/script/release CHANGED
@@ -18,9 +18,9 @@ gem_name=httparty
18
18
  rm -rf $gem_name-*.gem
19
19
  gem build -q $gem_name.gemspec
20
20
 
21
- # Make sure we're on the master branch.
22
- (git branch | grep -q '* master') || {
23
- echo "Only release from the master branch."
21
+ # Make sure we're on the main branch.
22
+ (git branch | grep -q '* main') || {
23
+ echo "Only release from the main branch."
24
24
  exit 1
25
25
  }
26
26
 
@@ -39,4 +39,4 @@ git fetch -t origin
39
39
 
40
40
  # Tag it and bag it.
41
41
  gem push $gem_name-*.gem && git tag "$tag" &&
42
- git push origin master && git push origin "$tag"
42
+ git push origin main && git push origin "$tag"
metadata CHANGED
@@ -1,16 +1,30 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: httparty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.22.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
8
  - Sandro Turriate
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-09-29 00:00:00.000000000 Z
12
+ date: 2024-04-29 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: csv
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
14
28
  - !ruby/object:Gem::Dependency
15
29
  name: multi_xml
16
30
  requirement: !ruby/object:Gem::Requirement
@@ -26,19 +40,19 @@ dependencies:
26
40
  - !ruby/object:Gem::Version
27
41
  version: 0.5.2
28
42
  - !ruby/object:Gem::Dependency
29
- name: mime-types
43
+ name: mini_mime
30
44
  requirement: !ruby/object:Gem::Requirement
31
45
  requirements:
32
- - - "~>"
46
+ - - ">="
33
47
  - !ruby/object:Gem::Version
34
- version: '3.0'
48
+ version: 1.0.0
35
49
  type: :runtime
36
50
  prerelease: false
37
51
  version_requirements: !ruby/object:Gem::Requirement
38
52
  requirements:
39
- - - "~>"
53
+ - - ">="
40
54
  - !ruby/object:Gem::Version
41
- version: '3.0'
55
+ version: 1.0.0
42
56
  description: Makes http fun! Also, makes consuming restful web services dead easy.
43
57
  email:
44
58
  - nunemaker@gmail.com
@@ -48,11 +62,11 @@ extensions: []
48
62
  extra_rdoc_files: []
49
63
  files:
50
64
  - ".editorconfig"
65
+ - ".github/dependabot.yml"
51
66
  - ".github/workflows/ci.yml"
52
67
  - ".gitignore"
53
68
  - ".rubocop.yml"
54
69
  - ".rubocop_todo.yml"
55
- - ".simplecov"
56
70
  - CONTRIBUTING.md
57
71
  - Changelog.md
58
72
  - Gemfile
@@ -124,15 +138,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
124
138
  requirements:
125
139
  - - ">="
126
140
  - !ruby/object:Gem::Version
127
- version: 2.3.0
141
+ version: 2.7.0
128
142
  required_rubygems_version: !ruby/object:Gem::Requirement
129
143
  requirements:
130
144
  - - ">="
131
145
  - !ruby/object:Gem::Version
132
146
  version: '0'
133
147
  requirements: []
134
- rubygems_version: 3.0.3
135
- signing_key:
148
+ rubygems_version: 3.3.7
149
+ signing_key:
136
150
  specification_version: 4
137
151
  summary: Makes http fun! Also, makes consuming restful web services dead easy.
138
152
  test_files: []
data/.simplecov DELETED
@@ -1 +0,0 @@
1
- SimpleCov.start "test_frameworks"