cyrun 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/.standard.yml +3 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +31 -0
- data/Rakefile +14 -0
- data/cyrun.gemspec +31 -0
- data/exe/cyrun +77 -0
- data/lib/cyrun/group_builder.rb +25 -0
- data/lib/cyrun/logger.rb +24 -0
- data/lib/cyrun/result_directory.rb +73 -0
- data/lib/cyrun/runner.rb +143 -0
- data/lib/cyrun/single_spec_execution_result.rb +14 -0
- data/lib/cyrun/spec_collector.rb +24 -0
- data/lib/cyrun/spec_group_execution_result.rb +56 -0
- data/lib/cyrun/spec_task.rb +16 -0
- data/lib/cyrun/task_runner.rb +86 -0
- data/lib/cyrun/timing.rb +14 -0
- data/lib/cyrun/version.rb +5 -0
- data/lib/cyrun.rb +16 -0
- data/rbs_collection.yaml +15 -0
- data/sig/cyrun/logging.rbs +6 -0
- data/sig/cyrun.rbs +4 -0
- data/sig/group_builder.rbs +12 -0
- data/sig/logger.rbs +6 -0
- data/sig/result_directory.rbs +23 -0
- data/sig/runner.rbs +34 -0
- data/sig/single_spec_execution_result.rbs +10 -0
- data/sig/spec_collector.rbs +18 -0
- data/sig/spec_group_execution_result.rbs +18 -0
- data/sig/spec_task.rbs +10 -0
- data/sig/task_runner.rbs +22 -0
- data/sig/timing.rbs +5 -0
- metadata +79 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f36d84fb3bdf5d10aa072b188d2777c968004eadd8a6f9fcaf99369c03f1f08f
|
4
|
+
data.tar.gz: 24d85427355c870c69c0ad77e2d058a7ae5b8f5e1f5b21214efb87cb7ba12e60
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a18ca4b67327cbe269aa455fb5fe2425874142cbcbe05b6e303856a1dd94160c3faa91d5314aace4558482d308e2c349505aa8f787549f5bbdab021827bdf794
|
7
|
+
data.tar.gz: baf623d30f79bea0a540115b7f184593e9d703aea7ee85ade631d01b7923518c5b75f046979c0afa7f972aa89742e1bfe94d7a540de1fd9e0671a6f9a46c5b31
|
data/.standard.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source 'https://rubygems.org'
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in cyrun.gemspec
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
gem 'rake', '~> 13.0'
|
9
|
+
|
10
|
+
gem 'minitest', '~> 5.0'
|
11
|
+
|
12
|
+
gem 'standard', '~> 1.3'
|
13
|
+
|
14
|
+
|
15
|
+
gem 'nokogiri'
|
16
|
+
|
17
|
+
gem 'colorize'
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 Oskar Kirmis
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Cyrun
|
2
|
+
|
3
|
+
cyrun is a command line utility to run and automatically retry failed cypress specs.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Install the gem and add to the application's Gemfile by executing:
|
8
|
+
|
9
|
+
$ bundle add cyrun
|
10
|
+
|
11
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
12
|
+
|
13
|
+
$ gem install cyrun
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
cyrun uses npm to run the specs. It will call:
|
18
|
+
|
19
|
+
```shell
|
20
|
+
npm run <task-name-you-specify> -- --specs <list-of-specs-to-run> --config <screenshot-configuration> --headless --reporter junit --reporter-options <options>
|
21
|
+
```
|
22
|
+
|
23
|
+
So for example, cyrun could be used as follows:
|
24
|
+
|
25
|
+
```shell
|
26
|
+
cyrun --task npm-task-for-cypress --working-directory --pattern "apps/e2e/**/*.spec.ts"
|
27
|
+
```
|
28
|
+
|
29
|
+
## License
|
30
|
+
|
31
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.libs << "lib"
|
9
|
+
t.test_files = FileList["test/**/test_*.rb"]
|
10
|
+
end
|
11
|
+
|
12
|
+
require "standard/rake"
|
13
|
+
|
14
|
+
task default: %i[test standard]
|
data/cyrun.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/cyrun/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'cyrun'
|
7
|
+
spec.version = Cyrun::VERSION
|
8
|
+
spec.authors = ['Oskar Kirmis']
|
9
|
+
spec.email = ['oskar.kirmis@posteo.de']
|
10
|
+
|
11
|
+
spec.summary = 'Run and retry cypress specs'
|
12
|
+
spec.description = 'Run cypress specs in batches and re-run specs or test them for stability.'
|
13
|
+
spec.homepage = 'https://git.iftrue.de/okirmis/cyrun'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.required_ruby_version = '>= 2.6.0'
|
16
|
+
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
18
|
+
spec.metadata['source_code_uri'] = 'https://git.iftrue.de/okirmis/cyrun'
|
19
|
+
spec.metadata['changelog_uri'] = 'https://git.iftrue.de/okirmis/cyrun'
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(__dir__) do
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
(File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ spec/ features/ .git .circleci appveyor])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
spec.bindir = 'exe'
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ['lib']
|
31
|
+
end
|
data/exe/cyrun
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'cyrun'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
npm_task_name = 'e2e'
|
8
|
+
pattern = '*/**/*.cy.{ts,js}'
|
9
|
+
working_directory = Dir.pwd
|
10
|
+
results_directory = "#{Dir.pwd}/results"
|
11
|
+
group_size = 10000
|
12
|
+
max_retries = nil
|
13
|
+
iterations = nil
|
14
|
+
screenshots_directory = "#{Dir.pwd}/screenshots"
|
15
|
+
screenshots_pattern = '*/**/screenshots/**/*.png'
|
16
|
+
|
17
|
+
OptionParser.new do |opts|
|
18
|
+
opts.banner = 'Usage: cyrun [options]'
|
19
|
+
|
20
|
+
opts.on('-t', '--task TASK', String, 'NPM task to execute') do |t|
|
21
|
+
npm_task_name = t
|
22
|
+
end
|
23
|
+
|
24
|
+
opts.on('-p', '--pattern PATTERN', String, 'File path pattern') do |p|
|
25
|
+
pattern = p
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on('-w', '--working-directory DIRECTORY', String, 'Working directory') do |w|
|
29
|
+
working_directory = w
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.on('-r', '--retries RETRIES', Integer, 'Maximum number of retries') do |r|
|
33
|
+
max_retries = r
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on('-g', '--group-size GROUPSIZE', Integer, 'Number of specs to process in a batch') do |g|
|
37
|
+
group_size = g
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on('-i', '--stable-runs ITERATIONS', Integer, 'Number of times the spec is run (an has to pass)') do |i|
|
41
|
+
iterations = i
|
42
|
+
end
|
43
|
+
|
44
|
+
opts.on('-d', '--results-directory DIRECTORY', String, 'Directory where to store the result files') do |d|
|
45
|
+
results_directory = d
|
46
|
+
end
|
47
|
+
|
48
|
+
opts.on('-s', '--screenshots-directory DIRECTORY', String, 'Directory where to store the screenshot files') do |d|
|
49
|
+
screenshots_directory = d
|
50
|
+
end
|
51
|
+
|
52
|
+
opts.on('--screenshots-pattern PATTERN', String, 'Glob pattern where to look for screenshots') do |p|
|
53
|
+
screenshots_pattern = p
|
54
|
+
end
|
55
|
+
end.parse!
|
56
|
+
|
57
|
+
if !iterations.nil? && !max_retries.nil?
|
58
|
+
Cyrun::Logger.error 'Only --iterations OR --retries can be specified.'
|
59
|
+
Kernel.exit!(-1)
|
60
|
+
end
|
61
|
+
|
62
|
+
max_retries = 1 if iterations.nil? && max_retries.nil?
|
63
|
+
|
64
|
+
base_directory = File.expand_path(working_directory)
|
65
|
+
Dir.chdir base_directory
|
66
|
+
|
67
|
+
runner = Cyrun::Runner.new(
|
68
|
+
base_directory,
|
69
|
+
pattern,
|
70
|
+
screenshots_pattern,
|
71
|
+
group_size,
|
72
|
+
npm_task_name,
|
73
|
+
max_retries: max_retries,
|
74
|
+
iterations: iterations
|
75
|
+
)
|
76
|
+
runner.run
|
77
|
+
runner.store_results(results_directory, screenshots_directory)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrun
|
4
|
+
class GroupBuilder
|
5
|
+
def initialize(group_size)
|
6
|
+
@queue = []
|
7
|
+
@group_size = group_size
|
8
|
+
end
|
9
|
+
|
10
|
+
# @param [SpecTask] task
|
11
|
+
def add_task(task)
|
12
|
+
@queue.push task
|
13
|
+
end
|
14
|
+
|
15
|
+
def next_group
|
16
|
+
group = []
|
17
|
+
group.push @queue.shift while @queue.any? && group.size < @group_size
|
18
|
+
group
|
19
|
+
end
|
20
|
+
|
21
|
+
def any?
|
22
|
+
@queue.any?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/cyrun/logger.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'colorize'
|
3
|
+
|
4
|
+
module Cyrun
|
5
|
+
class Logger
|
6
|
+
def self.error(*message)
|
7
|
+
puts "[ERROR] #{message.map(&:to_s).join}".red
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.info(*message)
|
11
|
+
puts "[INFO] #{message.map(&:to_s).join}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module Logging
|
16
|
+
def info(*args)
|
17
|
+
Logger.info(*args)
|
18
|
+
end
|
19
|
+
|
20
|
+
def error(*args)
|
21
|
+
Logger.error(*args)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
module Cyrun
|
6
|
+
class ResultDirectory
|
7
|
+
attr_reader :temp_directory, :screenshots_pattern, :spec_tasks
|
8
|
+
|
9
|
+
def initialize(temp_directory, screenshots_pattern, spec_tasks)
|
10
|
+
@temp_directory = temp_directory
|
11
|
+
@spec_tasks = spec_tasks
|
12
|
+
@screenshots_pattern = screenshots_pattern
|
13
|
+
end
|
14
|
+
|
15
|
+
def read_result_files
|
16
|
+
absolute_filenames = Dir
|
17
|
+
.glob('results-*.xml', base: temp_directory)
|
18
|
+
.map { |filename| File.expand_path(filename, temp_directory) }
|
19
|
+
|
20
|
+
results = absolute_filenames
|
21
|
+
.map { |filename| read_result_file filename }
|
22
|
+
.reject(&:nil?)
|
23
|
+
|
24
|
+
spec_tasks.map do |task|
|
25
|
+
find_result_for_task(results, task) || create_failed_spec_result(task)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def screenshots
|
30
|
+
Dir
|
31
|
+
.glob(screenshots_pattern, base: Dir.pwd)
|
32
|
+
.map { |filename| File.expand_path(filename, Dir.pwd) }
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def read_result_file(absolute_path)
|
38
|
+
document = Nokogiri::XML(File.read(absolute_path))
|
39
|
+
|
40
|
+
failure_count = extract_failures_from_document document
|
41
|
+
time_in_seconds = extract_time_in_seconds_from_document document
|
42
|
+
filename = extract_filename_from_document document
|
43
|
+
|
44
|
+
spec = spec_tasks.detect { |task| task.filename.end_with?(File.basename(filename)) }
|
45
|
+
return nil if spec.nil?
|
46
|
+
|
47
|
+
SingleSpecExecutionResult.new(spec, absolute_path, time_in_seconds, failure_count.zero?)
|
48
|
+
end
|
49
|
+
|
50
|
+
def extract_failures_from_document(document)
|
51
|
+
document.css('testsuites').first.attr('failures').to_i
|
52
|
+
end
|
53
|
+
|
54
|
+
def extract_time_in_seconds_from_document(document)
|
55
|
+
document.css('testsuites').first.attr('time').to_f.round
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param [Nokogiri::XML::Document] document
|
59
|
+
def extract_filename_from_document(document)
|
60
|
+
document.css('testsuite').first&.attr('file').to_s
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def create_failed_spec_result(task)
|
66
|
+
SingleSpecExecutionResult.new(task, nil, 0.0, false)
|
67
|
+
end
|
68
|
+
|
69
|
+
def find_result_for_task(results, task)
|
70
|
+
results.detect { |result| result.task == task }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/cyrun/runner.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Cyrun
|
6
|
+
class Runner
|
7
|
+
include Logging
|
8
|
+
|
9
|
+
attr_reader :collector, :group_builder, :task_runner,
|
10
|
+
:overall_results, :max_retries, :iterations
|
11
|
+
|
12
|
+
def initialize(base_directory, pattern, screenshots_pattern, group_size, npm_task_name, max_retries: nil, iterations: nil)
|
13
|
+
@collector = SpecCollector.new(base_directory, npm_task_name, [pattern])
|
14
|
+
@group_builder = GroupBuilder.new(group_size)
|
15
|
+
@task_runner = TaskRunner.new(screenshots_pattern)
|
16
|
+
|
17
|
+
@overall_results = SpecGroupExecutionResult.create_empty
|
18
|
+
@max_retries = max_retries
|
19
|
+
@iterations = iterations
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
collect_specs
|
24
|
+
run_next_group while group_builder.any?
|
25
|
+
|
26
|
+
overall_results
|
27
|
+
end
|
28
|
+
|
29
|
+
def store_results(results_directory, screenshots_directory)
|
30
|
+
latest_results = overall_results.latest_results
|
31
|
+
invalid_results = latest_results.select { |result| result.result_filename.nil? }
|
32
|
+
valid_results = latest_results - invalid_results
|
33
|
+
|
34
|
+
copy_result_files valid_results, results_directory
|
35
|
+
copy_screenshots overall_results, screenshots_directory
|
36
|
+
|
37
|
+
invalid_results.empty?
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def copy_result_files(results, directory)
|
43
|
+
FileUtils.mkdir_p directory
|
44
|
+
|
45
|
+
success = true
|
46
|
+
|
47
|
+
info "Copying #{results.size} result files"
|
48
|
+
results.each do |result|
|
49
|
+
FileUtils.cp(result.result_filename, directory)
|
50
|
+
rescue StandardError
|
51
|
+
error "Failed to copy results for #{result.task.filename}"
|
52
|
+
success = false
|
53
|
+
end
|
54
|
+
|
55
|
+
success
|
56
|
+
end
|
57
|
+
|
58
|
+
def copy_screenshots(results, directory)
|
59
|
+
FileUtils.mkdir_p directory
|
60
|
+
|
61
|
+
success = true
|
62
|
+
|
63
|
+
info "Copying #{results.screenshots.size} screenshots"
|
64
|
+
results.screenshots.each do |path|
|
65
|
+
FileUtils.cp(path, "#{directory}/#{SecureRandom.uuid}-#{File.basename(path)}")
|
66
|
+
rescue StandardError
|
67
|
+
error "Failed to copy screenshot #{path}"
|
68
|
+
success = false
|
69
|
+
end
|
70
|
+
|
71
|
+
success
|
72
|
+
end
|
73
|
+
|
74
|
+
def collect_specs
|
75
|
+
info 'Collecting specs ...'
|
76
|
+
collector.files.each do |filename|
|
77
|
+
info filename
|
78
|
+
|
79
|
+
group_builder.add_task(
|
80
|
+
collector.create_task(filename)
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
info "Collected #{collector.files.size} specs"
|
85
|
+
end
|
86
|
+
|
87
|
+
def run_next_group
|
88
|
+
group = group_builder.next_group
|
89
|
+
info("Running #{group.size} specs")
|
90
|
+
results = task_runner.run(group)
|
91
|
+
info('Group completed with the following results:')
|
92
|
+
|
93
|
+
overall_results.merge!(results)
|
94
|
+
|
95
|
+
results
|
96
|
+
.single_results
|
97
|
+
.each(&method(:handle_single_execution_result))
|
98
|
+
|
99
|
+
log_results overall_results
|
100
|
+
end
|
101
|
+
|
102
|
+
def handle_single_execution_result(result)
|
103
|
+
log_spec_result result
|
104
|
+
requeue_on_for_retries(result, overall_results, max_retries, group_builder) unless max_retries.nil?
|
105
|
+
requeue_for_iteration(result, overall_results, iterations, group_builder) unless iterations.nil?
|
106
|
+
end
|
107
|
+
|
108
|
+
def requeue_on_for_retries(result, overall_results, max_retries, group_builder)
|
109
|
+
return if result.success
|
110
|
+
|
111
|
+
if overall_results.task_fail_count(result.task) > max_retries
|
112
|
+
Cyrun::Logger.info(" -> Finally failed #{result.task.name}")
|
113
|
+
else
|
114
|
+
Cyrun::Logger.info(" -> Re-queuing #{result.task.name}")
|
115
|
+
group_builder.add_task result.task
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def requeue_for_iteration(result, overall_results, iterations, group_builder)
|
120
|
+
if result.success
|
121
|
+
group_builder.add_task result.task if overall_results.task_result_count(result.task) < iterations
|
122
|
+
else
|
123
|
+
Cyrun::Logger.info(" -> Not re-queuing #{result.task.name}")
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def log_results(overall_results)
|
128
|
+
info(
|
129
|
+
"Execution time: #{overall_results.gross_time_in_seconds} seconds, ",
|
130
|
+
"Spec time: #{overall_results.net_time_in_seconds} seconds, ",
|
131
|
+
"Effectiveness: #{(overall_results.effectiveness * 100).round} %"
|
132
|
+
)
|
133
|
+
end
|
134
|
+
|
135
|
+
def log_spec_result(result)
|
136
|
+
if result.success
|
137
|
+
info(" - Spec #{result.task.name} completed (#{result.net_time_in_seconds} seconds)")
|
138
|
+
else
|
139
|
+
info(" - Spec #{result.task.name} failed (#{result.net_time_in_seconds} seconds)")
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrun
|
4
|
+
class SingleSpecExecutionResult
|
5
|
+
attr_reader :task, :net_time_in_seconds, :success, :result_filename
|
6
|
+
|
7
|
+
def initialize(task, result_filename, net_time_in_seconds, success)
|
8
|
+
@task = task
|
9
|
+
@net_time_in_seconds = net_time_in_seconds
|
10
|
+
@success = success
|
11
|
+
@result_filename = result_filename
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrun
|
4
|
+
class SpecCollector
|
5
|
+
attr_reader :patterns, :base_directory, :npm_task_name
|
6
|
+
|
7
|
+
def initialize(base_directory, npm_task_name, patterns)
|
8
|
+
@patterns = patterns
|
9
|
+
@npm_task_name = npm_task_name
|
10
|
+
@base_directory = base_directory
|
11
|
+
end
|
12
|
+
|
13
|
+
def files
|
14
|
+
@files ||= patterns
|
15
|
+
.map { |pattern| Dir.glob(pattern, base: base_directory) }
|
16
|
+
.flatten
|
17
|
+
.map { |filename| File.expand_path(filename, base_directory) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_task(filename)
|
21
|
+
SpecTask.new(filename, @npm_task_name)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrun
|
4
|
+
class SpecGroupExecutionResult
|
5
|
+
attr_reader :single_results, :screenshots, :gross_time_in_seconds
|
6
|
+
|
7
|
+
def self.create_empty
|
8
|
+
SpecGroupExecutionResult.new([], [], 0)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(single_results, screenshots, gross_time_in_seconds)
|
12
|
+
@single_results = single_results
|
13
|
+
@gross_time_in_seconds = gross_time_in_seconds
|
14
|
+
@screenshots = screenshots
|
15
|
+
end
|
16
|
+
|
17
|
+
def net_time_in_seconds
|
18
|
+
single_results.map(&:net_time_in_seconds).sum
|
19
|
+
end
|
20
|
+
|
21
|
+
def effectiveness
|
22
|
+
return 0.0 if gross_time_in_seconds.zero?
|
23
|
+
|
24
|
+
net_time_in_seconds.to_f / gross_time_in_seconds
|
25
|
+
end
|
26
|
+
|
27
|
+
def merge!(other)
|
28
|
+
@single_results += other.single_results
|
29
|
+
@gross_time_in_seconds += other.gross_time_in_seconds
|
30
|
+
@screenshots += other.screenshots
|
31
|
+
end
|
32
|
+
|
33
|
+
def task_fail_count(task)
|
34
|
+
single_results
|
35
|
+
.select { |result| !result.success && result.task == task }
|
36
|
+
.size
|
37
|
+
end
|
38
|
+
|
39
|
+
def task_result_count(task)
|
40
|
+
task_results(task).size
|
41
|
+
end
|
42
|
+
|
43
|
+
def task_results(task)
|
44
|
+
single_results
|
45
|
+
.select { |result| result.task == task }
|
46
|
+
end
|
47
|
+
|
48
|
+
def latest_results
|
49
|
+
tasks = single_results.map(&:task).uniq
|
50
|
+
|
51
|
+
tasks.map do |task|
|
52
|
+
task_results(task).last
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cyrun
|
4
|
+
class SpecTask
|
5
|
+
attr_reader :filename, :npm_task_name
|
6
|
+
|
7
|
+
def initialize(filename, npm_task_name)
|
8
|
+
@filename = filename
|
9
|
+
@npm_task_name = npm_task_name
|
10
|
+
end
|
11
|
+
|
12
|
+
def name
|
13
|
+
File.basename(filename)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'English'
|
4
|
+
require 'tmpdir'
|
5
|
+
require 'securerandom'
|
6
|
+
require 'time'
|
7
|
+
|
8
|
+
module Cyrun
|
9
|
+
class TaskRunner
|
10
|
+
include Logging
|
11
|
+
|
12
|
+
CURRENT_SPEC_REGEX = /^\s*Running:\s+(\S+)\s+\([0-9]+ of [0-9]+\)\s*$/.freeze
|
13
|
+
|
14
|
+
attr_reader :screenshots_pattern
|
15
|
+
|
16
|
+
def initialize(screenshots_pattern)
|
17
|
+
@screenshots_pattern = screenshots_pattern
|
18
|
+
end
|
19
|
+
|
20
|
+
def run(group)
|
21
|
+
temp_directory = create_directory
|
22
|
+
command = build_command(group, temp_directory)
|
23
|
+
|
24
|
+
info "Command: #{command.join " "}"
|
25
|
+
|
26
|
+
execution_time = Cyrun::Timing.run do
|
27
|
+
execute_command command
|
28
|
+
end
|
29
|
+
|
30
|
+
results = ResultDirectory.new(temp_directory, screenshots_pattern, group)
|
31
|
+
SpecGroupExecutionResult.new(
|
32
|
+
results.read_result_files,
|
33
|
+
results.screenshots,
|
34
|
+
execution_time
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def create_directory
|
41
|
+
Dir.mktmpdir(SecureRandom.uuid)
|
42
|
+
end
|
43
|
+
|
44
|
+
def build_command(spec_tasks, temp_directory)
|
45
|
+
[
|
46
|
+
'npm', 'run', spec_tasks.first&.npm_task_name, '--'
|
47
|
+
] +
|
48
|
+
build_spec_arguments(spec_tasks) +
|
49
|
+
build_reporter_options(temp_directory)
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_spec_arguments(spec_tasks)
|
53
|
+
['--spec', spec_tasks.map(&:filename).join(',')]
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_reporter_options(temp_directory)
|
57
|
+
['--headless', '--reporter', 'junit', '--reporter-options',
|
58
|
+
"mochaFile=#{temp_directory}/results-[hash].xml,toConsole=true"]
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def execute_command(command)
|
64
|
+
output = []
|
65
|
+
|
66
|
+
# @type [IO]
|
67
|
+
process = IO.popen(command, err: %i[child out])
|
68
|
+
output.push process_command_output_line(process.gets) until process.eof?
|
69
|
+
process.close
|
70
|
+
|
71
|
+
return if $CHILD_STATUS.success?
|
72
|
+
|
73
|
+
Logger.error('Command failed')
|
74
|
+
Logger.error(output.join("\n"))
|
75
|
+
end
|
76
|
+
|
77
|
+
def process_command_output_line(line)
|
78
|
+
line ||= ''
|
79
|
+
current_spec_parts = line.scan CURRENT_SPEC_REGEX
|
80
|
+
|
81
|
+
Logger.info(' - Running ', current_spec_parts.first&.first) if current_spec_parts.any?
|
82
|
+
|
83
|
+
line
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/cyrun/timing.rb
ADDED
data/lib/cyrun.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'cyrun/version'
|
4
|
+
require_relative 'cyrun/timing'
|
5
|
+
require_relative 'cyrun/logger'
|
6
|
+
require_relative 'cyrun/result_directory'
|
7
|
+
require_relative 'cyrun/group_builder'
|
8
|
+
require_relative 'cyrun/task_runner'
|
9
|
+
require_relative 'cyrun/spec_collector'
|
10
|
+
require_relative 'cyrun/spec_task'
|
11
|
+
require_relative 'cyrun/single_spec_execution_result'
|
12
|
+
require_relative 'cyrun/spec_group_execution_result'
|
13
|
+
require_relative 'cyrun/runner'
|
14
|
+
|
15
|
+
module Cyrun
|
16
|
+
end
|
data/rbs_collection.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Download sources
|
2
|
+
sources:
|
3
|
+
- name: ruby/gem_rbs_collection
|
4
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
5
|
+
revision: main
|
6
|
+
repo_dir: gems
|
7
|
+
|
8
|
+
# A directory to install the downloaded RBSs
|
9
|
+
path: .gem_rbs_collection
|
10
|
+
|
11
|
+
gems:
|
12
|
+
# Skip loading rbs gem's RBS.
|
13
|
+
# It's unnecessary if you don't use rbs as a library.
|
14
|
+
- name: rbs
|
15
|
+
ignore: true
|
data/sig/cyrun.rbs
ADDED
data/sig/logger.rbs
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Cyrun
|
2
|
+
class ResultDirectory
|
3
|
+
attr_reader spec_tasks: Array[SpecTask]
|
4
|
+
attr_reader temp_directory: String
|
5
|
+
attr_reader screenshots_pattern: String
|
6
|
+
|
7
|
+
def initialize: (String, String, Array[SpecTask]) -> void
|
8
|
+
|
9
|
+
def extract_failures_from_document: (Nokogiri::XML::Document) -> Integer
|
10
|
+
def extract_filename_from_document: (Nokogiri::XML::Document) -> String
|
11
|
+
def extract_time_in_seconds_from_document: (Nokogiri::XML::Document) -> Float
|
12
|
+
|
13
|
+
def read_result_file: (String) -> SingleSpecExecutionResult?
|
14
|
+
|
15
|
+
def read_result_files: -> Array[SingleSpecExecutionResult]
|
16
|
+
def screenshots: -> Array[String]
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def find_result_for_task: (Array[SingleSpecExecutionResult], SpecTask) -> SingleSpecExecutionResult?
|
21
|
+
def create_failed_spec_result: (SpecTask) -> SingleSpecExecutionResult
|
22
|
+
end
|
23
|
+
end
|
data/sig/runner.rbs
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Cyrun
|
2
|
+
class Runner
|
3
|
+
|
4
|
+
attr_reader collector: SpecCollector
|
5
|
+
attr_reader group_builder: GroupBuilder
|
6
|
+
attr_reader iterations: Integer?
|
7
|
+
attr_reader max_retries: Integer?
|
8
|
+
attr_reader npm_task_name: String
|
9
|
+
attr_reader overall_results: SpecGroupExecutionResult
|
10
|
+
attr_reader task_runner: TaskRunner
|
11
|
+
|
12
|
+
def initialize: (String, String, String, Integer, String, max_retries: Integer?, iterations: Integer?) -> void
|
13
|
+
|
14
|
+
def store_results: (String, String)-> bool
|
15
|
+
|
16
|
+
def run: -> SpecGroupExecutionResult
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def collect_specs: (String) -> void
|
21
|
+
|
22
|
+
def copy_result_files: (Array[SingleSpecExecutionResult], String) -> bool
|
23
|
+
def copy_screenshots: (SpecGroupExecutionResult, String) -> bool
|
24
|
+
|
25
|
+
def handle_single_execution_result: (SingleSpecExecutionResult) -> void
|
26
|
+
def log_results: (SpecGroupExecutionResult) -> void
|
27
|
+
def log_spec_result: (SingleSpecExecutionResult) -> void
|
28
|
+
|
29
|
+
def requeue_on_for_retries: (SingleSpecExecutionResult, SpecGroupExecutionResult, Integer, GroupBuilder) -> void
|
30
|
+
def requeue_for_iteration: (SingleSpecExecutionResult, SpecGroupExecutionResult, Integer, GroupBuilder) -> void
|
31
|
+
|
32
|
+
def run_next_group: -> void
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Cyrun
|
2
|
+
class SpecCollector
|
3
|
+
@base_directory: String
|
4
|
+
@files: Array[String]
|
5
|
+
@npm_task_name: String
|
6
|
+
@patterns: Array[String]
|
7
|
+
|
8
|
+
attr_reader base_directory: String
|
9
|
+
attr_reader npm_task_name: String
|
10
|
+
attr_reader patterns: String
|
11
|
+
|
12
|
+
def initialize: (base_directory: String, patterns: Array[String]) -> void
|
13
|
+
|
14
|
+
def create_task: (String) -> SpecTask
|
15
|
+
|
16
|
+
def files: -> Array[String]
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Cyrun
|
2
|
+
class SpecGroupExecutionResult
|
3
|
+
def self.create_empty: -> SpecGroupExecutionResult
|
4
|
+
|
5
|
+
attr_reader gross_time_in_seconds: Integer
|
6
|
+
attr_reader screenshots: Array[String]
|
7
|
+
attr_reader single_results: Array[SingleSpecExecutionResult]
|
8
|
+
|
9
|
+
def initialize: (Array[SingleSpecExecutionResult], Array[String], Integer) -> void
|
10
|
+
def effectiveness: -> Float
|
11
|
+
def merge!: (SpecGroupExecutionResult)-> void
|
12
|
+
def net_time_in_seconds: -> Integer
|
13
|
+
def task_fail_count: (SpecTask) -> Integer
|
14
|
+
def task_result_count: (SpecTask) -> Integer
|
15
|
+
def task_results: (SpecTask) -> Array[SingleSpecExecutionResult]
|
16
|
+
def latest_results: -> Array[SingleSpecExecutionResult]
|
17
|
+
end
|
18
|
+
end
|
data/sig/spec_task.rbs
ADDED
data/sig/task_runner.rbs
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Cyrun
|
2
|
+
class TaskRunner
|
3
|
+
CURRENT_SPEC_REGEX: Regexp
|
4
|
+
|
5
|
+
attr_reader screenshots_pattern: String
|
6
|
+
|
7
|
+
def build_command: (Array[SpecTask], String) -> Array[String]
|
8
|
+
def build_reporter_options: (String) -> Array[String]
|
9
|
+
|
10
|
+
def build_spec_arguments: (Array[SpecTask]) -> Array[String]
|
11
|
+
|
12
|
+
def create_directory: -> String
|
13
|
+
|
14
|
+
|
15
|
+
def run: (Array[SpecTask]) -> SpecGroupExecutionResult
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def execute_command: (Array[String]) -> void
|
20
|
+
def process_command_output_line: (String?) -> String
|
21
|
+
end
|
22
|
+
end
|
data/sig/timing.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cyrun
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Oskar Kirmis
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-04-24 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Run cypress specs in batches and re-run specs or test them for stability.
|
14
|
+
email:
|
15
|
+
- oskar.kirmis@posteo.de
|
16
|
+
executables:
|
17
|
+
- cyrun
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- ".standard.yml"
|
22
|
+
- Gemfile
|
23
|
+
- LICENSE.txt
|
24
|
+
- README.md
|
25
|
+
- Rakefile
|
26
|
+
- cyrun.gemspec
|
27
|
+
- exe/cyrun
|
28
|
+
- lib/cyrun.rb
|
29
|
+
- lib/cyrun/group_builder.rb
|
30
|
+
- lib/cyrun/logger.rb
|
31
|
+
- lib/cyrun/result_directory.rb
|
32
|
+
- lib/cyrun/runner.rb
|
33
|
+
- lib/cyrun/single_spec_execution_result.rb
|
34
|
+
- lib/cyrun/spec_collector.rb
|
35
|
+
- lib/cyrun/spec_group_execution_result.rb
|
36
|
+
- lib/cyrun/spec_task.rb
|
37
|
+
- lib/cyrun/task_runner.rb
|
38
|
+
- lib/cyrun/timing.rb
|
39
|
+
- lib/cyrun/version.rb
|
40
|
+
- rbs_collection.yaml
|
41
|
+
- sig/cyrun.rbs
|
42
|
+
- sig/cyrun/logging.rbs
|
43
|
+
- sig/group_builder.rbs
|
44
|
+
- sig/logger.rbs
|
45
|
+
- sig/result_directory.rbs
|
46
|
+
- sig/runner.rbs
|
47
|
+
- sig/single_spec_execution_result.rbs
|
48
|
+
- sig/spec_collector.rbs
|
49
|
+
- sig/spec_group_execution_result.rbs
|
50
|
+
- sig/spec_task.rbs
|
51
|
+
- sig/task_runner.rbs
|
52
|
+
- sig/timing.rbs
|
53
|
+
homepage: https://git.iftrue.de/okirmis/cyrun
|
54
|
+
licenses:
|
55
|
+
- MIT
|
56
|
+
metadata:
|
57
|
+
homepage_uri: https://git.iftrue.de/okirmis/cyrun
|
58
|
+
source_code_uri: https://git.iftrue.de/okirmis/cyrun
|
59
|
+
changelog_uri: https://git.iftrue.de/okirmis/cyrun
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options: []
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.6.0
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
requirements: []
|
75
|
+
rubygems_version: 3.3.13
|
76
|
+
signing_key:
|
77
|
+
specification_version: 4
|
78
|
+
summary: Run and retry cypress specs
|
79
|
+
test_files: []
|