checkset 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +428 -0
- data/Rakefile +11 -0
- data/checks/example.rb +19 -0
- data/checkset.gemspec +34 -0
- data/examples/checks/basecamp/homepage.rb +24 -0
- data/examples/checks/hey/homepage.rb +24 -0
- data/examples/checks/hey/pricing.rb +24 -0
- data/examples/checks/homepage_loads.rb +23 -0
- data/examples/checks/once/homepage.rb +24 -0
- data/examples/checks/ruby/ruby_lang.rb +28 -0
- data/examples/checkset.yml +23 -0
- data/exe/checkset +5 -0
- data/lib/checkset/browser_manager.rb +83 -0
- data/lib/checkset/check.rb +129 -0
- data/lib/checkset/cli.rb +247 -0
- data/lib/checkset/config_file.rb +51 -0
- data/lib/checkset/configuration.rb +41 -0
- data/lib/checkset/credentials.rb +34 -0
- data/lib/checkset/init.rb +70 -0
- data/lib/checkset/prep.rb +38 -0
- data/lib/checkset/reporter/cli.rb +118 -0
- data/lib/checkset/reporter/json.rb +41 -0
- data/lib/checkset/result.rb +86 -0
- data/lib/checkset/runner.rb +245 -0
- data/lib/checkset/step_result.rb +73 -0
- data/lib/checkset/suite.rb +12 -0
- data/lib/checkset/version.rb +5 -0
- data/lib/checkset.rb +46 -0
- metadata +89 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Checkset
|
|
4
|
+
class Prep
|
|
5
|
+
attr_reader :credentials, :target_url
|
|
6
|
+
|
|
7
|
+
def initialize(credentials:, target_url:)
|
|
8
|
+
@credentials = credentials
|
|
9
|
+
@target_url = target_url
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Override in subclass — perform the prep action.
|
|
13
|
+
# Receives the same Playwright page the check will use,
|
|
14
|
+
# so browser state (cookies, localStorage) persists.
|
|
15
|
+
def satisfy(page)
|
|
16
|
+
raise NotImplementedError, "#{self.class}#satisfy must be implemented"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Override in subclass — optional optimization.
|
|
20
|
+
# Return true to skip satisfy (e.g., already signed in).
|
|
21
|
+
def satisfied?(page)
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Resolve a prep name to its class.
|
|
26
|
+
# :sign_in_as_admin → Preps::SignInAsAdmin
|
|
27
|
+
def self.resolve(name)
|
|
28
|
+
return name if name.is_a?(Class) && name < Checkset::Prep
|
|
29
|
+
|
|
30
|
+
const_name = name.to_s.split("_").map(&:capitalize).join
|
|
31
|
+
if defined?(Preps) && Preps.const_defined?(const_name)
|
|
32
|
+
Preps.const_get(const_name)
|
|
33
|
+
else
|
|
34
|
+
raise NameError, "Could not resolve prep :#{name} — expected class Preps::#{const_name}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Checkset
|
|
4
|
+
module Reporter
|
|
5
|
+
class CLI
|
|
6
|
+
COLORS = {
|
|
7
|
+
green: "\e[32m",
|
|
8
|
+
red: "\e[31m",
|
|
9
|
+
yellow: "\e[33m",
|
|
10
|
+
bold: "\e[1m",
|
|
11
|
+
dim: "\e[2m",
|
|
12
|
+
reset: "\e[0m"
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
def initialize(target_url:, suite_name: nil, io: $stdout)
|
|
16
|
+
@target_url = target_url
|
|
17
|
+
@suite_name = suite_name
|
|
18
|
+
@io = io
|
|
19
|
+
@color = io.respond_to?(:tty?) && io.tty?
|
|
20
|
+
@mutex = Mutex.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def print_header
|
|
24
|
+
puts
|
|
25
|
+
if @suite_name
|
|
26
|
+
puts "#{bold}Checkset#{reset} — #{bold}#{@suite_name}#{reset} → #{@target_url}"
|
|
27
|
+
else
|
|
28
|
+
puts "#{bold}Checkset#{reset} — #{@target_url}"
|
|
29
|
+
end
|
|
30
|
+
puts line
|
|
31
|
+
puts
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def report_result(result)
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
status = format_status(result.status)
|
|
37
|
+
name = result.check_name.to_s.split("::").last || result.check_name
|
|
38
|
+
duration = result.duration ? format("%.1fs", result.duration) : "?"
|
|
39
|
+
dots = "." * [2, 50 - name.length].max
|
|
40
|
+
|
|
41
|
+
puts " #{status} #{name} #{dim}#{dots}#{reset} #{duration}"
|
|
42
|
+
|
|
43
|
+
if result.failed?
|
|
44
|
+
result.step_results.select(&:failed?).each do |sr|
|
|
45
|
+
puts " #{red}✗#{reset} #{sr.type} #{dim}\"#{sr.name}\"#{reset} — #{sr.error}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
result.step_results.select(&:failed?).each do |sr|
|
|
49
|
+
if sr.screenshot_path
|
|
50
|
+
puts " #{dim}screenshot: #{sr.screenshot_path}#{reset}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if result.trace_path
|
|
55
|
+
puts " #{dim}trace: #{result.trace_path}#{reset}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
if result.error && result.step_results.none?(&:failed?)
|
|
59
|
+
puts " #{red}✗#{reset} #{result.error}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if result.status == :skipped
|
|
64
|
+
puts " #{yellow}⊘#{reset} #{result.error || "skipped"}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def print_summary(results)
|
|
70
|
+
puts
|
|
71
|
+
puts line
|
|
72
|
+
|
|
73
|
+
passed = results.count(&:passed?)
|
|
74
|
+
failed = results.count(&:failed?)
|
|
75
|
+
skipped = results.count { |r| r.status == :skipped }
|
|
76
|
+
total_duration = results.sum { |r| r.duration || 0 }
|
|
77
|
+
|
|
78
|
+
parts = []
|
|
79
|
+
parts << "#{green}#{passed} passed#{reset}" if passed > 0
|
|
80
|
+
parts << "#{red}#{failed} failed#{reset}" if failed > 0
|
|
81
|
+
parts << "#{yellow}#{skipped} skipped#{reset}" if skipped > 0
|
|
82
|
+
|
|
83
|
+
puts "#{parts.join(", ")} #{dim}(#{format("%.1fs", total_duration)})#{reset}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def print_results_path(path)
|
|
87
|
+
puts "#{dim}Results: #{path}#{reset}"
|
|
88
|
+
puts
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def puts(text = "")
|
|
94
|
+
@io.puts(text)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def line
|
|
98
|
+
"━" * 50
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def format_status(status)
|
|
102
|
+
case status
|
|
103
|
+
when :passed then "#{green}#{bold} PASS #{reset}"
|
|
104
|
+
when :failed then "#{red}#{bold} FAIL #{reset}"
|
|
105
|
+
when :skipped then "#{yellow}#{bold} SKIP #{reset}"
|
|
106
|
+
else " ?? "
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def bold = @color ? COLORS[:bold] : ""
|
|
111
|
+
def dim = @color ? COLORS[:dim] : ""
|
|
112
|
+
def green = @color ? COLORS[:green] : ""
|
|
113
|
+
def red = @color ? COLORS[:red] : ""
|
|
114
|
+
def yellow = @color ? COLORS[:yellow] : ""
|
|
115
|
+
def reset = @color ? COLORS[:reset] : ""
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Checkset
|
|
7
|
+
module Reporter
|
|
8
|
+
class JSON
|
|
9
|
+
def initialize(configuration: Checkset.configuration)
|
|
10
|
+
@configuration = configuration
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def write(results, target_url: @configuration.target_url, suite_name: nil)
|
|
14
|
+
dir = @configuration.results_dir
|
|
15
|
+
FileUtils.mkdir_p(dir)
|
|
16
|
+
|
|
17
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
|
18
|
+
prefix = suite_name ? "results_#{suite_name}" : "results"
|
|
19
|
+
path = File.join(dir, "#{prefix}_#{timestamp}.json")
|
|
20
|
+
|
|
21
|
+
total_duration = results.sum { |r| r.duration || 0 }
|
|
22
|
+
|
|
23
|
+
data = {
|
|
24
|
+
run_at: Time.now.iso8601,
|
|
25
|
+
target_url: target_url,
|
|
26
|
+
duration: total_duration.round(2),
|
|
27
|
+
summary: {
|
|
28
|
+
total: results.size,
|
|
29
|
+
passed: results.count(&:passed?),
|
|
30
|
+
failed: results.count(&:failed?),
|
|
31
|
+
skipped: results.count { |r| r.status == :skipped }
|
|
32
|
+
},
|
|
33
|
+
checks: results.map(&:to_h)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
File.write(path, ::JSON.pretty_generate(data))
|
|
37
|
+
path
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Checkset
|
|
4
|
+
class Result
|
|
5
|
+
attr_reader :check_name, :status, :started_at, :finished_at,
|
|
6
|
+
:step_results, :prep_results, :trace_path,
|
|
7
|
+
:error, :error_backtrace
|
|
8
|
+
|
|
9
|
+
def initialize(check_name:)
|
|
10
|
+
@check_name = check_name
|
|
11
|
+
@status = :pending
|
|
12
|
+
@started_at = nil
|
|
13
|
+
@finished_at = nil
|
|
14
|
+
@step_results = []
|
|
15
|
+
@prep_results = []
|
|
16
|
+
@trace_path = nil
|
|
17
|
+
@error = nil
|
|
18
|
+
@error_backtrace = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def start!
|
|
22
|
+
@status = :running
|
|
23
|
+
@started_at = Time.now
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def pass!
|
|
27
|
+
@status = :passed
|
|
28
|
+
@finished_at = Time.now
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def fail!(error: nil)
|
|
32
|
+
@status = :failed
|
|
33
|
+
@finished_at = Time.now
|
|
34
|
+
if error.is_a?(Exception)
|
|
35
|
+
@error = error.message
|
|
36
|
+
@error_backtrace = error.backtrace&.first(10)
|
|
37
|
+
elsif error
|
|
38
|
+
@error = error.to_s
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def skip!(reason: nil)
|
|
43
|
+
@status = :skipped
|
|
44
|
+
@finished_at = Time.now
|
|
45
|
+
@error = reason
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def add_step_result(step_result)
|
|
49
|
+
@step_results << step_result
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def add_prep_result(prep_result)
|
|
53
|
+
@prep_results << prep_result
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def trace_path=(path)
|
|
57
|
+
@trace_path = path
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def duration
|
|
61
|
+
return nil unless @started_at && @finished_at
|
|
62
|
+
@finished_at - @started_at
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def passed?
|
|
66
|
+
@status == :passed
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def failed?
|
|
70
|
+
@status == :failed
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_h
|
|
74
|
+
{
|
|
75
|
+
check: @check_name,
|
|
76
|
+
status: @status,
|
|
77
|
+
duration: duration,
|
|
78
|
+
preps: @prep_results.map(&:to_h),
|
|
79
|
+
steps: @step_results.map(&:to_h),
|
|
80
|
+
trace_path: @trace_path,
|
|
81
|
+
error: @error,
|
|
82
|
+
error_backtrace: @error_backtrace
|
|
83
|
+
}.compact
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Checkset
|
|
6
|
+
class Runner
|
|
7
|
+
def initialize(configuration: Checkset.configuration, filter: nil, tag: nil, target_url: nil, suite_name: nil, browser_manager: nil, preloaded: false)
|
|
8
|
+
@configuration = configuration
|
|
9
|
+
@filter = filter
|
|
10
|
+
@tag = tag
|
|
11
|
+
@target_url = target_url || @configuration.target_url
|
|
12
|
+
@suite_name = suite_name
|
|
13
|
+
@browser_manager = browser_manager || BrowserManager.new(configuration: configuration)
|
|
14
|
+
@owns_browser = browser_manager.nil?
|
|
15
|
+
@preloaded = preloaded
|
|
16
|
+
@credentials = Credentials.new(provider: configuration.credentials_provider)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Preload all check and prep files without running anything.
|
|
20
|
+
# Call this before spawning concurrent suite runners.
|
|
21
|
+
def self.preload(configuration: Checkset.configuration)
|
|
22
|
+
configuration.checks_paths.each do |path|
|
|
23
|
+
Dir.glob(File.join(path, "**", "*.rb")).sort.each { |f| require File.expand_path(f) }
|
|
24
|
+
end
|
|
25
|
+
configuration.preps_paths.each do |path|
|
|
26
|
+
Dir.glob(File.join(path, "**", "*.rb")).sort.each { |f| require File.expand_path(f) }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def run
|
|
31
|
+
if @preloaded
|
|
32
|
+
compute_check_files
|
|
33
|
+
else
|
|
34
|
+
load_check_files
|
|
35
|
+
load_prep_files
|
|
36
|
+
end
|
|
37
|
+
check_classes = discover_checks
|
|
38
|
+
|
|
39
|
+
if check_classes.empty?
|
|
40
|
+
Checkset.logger.warn("No checks found in #{@configuration.checks_paths.inspect}")
|
|
41
|
+
return []
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
@browser_manager.start! if @owns_browser
|
|
45
|
+
|
|
46
|
+
results = if @configuration.parallel > 1
|
|
47
|
+
run_parallel(check_classes, pool_size: @configuration.parallel)
|
|
48
|
+
else
|
|
49
|
+
check_classes.map { |klass| run_check(klass) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
results
|
|
53
|
+
ensure
|
|
54
|
+
@browser_manager.stop! if @owns_browser
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def compute_check_files
|
|
60
|
+
@check_files = []
|
|
61
|
+
|
|
62
|
+
@configuration.checks_paths.each do |path|
|
|
63
|
+
if @suite_name
|
|
64
|
+
@check_files.concat(Dir.glob(File.join(path, "*.rb")).sort)
|
|
65
|
+
@check_files.concat(Dir.glob(File.join(path, @suite_name, "**", "*.rb")).sort)
|
|
66
|
+
else
|
|
67
|
+
@check_files.concat(Dir.glob(File.join(path, "**", "*.rb")).sort)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
@check_files.map! { |f| File.expand_path(f) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def load_check_files
|
|
75
|
+
compute_check_files
|
|
76
|
+
@check_files.each { |f| require f }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def load_prep_files
|
|
80
|
+
@configuration.preps_paths.each do |path|
|
|
81
|
+
Dir.glob(File.join(path, "**", "*.rb")).sort.each { |f| require File.expand_path(f) }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def discover_checks
|
|
86
|
+
classes = ObjectSpace.each_object(Class).select { |klass| klass < Checkset::Check }
|
|
87
|
+
|
|
88
|
+
if @check_files
|
|
89
|
+
classes.select! do |klass|
|
|
90
|
+
source = klass.instance_method(:call).source_location&.first
|
|
91
|
+
source && @check_files.include?(source)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if @filter
|
|
96
|
+
classes.select! { |klass| klass.name&.include?(@filter) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if @tag
|
|
100
|
+
classes.select! { |klass| klass.tags.include?(@tag) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Sort top-level checks first, then suite-specific checks, alphabetically within each group
|
|
104
|
+
top_level_files = @suite_name ? @configuration.checks_paths.flat_map { |p| Dir.glob(File.join(p, "*.rb")).map { |f| File.expand_path(f) } } : []
|
|
105
|
+
|
|
106
|
+
classes.sort_by do |klass|
|
|
107
|
+
source = klass.instance_method(:call).source_location&.first
|
|
108
|
+
top_level = top_level_files.include?(source) ? 0 : 1
|
|
109
|
+
[top_level, klass.name || ""]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def run_check(klass)
|
|
114
|
+
max_attempts = @configuration.retries + 1
|
|
115
|
+
|
|
116
|
+
max_attempts.times do |attempt|
|
|
117
|
+
result = attempt_check(klass)
|
|
118
|
+
|
|
119
|
+
# Return immediately on pass, skip, or last attempt
|
|
120
|
+
if result.passed? || result.status == :skipped || attempt == max_attempts - 1
|
|
121
|
+
return result
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
Checkset.logger.info("Retrying #{klass.name} (attempt #{attempt + 2}/#{max_attempts})")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def attempt_check(klass)
|
|
129
|
+
result = Result.new(check_name: klass.name || klass.to_s)
|
|
130
|
+
result.start!
|
|
131
|
+
|
|
132
|
+
@browser_manager.with_context do |page, context|
|
|
133
|
+
# Run preps
|
|
134
|
+
prep_ok = run_preps(klass, page, result)
|
|
135
|
+
unless prep_ok
|
|
136
|
+
result.skip!(reason: "Prep failed")
|
|
137
|
+
save_trace(context, result)
|
|
138
|
+
next
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Run the check
|
|
142
|
+
check = klass.new(
|
|
143
|
+
page: page,
|
|
144
|
+
result: result,
|
|
145
|
+
credentials: @credentials,
|
|
146
|
+
target_url: @target_url,
|
|
147
|
+
configuration: @configuration
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
begin
|
|
151
|
+
check.call
|
|
152
|
+
|
|
153
|
+
if result.step_results.any?(&:failed?)
|
|
154
|
+
result.fail!
|
|
155
|
+
save_trace(context, result)
|
|
156
|
+
else
|
|
157
|
+
result.pass!
|
|
158
|
+
discard_trace(context)
|
|
159
|
+
end
|
|
160
|
+
rescue => e
|
|
161
|
+
# Emergency screenshot
|
|
162
|
+
emergency_screenshot(page, klass)
|
|
163
|
+
result.fail!(error: e)
|
|
164
|
+
save_trace(context, result)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
result
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def run_preps(klass, page, result)
|
|
172
|
+
klass.preps.each do |prep_name|
|
|
173
|
+
step_result = StepResult.new(name: prep_name.to_s, type: :prep)
|
|
174
|
+
step_result.start!
|
|
175
|
+
|
|
176
|
+
begin
|
|
177
|
+
prep_class = Prep.resolve(prep_name)
|
|
178
|
+
prep = prep_class.new(credentials: @credentials, target_url: @target_url)
|
|
179
|
+
|
|
180
|
+
if prep.satisfied?(page)
|
|
181
|
+
step_result.pass!
|
|
182
|
+
else
|
|
183
|
+
prep.satisfy(page)
|
|
184
|
+
step_result.pass!
|
|
185
|
+
end
|
|
186
|
+
rescue => e
|
|
187
|
+
step_result.fail!(error: e)
|
|
188
|
+
result.add_prep_result(step_result)
|
|
189
|
+
return false
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
result.add_prep_result(step_result)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
true
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def run_parallel(classes, pool_size:)
|
|
199
|
+
results = []
|
|
200
|
+
mutex = Mutex.new
|
|
201
|
+
queue = Queue.new
|
|
202
|
+
classes.each { |klass| queue << klass }
|
|
203
|
+
|
|
204
|
+
workers = pool_size.times.map do
|
|
205
|
+
Thread.new do
|
|
206
|
+
while (klass = begin; queue.pop(true); rescue ThreadError; nil; end)
|
|
207
|
+
r = run_check(klass)
|
|
208
|
+
mutex.synchronize { results << r }
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
workers.each(&:join)
|
|
214
|
+
results
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def save_trace(context, result)
|
|
218
|
+
check_name = result.check_name.to_s.gsub("::", "_").downcase
|
|
219
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
|
220
|
+
dir = @configuration.traces_dir
|
|
221
|
+
FileUtils.mkdir_p(dir)
|
|
222
|
+
path = File.join(dir, "#{check_name}_#{timestamp}.zip")
|
|
223
|
+
context.tracing.stop(path: path)
|
|
224
|
+
result.trace_path = path
|
|
225
|
+
rescue => e
|
|
226
|
+
Checkset.logger.warn("Failed to save trace: #{e.message}")
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def discard_trace(context)
|
|
230
|
+
context.tracing.stop
|
|
231
|
+
rescue => e
|
|
232
|
+
Checkset.logger.warn("Failed to discard trace: #{e.message}")
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def emergency_screenshot(page, klass)
|
|
236
|
+
check_name = (klass.name || "anonymous").gsub("::", "_").downcase
|
|
237
|
+
dir = File.join(@configuration.screenshots_dir, check_name)
|
|
238
|
+
FileUtils.mkdir_p(dir)
|
|
239
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S_%L")
|
|
240
|
+
page.screenshot(path: File.join(dir, "EMERGENCY_#{timestamp}.png"), fullPage: true)
|
|
241
|
+
rescue => e
|
|
242
|
+
Checkset.logger.warn("Emergency screenshot failed: #{e.message}")
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Checkset
|
|
4
|
+
class StepResult
|
|
5
|
+
attr_reader :name, :type, :status, :started_at, :finished_at,
|
|
6
|
+
:screenshot_path, :error, :error_backtrace
|
|
7
|
+
|
|
8
|
+
def initialize(name:, type:)
|
|
9
|
+
@name = name
|
|
10
|
+
@type = type # :verify, :step, :prep
|
|
11
|
+
@status = :pending
|
|
12
|
+
@started_at = nil
|
|
13
|
+
@finished_at = nil
|
|
14
|
+
@screenshot_path = nil
|
|
15
|
+
@error = nil
|
|
16
|
+
@error_backtrace = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def start!
|
|
20
|
+
@status = :running
|
|
21
|
+
@started_at = Time.now
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pass!(screenshot_path: nil)
|
|
25
|
+
@status = :passed
|
|
26
|
+
@finished_at = Time.now
|
|
27
|
+
@screenshot_path = screenshot_path
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def fail!(error:, screenshot_path: nil)
|
|
31
|
+
@status = :failed
|
|
32
|
+
@finished_at = Time.now
|
|
33
|
+
@screenshot_path = screenshot_path
|
|
34
|
+
if error.is_a?(Exception)
|
|
35
|
+
@error = error.message
|
|
36
|
+
@error_backtrace = error.backtrace&.first(10)
|
|
37
|
+
else
|
|
38
|
+
@error = error.to_s
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def skip!(reason: nil)
|
|
43
|
+
@status = :skipped
|
|
44
|
+
@finished_at = Time.now
|
|
45
|
+
@error = reason
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def duration
|
|
49
|
+
return nil unless @started_at && @finished_at
|
|
50
|
+
@finished_at - @started_at
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def passed?
|
|
54
|
+
@status == :passed
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def failed?
|
|
58
|
+
@status == :failed
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_h
|
|
62
|
+
{
|
|
63
|
+
name: @name,
|
|
64
|
+
type: @type,
|
|
65
|
+
status: @status,
|
|
66
|
+
duration: duration,
|
|
67
|
+
screenshot_path: @screenshot_path,
|
|
68
|
+
error: @error,
|
|
69
|
+
error_backtrace: @error_backtrace
|
|
70
|
+
}.compact
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/checkset.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
require_relative "checkset/version"
|
|
6
|
+
require_relative "checkset/configuration"
|
|
7
|
+
require_relative "checkset/credentials"
|
|
8
|
+
require_relative "checkset/step_result"
|
|
9
|
+
require_relative "checkset/result"
|
|
10
|
+
require_relative "checkset/browser_manager"
|
|
11
|
+
require_relative "checkset/prep"
|
|
12
|
+
require_relative "checkset/check"
|
|
13
|
+
require_relative "checkset/suite"
|
|
14
|
+
require_relative "checkset/config_file"
|
|
15
|
+
require_relative "checkset/runner"
|
|
16
|
+
require_relative "checkset/reporter/cli"
|
|
17
|
+
require_relative "checkset/reporter/json"
|
|
18
|
+
require_relative "checkset/init"
|
|
19
|
+
require_relative "checkset/cli"
|
|
20
|
+
|
|
21
|
+
module Checkset
|
|
22
|
+
class Error < StandardError; end
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
def configuration
|
|
26
|
+
@configuration ||= Configuration.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def configure
|
|
30
|
+
yield(configuration)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def logger
|
|
34
|
+
@logger ||= Logger.new($stdout, level: Logger::INFO)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def logger=(logger)
|
|
38
|
+
@logger = logger
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def reset!
|
|
42
|
+
@configuration = Configuration.new
|
|
43
|
+
@logger = nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|