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 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
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/testdouble/standard
3
+ ruby_version: 2.6
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
@@ -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
@@ -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
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrun
4
+ module Timing
5
+ def self.run
6
+ start_time = Time.now.to_i
7
+
8
+ yield
9
+
10
+ end_time = Time.now.to_i
11
+ end_time - start_time
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyrun
4
+ VERSION = "0.1.0"
5
+ end
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
@@ -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
@@ -0,0 +1,6 @@
1
+ module Cyrun
2
+ module Logging
3
+ def error: (*String? message) -> void
4
+ def info: (*String? message) -> void
5
+ end
6
+ end
data/sig/cyrun.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Cyrun
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
@@ -0,0 +1,12 @@
1
+ module Cyrun
2
+ class GroupBuilder
3
+ @group_size: Integer
4
+ @queue: Array[SpecTask]
5
+
6
+ def add_task: (task: SpecTask) -> void
7
+
8
+ def any?: -> bool
9
+
10
+ def next_group: -> Array[SpecTask]
11
+ end
12
+ end
data/sig/logger.rbs ADDED
@@ -0,0 +1,6 @@
1
+ module Cyrun
2
+ class Logger
3
+ def self.error: (*String? message) -> void
4
+ def self.info: (*String? message) -> void
5
+ end
6
+ end
@@ -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,10 @@
1
+ module Cyrun
2
+ class SingleSpecExecutionResult
3
+ attr_reader net_time_in_seconds: Float
4
+ attr_reader result_filename: String?
5
+ attr_reader success: bool
6
+ attr_reader task: SpecTask
7
+
8
+ def initialize: (SpecTask, String?, Float, bool) -> void
9
+ end
10
+ 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
@@ -0,0 +1,10 @@
1
+ module Cyrun
2
+ class SpecTask
3
+
4
+ attr_reader base_directory: String
5
+ attr_reader filename: String
6
+ attr_reader npm_task_name: String
7
+
8
+ def name: -> String
9
+ end
10
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ module Cyrun
2
+ module Timing
3
+ def self.run: { () -> void } -> Integer
4
+ end
5
+ end
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: []