\d+)(?\S+)?'/) do
98 | m = Regexp.last_match
99 | major = m['major'].to_i
100 | minor = m['minor'].to_i
101 | inc = m['inc'].to_i
102 | pre = m['pre']
103 |
104 | case args[:type]
105 | when /^maj/
106 | major += 1
107 | minor = 0
108 | inc = 0
109 | when /^min/
110 | minor += 1
111 | inc = 0
112 | else
113 | inc += 1
114 | end
115 |
116 | $stdout.puts "At version #{major}.#{minor}.#{inc}#{pre}"
117 | "VERSION = '#{major}.#{minor}.#{inc}#{pre}'"
118 | end
119 | File.open(version_file, 'w+') { |f| f.puts content }
120 | end
121 |
122 | task default: %i[test clobber package]
123 |
--------------------------------------------------------------------------------
/lib/curly/curl/json.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Curl
4 | # Class for CURLing a JSON response
5 | class Json
6 | attr_accessor :url
7 |
8 | attr_writer :compressed, :request_headers, :symbolize_names
9 |
10 | attr_reader :code, :json, :headers
11 |
12 | def to_data
13 | {
14 | url: @url,
15 | code: @code,
16 | json: @json,
17 | headers: @headers
18 | }
19 | end
20 |
21 | ##
22 | ## Create a new Curl::Json page object
23 | ##
24 | ## @param url [String] The url to curl
25 | ## @param headers [Hash] The headers to send
26 | ## @param compressed [Boolean] Expect compressed results
27 | ##
28 | ## @return [Curl::Json] Curl::Json object with url, code, parsed json, and response headers
29 | ##
30 | def initialize(url, options = {})
31 | @url = url
32 | @request_headers = options[:headers]
33 | @compressed = options[:compressed]
34 | @symbolize_names = options[:symbolize_names]
35 |
36 | @curl = TTY::Which.which('curl')
37 | end
38 |
39 | def curl
40 | page = curl_json
41 |
42 | raise "Error retrieving #{url}" if page.nil? || page.empty?
43 |
44 | @url = page[:url]
45 | @code = page[:code]
46 | @json = page[:json]
47 | @headers = page[:headers]
48 | end
49 |
50 | def path(path, json = @json)
51 | parts = path.split(/./)
52 | target = json
53 | parts.each do |part|
54 | if part =~ /(?[^\[]+)\[(?\d+)\]/
55 | target = target[key][int.to_i]
56 | else
57 | target = target[part]
58 | end
59 | end
60 |
61 | target
62 | end
63 |
64 | private
65 |
66 | ##
67 | ## Curl the JSON contents
68 | ##
69 | ## @param url [String] The url
70 | ## @param headers [Hash] The headers to send
71 | ## @param compressed [Boolean] Expect compressed results
72 | ##
73 | ## @return [Hash] hash of url, code, headers, and parsed json
74 | ##
75 | def curl_json
76 | flags = 'SsLi'
77 | agents = [
78 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.1',
79 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.',
80 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.3',
81 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.'
82 | ]
83 |
84 | headers = @headers.nil? ? '' : @headers.map { |h, v| %(-H "#{h}: #{v}") }.join(' ')
85 | compress = @compressed ? '--compressed' : ''
86 | source = `#{@curl} -#{flags} #{compress} #{headers} '#{@url}' 2>/dev/null`
87 | agent = 0
88 | while source.nil? || source.empty?
89 | source = `#{@curl} -#{flags} #{compress} -A "#{agents[agent]}" #{headers} '#{@url}' 2>/dev/null`
90 | break if agent >= agents.count - 1
91 | end
92 |
93 | return false if source.nil? || source.empty?
94 |
95 | source.strip!
96 |
97 | headers = {}
98 | lines = source.split(/\r\n/)
99 | code = lines[0].match(/(\d\d\d)/)[1]
100 | lines.shift
101 | lines.each_with_index do |line, idx|
102 | if line =~ /^([\w-]+): (.*?)$/
103 | m = Regexp.last_match
104 | headers[m[1]] = m[2]
105 | else
106 | source = lines[idx..].join("\n")
107 | break
108 | end
109 | end
110 |
111 | json = source.strip.force_encoding('utf-8')
112 | begin
113 | json.gsub!(/[\u{1F600}-\u{1F6FF}]/, '')
114 | { url: @url, code: code, headers: headers, json: JSON.parse(json, symbolize_names: @symbolize_names) }
115 | rescue StandardError
116 | { url: @url, code: code, headers: headers, json: nil }
117 | end
118 | end
119 | end
120 | end
121 |
--------------------------------------------------------------------------------
/lib/curly/array.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Array helpers
4 | class ::Array
5 | ##
6 | ## Remove extra spaces from each element of an array of
7 | ## strings
8 | ##
9 | ## @return [Array] cleaned array
10 | ##
11 | def clean
12 | map(&:clean)
13 | end
14 |
15 | ##
16 | ## @see #clean
17 | ##
18 | def clean!
19 | replace clean
20 | end
21 |
22 | ##
23 | ## Strip HTML tags from each element of an array of
24 | ## strings
25 | ##
26 | ## @return [Array] array of strings with HTML tags removed
27 | ##
28 | def strip_tags
29 | map(&:strip_tags)
30 | end
31 |
32 | ##
33 | ## Destructive version of #strip_tags
34 | ##
35 | ## @see #strip_tags
36 | ##
37 | def strip_tags!
38 | replace strip_tags
39 | end
40 |
41 | ##
42 | ## Remove duplicate links from an array of link objects
43 | ##
44 | ## @return [Array] deduped array of link objects
45 | ##
46 | def dedup_links
47 | used = []
48 | good = []
49 | each do |link|
50 | href = link[:href].sub(%r{/$}, '')
51 | next if used.include?(href)
52 |
53 | used.push(href)
54 | good.push(link)
55 | end
56 |
57 | good
58 | end
59 |
60 | ##
61 | ## Destructive version of #dedup_links
62 | ##
63 | ## @see #dedup_links
64 | ##
65 | def dedup_links!
66 | replace dedup_links
67 | end
68 |
69 | ##
70 | ## Run a query on array elements
71 | ##
72 | ## @param path [String] dot.syntax path to compare
73 | ##
74 | ## @return [Array] elements matching dot query
75 | ##
76 | def dot_query(path)
77 | res = map { |el| el.dot_query(path) }
78 | res.delete_if { |r| !r }
79 | res.delete_if(&:empty?)
80 | res
81 | end
82 |
83 | ##
84 | ## Gets the value of every item in the array
85 | ##
86 | ## @param path The query path (dot syntax)
87 | ##
88 | ## @return [Array] array of values
89 | ##
90 | def get_value(path)
91 | map { |el| el.get_value(path) }
92 | end
93 |
94 | ##
95 | ## Convert every item in the array to HTML
96 | ##
97 | ## @return [String] Html representation of the object.
98 | ##
99 | def to_html
100 | map(&:to_html)
101 | end
102 |
103 | ##
104 | ## Test if a tag contains an attribute matching filter
105 | ## queries
106 | ##
107 | ## @param tag_name [String] The tag name
108 | ## @param classes [String] The classes to match
109 | ## @param id [String] The id attribute to
110 | ## match
111 | ## @param attribute [String] The attribute
112 | ## @param operator [String] The operator, <>= *=
113 | ## $= ^=
114 | ## @param value [String] The value to match
115 | ## @param descendant [Boolean] Check descendant tags
116 | ##
117 | ## @return [Boolean] tag matches
118 | ##
119 | def tag_match(tag_name, classes, id, attribute, operator, value, descendant: false)
120 | tag = self
121 | keep = true
122 |
123 | keep = false if tag_name && !tag['tag'] =~ /^#{tag_name}$/i
124 |
125 | if tag.key?('attrs') && tag['attrs']
126 | if keep && id
127 | tag_id = tag['attrs'].filter { |a| a['key'] == 'id' }.first['value']
128 | keep = tag_id && tag_id =~ /#{id}/i
129 | end
130 |
131 | if keep && classes
132 | cls = tag['attrs'].filter { |a| a['key'] == 'class' }.first
133 | if cls
134 | all = true
135 | classes.each { |c| all = cls['value'].include?(c) }
136 | keep = all
137 | else
138 | keep = false
139 | end
140 | end
141 |
142 | if keep && attribute
143 | attributes = tag['attrs'].filter { |a| a['key'] =~ /^#{attribute}$/i }
144 | any = false
145 | attributes.each do |a|
146 | break if any
147 |
148 | any = case operator
149 | when /^*/
150 | a['value'] =~ /#{value}/i
151 | when /^\^/
152 | a['value'] =~ /^#{value}/i
153 | when /^\$/
154 | a['value'] =~ /#{value}$/i
155 | else
156 | a['value'] =~ /^#{value}$/i
157 | end
158 | end
159 | keep = any
160 | end
161 | end
162 |
163 | return false if descendant && !keep
164 |
165 | if !descendant && tag.key?('tags')
166 | tags = tag['tags'].filter { |t| t.tag_match(tag_name, classes, id, attribute, operator, value) }
167 | tags.count.positive?
168 | else
169 | keep
170 | end
171 | end
172 |
173 | ##
174 | ## Clean up output, shrink single-item arrays, ensure array output
175 | ##
176 | ## @return [Array] cleaned up array
177 | ##
178 | def clean_output
179 | output = dup
180 | while output.is_a?(Array) && output.count == 1
181 | output = output[0]
182 | end
183 | return [] unless output
184 |
185 | output.ensure_array
186 | end
187 |
188 | ##
189 | ## Ensure that an object is an array
190 | ##
191 | ## @return [Array] object as Array
192 | ##
193 | def ensure_array
194 | return self
195 | end
196 | end
197 |
--------------------------------------------------------------------------------
/test/helpers/threaded_tests.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'tty-spinner'
4 | require 'tty-progressbar'
5 | require 'open3'
6 | require 'shellwords'
7 | require 'fileutils'
8 | require 'pastel'
9 |
10 | class ThreadedTests
11 | def run(pattern: '*', max_threads: 8, max_tests: 0)
12 | pastel = Pastel.new
13 |
14 | start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
15 | @results = File.expand_path('results.log')
16 |
17 | max_threads = 1000 if max_threads.to_i == 0
18 |
19 | shuffle = false
20 |
21 | unless pattern =~ /shuffle/i
22 | pattern = "test/curlyq_*#{pattern}*_test.rb"
23 | else
24 | pattern = "test/curlyq_*_test.rb"
25 | shuffle = true
26 | end
27 |
28 | tests = Dir.glob(pattern)
29 |
30 | tests.shuffle! if shuffle
31 |
32 | if max_tests.to_i > 0
33 | tests = tests.slice(0, max_tests.to_i - 1)
34 | end
35 |
36 | puts pastel.cyan("#{tests.count} test files")
37 |
38 | banner = "Running tests [:bar] T/A (#{max_threads.to_s} threads)"
39 |
40 | progress = TTY::ProgressBar::Multi.new(banner,
41 | width: 12,
42 | clear: true,
43 | hide_cursor: true)
44 | @children = []
45 | tests.each do |t|
46 | test_name = File.basename(t, '.rb').sub(/curlyq_(.*?)_test/, '\1')
47 | new_sp = progress.register("[:bar] #{test_name}:status",
48 | total: tests.count + 8,
49 | width: 1,
50 | head: ' ',
51 | unknown: ' ',
52 | hide_cursor: true,
53 | clear: true)
54 | status = ': waiting'
55 | @children.push([test_name, new_sp, status])
56 | end
57 |
58 | @elapsed = 0.0
59 | @test_total = 0
60 | @assrt_total = 0
61 | @error_out = []
62 | @threads = []
63 | @running_tests = []
64 |
65 | begin
66 | finish_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
67 | while @children.count.positive?
68 |
69 | slices = @children.slice!(0, max_threads)
70 | slices.each { |c| c[1].start }
71 | slices.each do |s|
72 | @threads << Thread.new do
73 | run_test(s)
74 | finish_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
75 | end
76 | end
77 |
78 | @threads.each { |t| t.join }
79 | end
80 |
81 | finish_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
82 |
83 | progress.finish
84 | rescue
85 | progress.stop
86 | ensure
87 | msg = @running_tests.map { |t| t[1].format.sub(/^\[:bar\] (.*?):status/, "\\1#{t[2]}") }.join("\n")
88 |
89 | output = []
90 | output << if @error_out.count.positive?
91 | pastel.red("#{@error_out.count} Issues")
92 | else
93 | pastel.green('Success')
94 | end
95 | output << pastel.green("#{@test_total} tests")
96 | output << pastel.cyan("#{@assrt_total} assertions")
97 | output << pastel.yellow("#{(finish_time - start_time).round(3)}s")
98 | puts output.join(', ')
99 |
100 | if @error_out.count.positive?
101 | puts @error_out.join(pastel.white("\n----\n"))
102 | Process.exit 1
103 | end
104 | end
105 | end
106 |
107 | def run_test(s)
108 | pastel = Pastel.new
109 |
110 | bar = s[1]
111 | s[2] = ": #{pastel.green('running')}"
112 | bar.advance(status: s[2])
113 |
114 | if @running_tests.count.positive?
115 | @running_tests.each do |b|
116 | prev_bar = b[1]
117 | if prev_bar.complete?
118 | prev_bar.reset
119 | prev_bar.advance(status: b[2])
120 | prev_bar.finish
121 | else
122 | prev_bar.update(head: ' ', unfinished: ' ')
123 | prev_bar.advance(status: b[2])
124 | end
125 | end
126 | end
127 |
128 | @running_tests.push(s)
129 | out, _err, status = Open3.capture3(ENV, 'rake', "test:#{s[0]}", stdin_data: nil)
130 | time = out.match(/^Finished in (?